This is an automated email from the ASF dual-hosted git repository.

twolf pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git

commit 1e27daaecc2ce3d18b06f66e67f7220dfc65102c
Author: Thomas Wolf <tw...@apache.org>
AuthorDate: Sat Feb 22 14:53:25 2025 +0100

    OpenSshCertificate: access options and extensions as maps
    
    A Map<String, String> interface is much more convenient for working with
    these options than a List<CertificateOption>. Using sorted maps also
    internally for storing these options we can automatically fulfill the
    lexicographic ordering and uniqueness requirements.
---
 .../common/config/keys/OpenSshCertificate.java     |  42 +++++-
 .../common/config/keys/OpenSshCertificateImpl.java | 142 ++++++++++++++++++++-
 .../certificate/OpenSshCertificateBuilder.java     | 108 ++++++++--------
 .../sshd/server/auth/pubkey/UserAuthPublicKey.java |   4 +-
 .../certificates/OpenSSHCertificateParserTest.java |  10 +-
 .../common/auth/PublicKeyAuthenticationTest.java   |   3 +-
 6 files changed, 235 insertions(+), 74 deletions(-)

diff --git 
a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/OpenSshCertificate.java
 
b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/OpenSshCertificate.java
index d345a3aac..f9f0cef30 100644
--- 
a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/OpenSshCertificate.java
+++ 
b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/OpenSshCertificate.java
@@ -20,11 +20,13 @@ package org.apache.sshd.common.config.keys;
 
 import java.security.PrivateKey;
 import java.security.PublicKey;
+import java.time.Instant;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
+import java.util.SortedMap;
 import java.util.concurrent.TimeUnit;
 
 import org.apache.sshd.common.util.ValidateUtils;
@@ -45,8 +47,7 @@ public interface OpenSshCertificate extends SshPublicKey, 
PrivateKey {
         /** User key certificate. */
         USER,
         /** Host key certificate. */
-        HOST,
-        ;
+        HOST;
 
         public static final List<Type> VALUES = 
Collections.unmodifiableList(Arrays.asList(values()));
 
@@ -147,17 +148,33 @@ public interface OpenSshCertificate extends SshPublicKey, 
PrivateKey {
     /**
      * Retrieves the critical options set in the certificate.
      *
-     * @return the critical options as a list, never {@code null} but possibly 
empty
+     * @return the critical options as an unmodifiable list, never {@code 
null} but possibly empty
+     * @see    #getCriticalOptionsMap()
      */
     List<CertificateOption> getCriticalOptions();
 
+    /**
+     * Retrieves the critical options set in the certificate.
+     *
+     * @return the critical options as an unmodifiable map, never {@code null} 
but possibly empty
+     */
+    SortedMap<String, String> getCriticalOptionsMap();
+
     /**
      * Retrieves the extensions set in the certificate.
      *
-     * @return the extensions as a list, never {@code null} but possibly empty
+     * @return the extensions as an unmodifiable list, never {@code null} but 
possibly empty
+     * @see    #getExtensionsMap()
      */
     List<CertificateOption> getExtensions();
 
+    /**
+     * Retrieves the extensions set in the certificate.
+     *
+     * @return the extensions as an unmodifiable map, never {@code null} but 
possibly empty
+     */
+    SortedMap<String, String> getExtensionsMap();
+
     /**
      * Retrieves the "reserved" field of the certificate. OpenSSH currently 
doesn't use it and ignores it.
      *
@@ -210,8 +227,21 @@ public interface OpenSshCertificate extends SshPublicKey, 
PrivateKey {
      */
     static boolean isValidNow(OpenSshCertificate cert) {
         long now = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis());
-        return Long.compareUnsigned(cert.getValidAfter(), now) <= 0
-                && Long.compareUnsigned(now, cert.getValidBefore()) < 0;
+        return Long.compareUnsigned(cert.getValidAfter(), now) <= 0 && 
Long.compareUnsigned(now, cert.getValidBefore()) < 0;
+    }
+
+    /**
+     * Determines whether the given {@link OpenSshCertificate} is valid at the 
given {@link Instant}.
+     *
+     * @param  cert to check
+     * @return      {@code true} if the certificate is valid according to its 
timestamps, {@code false} otherwise
+     */
+    static boolean isValidAt(OpenSshCertificate cert, Instant time) {
+        long at = time.getEpochSecond();
+        if (at < 0) {
+            return false;
+        }
+        return Long.compareUnsigned(cert.getValidAfter(), at) <= 0 && 
Long.compareUnsigned(at, cert.getValidBefore()) < 0;
     }
 
     /**
diff --git 
a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/OpenSshCertificateImpl.java
 
b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/OpenSshCertificateImpl.java
index e38ced610..f2c182b54 100644
--- 
a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/OpenSshCertificateImpl.java
+++ 
b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/OpenSshCertificateImpl.java
@@ -20,14 +20,19 @@ package org.apache.sshd.common.config.keys;
 
 import java.security.PublicKey;
 import java.time.Instant;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
 import java.util.List;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
 import java.util.concurrent.TimeUnit;
 
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.NumberUtils;
+import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
 
 /**
@@ -49,8 +54,8 @@ public class OpenSshCertificateImpl implements 
OpenSshCertificate {
     private long validAfter = OpenSshCertificate.MIN_EPOCH;
     // match ssh-keygen behavior where the default would be forever
     private long validBefore = OpenSshCertificate.INFINITY;
-    private List<CertificateOption> criticalOptions;
-    private List<CertificateOption> extensions;
+    private SortedMap<String, String> criticalOptions;
+    private SortedMap<String, String> extensions;
     private String reserved;
     private PublicKey caPubKey;
     private byte[] message;
@@ -112,12 +117,32 @@ public class OpenSshCertificateImpl implements 
OpenSshCertificate {
 
     @Override
     public List<CertificateOption> getCriticalOptions() {
-        return criticalOptions == null ? Collections.emptyList() : 
criticalOptions;
+        if (criticalOptions == null || criticalOptions.isEmpty()) {
+            return Collections.emptyList();
+        }
+        List<CertificateOption> list = new ArrayList<>(criticalOptions.size());
+        criticalOptions.forEach((k, v) -> list.add(new CertificateOption(k, 
v)));
+        return Collections.unmodifiableList(list);
+    }
+
+    @Override
+    public SortedMap<String, String> getCriticalOptionsMap() {
+        return criticalOptions == null ? Collections.emptySortedMap() : 
Collections.unmodifiableSortedMap(criticalOptions);
     }
 
     @Override
     public List<CertificateOption> getExtensions() {
-        return extensions == null ? Collections.emptyList() : extensions;
+        if (extensions == null || extensions.isEmpty()) {
+            return Collections.emptyList();
+        }
+        List<CertificateOption> list = new ArrayList<>(extensions.size());
+        extensions.forEach((k, v) -> list.add(new CertificateOption(k, v)));
+        return Collections.unmodifiableList(list);
+    }
+
+    @Override
+    public SortedMap<String, String> getExtensionsMap() {
+        return extensions == null ? Collections.emptySortedMap() : 
Collections.unmodifiableSortedMap(extensions);
     }
 
     @Override
@@ -236,12 +261,117 @@ public class OpenSshCertificateImpl implements 
OpenSshCertificate {
         }
     }
 
+    /**
+     * Sets the critical options of the certificate, overriding any options 
set earlier.
+     *
+     * @param criticalOptions to set; may be {@code null} or empty to remove 
all previously set options
+     */
     public void setCriticalOptions(List<CertificateOption> criticalOptions) {
-        this.criticalOptions = criticalOptions;
+        if (criticalOptions == null || criticalOptions.isEmpty()) {
+            this.criticalOptions = null;
+        } else {
+            this.criticalOptions = new TreeMap<>();
+            for (CertificateOption option : criticalOptions) {
+                String data = option.getData();
+                this.criticalOptions.put(option.getName(), data == null ? "" : 
data);
+            }
+        }
+    }
+
+    /**
+     * Sets the critical options of the certificate, overriding any options 
set earlier.
+     *
+     * @param criticalOptions to set; may be {@code null} or empty to remove 
all previously set options
+     */
+    public void setCriticalOptions(Map<String, String> criticalOptions) {
+        if (criticalOptions == null || criticalOptions.isEmpty()) {
+            this.criticalOptions = null;
+        } else {
+            this.criticalOptions = new TreeMap<>();
+            criticalOptions.forEach((k, v) -> {
+                String data = v == null ? "" : v;
+                this.criticalOptions.put(k, data);
+            });
+        }
+    }
+
+    /**
+     * Adds a critical option to the certificate, or removes it if {@code 
value == null}. To add an option with an empty
+     * value, use an empty string as value. If the certificate already has an 
option with the given name it is replaced.
+     *
+     * @param  name  of the option to set
+     * @param  value of the option
+     * @return       {@code true} if the map did not contain the name; {@code 
false} if it did
+     */
+    public boolean addCriticalOption(String name, String value) {
+        String key = ValidateUtils.checkNotNullAndNotEmpty(name, "Critical 
option name must be set");
+        if (value == null) {
+            if (criticalOptions == null) {
+                return true;
+            }
+            return criticalOptions.remove(key) == null;
+        }
+        if (criticalOptions == null) {
+            criticalOptions = new TreeMap<>();
+        }
+        return criticalOptions.put(key, value) == null;
     }
 
+    /**
+     * Sets the extensions of the certificate, overriding any extensions set 
earlier.
+     *
+     * @param extensions to set; may be {@code null} or empty to remove all 
previously set extensions
+     */
     public void setExtensions(List<CertificateOption> extensions) {
-        this.extensions = extensions;
+        if (extensions == null || extensions.isEmpty()) {
+            this.extensions = null;
+        } else {
+            this.extensions = new TreeMap<>();
+            for (CertificateOption option : extensions) {
+                String data = option.getData();
+                this.extensions.put(option.getName(), data == null ? "" : 
data);
+            }
+        }
+    }
+
+    /**
+     * Sets the extensions of the certificate, overriding any extensions set 
earlier.
+     *
+     * @param extensions to set; may be {@code null} or empty to remove all 
previously set extensions
+     */
+    public void setExtensions(Map<String, String> extensions) {
+        if (extensions == null || extensions.isEmpty()) {
+            this.extensions = null;
+        } else {
+            this.extensions = new TreeMap<>();
+            extensions.forEach((k, v) -> {
+                String data = v == null ? "" : v;
+                this.extensions.put(k, data);
+            });
+        }
+    }
+
+    /**
+     * Adds an extension to the certificate, or removes it if {@code value == 
null}. To add an extension with an empty
+     * value, use an empty string as value. If the certificate already has an 
extension with the given name it is
+     * replaced.
+     *
+     * @param  name  of the extension to set
+     * @param  value of the extension
+     * @return       {@code true} if the map did not contain the name; {@code 
false} if it did
+     */
+    public boolean addExtension(String name, String value) {
+        String key = ValidateUtils.checkNotNullAndNotEmpty(name, "Extension 
name must be set");
+        if (value == null) {
+            if (extensions == null) {
+                return true;
+            }
+            return extensions.remove(key) == null;
+        }
+        if (extensions == null) {
+            extensions = new TreeMap<>();
+        }
+        return extensions.put(key, value) == null;
     }
 
     public void setReserved(String reserved) {
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/certificate/OpenSshCertificateBuilder.java
 
b/sshd-core/src/main/java/org/apache/sshd/certificate/OpenSshCertificateBuilder.java
index e8e6597e3..5d5f51ed8 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/certificate/OpenSshCertificateBuilder.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/certificate/OpenSshCertificateBuilder.java
@@ -25,18 +25,15 @@ import java.security.SecureRandom;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashSet;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
 
 import org.apache.sshd.common.BaseBuilder;
 import org.apache.sshd.common.NamedFactory;
 import org.apache.sshd.common.config.keys.KeyUtils;
 import org.apache.sshd.common.config.keys.OpenSshCertificate;
+import org.apache.sshd.common.config.keys.OpenSshCertificate.CertificateOption;
 import org.apache.sshd.common.config.keys.OpenSshCertificate.Type;
 import org.apache.sshd.common.config.keys.OpenSshCertificateImpl;
 import org.apache.sshd.common.keyprovider.KeyPairProvider;
@@ -68,10 +65,8 @@ public class OpenSshCertificateBuilder {
     protected long serial;
     protected String id;
     protected Collection<String> principals;
-    // criticalOptions and extensions must be lexically ordered by "name" if 
they appear in the
-    // sequence. Each named option may only appear once in a certificate.
-    protected List<OpenSshCertificate.CertificateOption> criticalOptions;
-    protected List<OpenSshCertificate.CertificateOption> extensions;
+    protected final Map<String, String> criticalOptions = new HashMap<>();
+    protected final Map<String, String> extensions = new HashMap<>();
     // match ssh-keygen behavior where the default would be forever
     protected long validAfter = OpenSshCertificate.MIN_EPOCH;
     // match ssh-keygen behavior where the default would be forever
@@ -111,15 +106,59 @@ public class OpenSshCertificateBuilder {
         return this;
     }
 
+    public OpenSshCertificateBuilder criticalOption(String name, String data) {
+        String key = ValidateUtils.checkNotNullAndNotEmpty(name, "Critical 
option name must be set");
+        this.criticalOptions.put(key, data == null ? "" : data);
+        return this;
+    }
+
     public OpenSshCertificateBuilder 
criticalOptions(List<OpenSshCertificate.CertificateOption> criticalOptions) {
-        validateOptions(criticalOptions);
-        this.criticalOptions = lexicallyOrderOptions(criticalOptions);
+        this.criticalOptions.clear();
+        for (CertificateOption option : criticalOptions) {
+            String key = 
ValidateUtils.checkNotNullAndNotEmpty(option.getName(), "Critical option name 
must be set");
+            String value = option.getData();
+            String prev = this.criticalOptions.put(key, value == null ? "" : 
value);
+            if (prev != null) {
+                throw new IllegalArgumentException("Duplicate certificate 
option " + key);
+            }
+        }
+        return this;
+    }
+
+    public OpenSshCertificateBuilder criticalOptions(Map<String, String> 
criticalOptions) {
+        this.criticalOptions.clear();
+        criticalOptions.forEach((k, v) -> {
+            String key = ValidateUtils.checkNotNullAndNotEmpty(k, "Critical 
option name must be set");
+            this.criticalOptions.put(key, v == null ? "" : v);
+        });
+        return this;
+    }
+
+    public OpenSshCertificateBuilder extension(String name, String data) {
+        String key = ValidateUtils.checkNotNullAndNotEmpty(name, "Extension 
name must be set");
+        this.extensions.put(key, data == null ? "" : data);
         return this;
     }
 
     public OpenSshCertificateBuilder 
extensions(List<OpenSshCertificate.CertificateOption> extensions) {
-        validateOptions(extensions);
-        this.extensions = lexicallyOrderOptions(extensions);
+        this.extensions.clear();
+        for (CertificateOption option : extensions) {
+            String key = 
ValidateUtils.checkNotNullAndNotEmpty(option.getName(), "Extension name must be 
set");
+            String value = option.getData();
+            String prev = this.extensions.put(key, value == null ? "" : value);
+            if (prev != null) {
+                throw new IllegalArgumentException("Duplicate certificate 
extension " + key);
+            }
+        }
+        return this;
+    }
+
+    public OpenSshCertificateBuilder extensions(Map<String, String> 
extensions) {
+        this.extensions.clear();
+        extensions.forEach((k, v) -> {
+            String key = ValidateUtils.checkNotNullAndNotEmpty(k, "Extension 
name must be set");
+            this.extensions.put(key, v == null ? "" : v);
+        });
         return this;
     }
 
@@ -228,11 +267,11 @@ public class OpenSshCertificateBuilder {
         if (principals != null && !principals.isEmpty()) {
             cert.setPrincipals(new ArrayList<>(principals));
         }
-        if (criticalOptions != null && !criticalOptions.isEmpty()) {
-            cert.setCriticalOptions(new ArrayList<>(criticalOptions));
+        if (!criticalOptions.isEmpty()) {
+            cert.setCriticalOptions(criticalOptions);
         }
-        if (extensions != null && !extensions.isEmpty()) {
-            cert.setExtensions(new ArrayList<>(extensions));
+        if (!extensions.isEmpty()) {
+            cert.setExtensions(extensions);
         }
         cert.setValidAfter(validAfter);
         cert.setValidBefore(validBefore);
@@ -277,39 +316,4 @@ public class OpenSshCertificateBuilder {
 
         return cert;
     }
-
-    /**
-     * Validates that there are no duplicate options.
-     *
-     * @param  options                  the options to check
-     * @throws IllegalArgumentException if there are duplicates
-     */
-    private void validateOptions(List<OpenSshCertificate.CertificateOption> 
options) {
-        if (options != null && !options.isEmpty()) {
-            // check if any duplicates
-            Set<String> names = new HashSet<>();
-            Set<String> duplicates = options.stream().filter(option -> 
!names.add(option.getName()))
-                    .map(OpenSshCertificate.CertificateOption::getName)
-                    .collect(Collectors.toSet());
-            if (!duplicates.isEmpty()) {
-                throw new IllegalArgumentException("Duplicate option: " + 
duplicates);
-            }
-        }
-    }
-
-    /**
-     * Lexically orders certificate options by name.
-     *
-     * @param  options the options to order
-     * @return         a list containing the options in lexical order
-     */
-    private List<OpenSshCertificate.CertificateOption> lexicallyOrderOptions(
-            List<OpenSshCertificate.CertificateOption> options) {
-        if (options != null && !options.isEmpty()) {
-            return options.stream()
-                    
.sorted(Comparator.comparing(OpenSshCertificate.CertificateOption::getName))
-                    .collect(Collectors.toList());
-        }
-        return Collections.emptyList();
-    }
 }
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/server/auth/pubkey/UserAuthPublicKey.java
 
b/sshd-core/src/main/java/org/apache/sshd/server/auth/pubkey/UserAuthPublicKey.java
index 3280170de..d16534373 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/server/auth/pubkey/UserAuthPublicKey.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/server/auth/pubkey/UserAuthPublicKey.java
@@ -33,7 +33,6 @@ import org.apache.sshd.common.RuntimeSshException;
 import org.apache.sshd.common.SshConstants;
 import org.apache.sshd.common.config.keys.KeyUtils;
 import org.apache.sshd.common.config.keys.OpenSshCertificate;
-import org.apache.sshd.common.config.keys.OpenSshCertificate.CertificateOption;
 import org.apache.sshd.common.net.InetAddressRange;
 import org.apache.sshd.common.signature.Signature;
 import org.apache.sshd.common.signature.SignatureFactoriesManager;
@@ -204,8 +203,7 @@ public class UserAuthPublicKey extends AbstractUserAuth 
implements SignatureFact
     }
 
     protected void verifyCertificateSources(ServerSession session, 
OpenSshCertificate cert) throws CertificateException {
-        String allowedSources = cert.getCriticalOptions().stream().filter(c -> 
"source-address".equals(c.getName()))
-                .map(CertificateOption::getData).findAny().orElse(null);
+        String allowedSources = 
cert.getCriticalOptionsMap().get("source-address");
         if (allowedSources == null) {
             return;
         }
diff --git 
a/sshd-core/src/test/java/org/apache/sshd/certificates/OpenSSHCertificateParserTest.java
 
b/sshd-core/src/test/java/org/apache/sshd/certificates/OpenSSHCertificateParserTest.java
index a2e317d11..9c69e7816 100644
--- 
a/sshd-core/src/test/java/org/apache/sshd/certificates/OpenSSHCertificateParserTest.java
+++ 
b/sshd-core/src/test/java/org/apache/sshd/certificates/OpenSSHCertificateParserTest.java
@@ -98,11 +98,11 @@ public class OpenSSHCertificateParserTest extends 
BaseTestSupport {
             assertTrue(typedCert.getCriticalOptions().isEmpty());
             assertEquals(
                     Arrays.asList(
-                            new 
OpenSshCertificate.CertificateOption("permit-X11-forwarding"),
-                            new 
OpenSshCertificate.CertificateOption("permit-agent-forwarding"),
-                            new 
OpenSshCertificate.CertificateOption("permit-port-forwarding"),
-                            new 
OpenSshCertificate.CertificateOption("permit-pty"),
-                            new 
OpenSshCertificate.CertificateOption("permit-user-rc")),
+                            new 
OpenSshCertificate.CertificateOption("permit-X11-forwarding", ""),
+                            new 
OpenSshCertificate.CertificateOption("permit-agent-forwarding", ""),
+                            new 
OpenSshCertificate.CertificateOption("permit-port-forwarding", ""),
+                            new 
OpenSshCertificate.CertificateOption("permit-pty", ""),
+                            new 
OpenSshCertificate.CertificateOption("permit-user-rc", "")),
                     typedCert.getExtensions());
             assertEquals(params.sigAlgorithm, 
typedCert.getSignatureAlgorithm());
             verifySignature(typedCert);
diff --git 
a/sshd-core/src/test/java/org/apache/sshd/common/auth/PublicKeyAuthenticationTest.java
 
b/sshd-core/src/test/java/org/apache/sshd/common/auth/PublicKeyAuthenticationTest.java
index 67a7d5949..7d8a2db04 100644
--- 
a/sshd-core/src/test/java/org/apache/sshd/common/auth/PublicKeyAuthenticationTest.java
+++ 
b/sshd-core/src/test/java/org/apache/sshd/common/auth/PublicKeyAuthenticationTest.java
@@ -49,7 +49,6 @@ import org.apache.sshd.common.SshException;
 import org.apache.sshd.common.config.keys.FilePasswordProvider;
 import org.apache.sshd.common.config.keys.KeyUtils;
 import org.apache.sshd.common.config.keys.OpenSshCertificate;
-import org.apache.sshd.common.config.keys.OpenSshCertificate.CertificateOption;
 import org.apache.sshd.common.config.keys.OpenSshCertificateImpl;
 import org.apache.sshd.common.keyprovider.KeyIdentityProvider;
 import org.apache.sshd.common.keyprovider.KeyPairProvider;
@@ -593,7 +592,7 @@ public class PublicKeyAuthenticationTest extends 
AuthenticationTestSupport {
                 .id("test-cert-ecdsa-sha2-nistp256") //
                 .validBefore(System.currentTimeMillis() + 
TimeUnit.HOURS.toMillis(1)) //
                 .principals(Collections.singletonList("user01")) //
-                .criticalOptions(Collections.singletonList(new 
CertificateOption("source-address", sources)))
+                .criticalOption("source-address", sources)
                 .sign(caKeypair, "ecdsa-sha2-nistp256");
 
         // Configure the ssh server

Reply via email to