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

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

commit 333c3c04bb34910b804f87233cc8e4b9a4583ba4
Author: Thomas Wolf <[email protected]>
AuthorDate: Thu Jun 4 13:02:51 2026 +0200

    Fix server-side user certificate handling
    
    Complete checking critical options present in a certificate, and add a
    unit test.
---
 .../common/config/keys/OpenSshCertificate.java     |  16 ++
 .../sshd/server/auth/pubkey/UserAuthPublicKey.java |  22 +-
 .../auth/UserAuthPublicKeyCertificateTest.java     | 234 +++++++++++++++++++++
 3 files changed, 270 insertions(+), 2 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 b18cb5d01..1691b2614 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
@@ -76,6 +76,22 @@ public interface OpenSshCertificate extends SshPublicKey, 
PrivateKey {
      */
     long INFINITY = 0xffff_ffff_ffff_ffffL;
 
+    /**
+     * Critical option. If present, a signature made with the certified key 
must include the corresponding assertion.
+     */
+    String VERIFY_REQUIRED = "verify-required";
+
+    /**
+     * Critical option: if present a string value giving the forced command 
for "exec" or "shell" channel sessions, or
+     * for subsystems.
+     */
+    String FORCE_COMMAND = "force-command";
+
+    /**
+     * Critical option. Comma-separated list of CIDRs for allowed peers.
+     */
+    String SOURCE_ADDRESS = "source-address";
+
     /**
      * Retrieves the raw SSH key type of this certificate.
      *
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 a1f07629d..9b4b4b40d 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
@@ -26,6 +26,7 @@ import java.security.SignatureException;
 import java.security.cert.CertificateException;
 import java.util.Collection;
 import java.util.List;
+import java.util.Map;
 
 import org.apache.sshd.common.NamedFactory;
 import org.apache.sshd.common.NamedResource;
@@ -110,7 +111,7 @@ public class UserAuthPublicKey extends AbstractUserAuth 
implements SignatureFact
                     throw new CertificateException("expired");
                 }
                 verifyCertificateSignature(session, cert);
-                verifyCertificateSources(session, cert);
+                verifyCriticalOptions(session, cert);
             } catch (Exception e) {
                 warn("doAuth({}@{}): public key certificate (id={}) is not 
valid: {}", username, session, cert.getId(),
                         e.getMessage(), e);
@@ -217,8 +218,25 @@ public class UserAuthPublicKey extends AbstractUserAuth 
implements SignatureFact
         }
     }
 
+    protected void verifyCriticalOptions(ServerSession session, 
OpenSshCertificate cert) throws CertificateException {
+        Map<String, String> criticalOptions = cert.getCriticalOptionsMap();
+        for (String option : criticalOptions.keySet()) {
+            if (OpenSshCertificate.VERIFY_REQUIRED.equals(option)) {
+                throw new CertificateException(
+                        "Rejected by verify-required critical option: not 
implemented yet, have certificate type "
+                                               + cert.getKeyType());
+            } else if (OpenSshCertificate.SOURCE_ADDRESS.equals(option)) {
+                verifyCertificateSources(session, cert);
+            } else if (OpenSshCertificate.FORCE_COMMAND.equals(option)) {
+                throw new CertificateException("Rejected by force-command 
critical option: not implemented yet");
+            } else {
+                throw new CertificateException("Rejected by unknown critical 
option " + option);
+            }
+        }
+    }
+
     protected void verifyCertificateSources(ServerSession session, 
OpenSshCertificate cert) throws CertificateException {
-        String allowedSources = 
cert.getCriticalOptionsMap().get("source-address");
+        String allowedSources = 
cert.getCriticalOptionsMap().get(OpenSshCertificate.SOURCE_ADDRESS);
         if (allowedSources == null) {
             return;
         }
diff --git 
a/sshd-core/src/test/java/org/apache/sshd/server/auth/UserAuthPublicKeyCertificateTest.java
 
b/sshd-core/src/test/java/org/apache/sshd/server/auth/UserAuthPublicKeyCertificateTest.java
new file mode 100644
index 000000000..066e15518
--- /dev/null
+++ 
b/sshd-core/src/test/java/org/apache/sshd/server/auth/UserAuthPublicKeyCertificateTest.java
@@ -0,0 +1,234 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sshd.server.auth;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.security.SignatureException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.concurrent.ThreadLocalRandom;
+
+import org.apache.sshd.certificate.OpenSshCertificateBuilder;
+import org.apache.sshd.common.Factory;
+import org.apache.sshd.common.SshConstants;
+import org.apache.sshd.common.auth.AbstractUserAuthServiceFactory;
+import org.apache.sshd.common.config.keys.OpenSshCertificate;
+import org.apache.sshd.common.io.IoSession;
+import org.apache.sshd.common.random.JceRandomFactory;
+import org.apache.sshd.common.random.Random;
+import org.apache.sshd.common.random.SingletonRandomFactory;
+import org.apache.sshd.common.signature.BuiltinSignatures;
+import org.apache.sshd.common.signature.Signature;
+import org.apache.sshd.common.util.buffer.BufferUtils;
+import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
+import org.apache.sshd.common.util.security.SecurityUtils;
+import org.apache.sshd.server.ServerFactoryManager;
+import org.apache.sshd.server.auth.pubkey.AcceptAllPublickeyAuthenticator;
+import org.apache.sshd.server.auth.pubkey.UserAuthPublicKey;
+import org.apache.sshd.server.session.ServerSessionImpl;
+import org.apache.sshd.util.test.BaseTestSupport;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.mockito.Mockito;
+
+/**
+ * Unit test for {@link UserAuthPublickey} handling certificates on the 
server-side.
+ */
+@Tag("NoIoTestCase")
+class UserAuthPublicKeyCertificateTest extends BaseTestSupport {
+
+    @ParameterizedTest(name = "auth {0} {1}")
+    @CsvSource({ //
+            ",,true", //
+            "source-address,10.1.2.3/32,true", //
+            "source-address,10.1/16,true", //
+            "source-address,'10.2/16,10.1/16', true", //
+            "source-address,10.2/16,false", //
+            "source-address,10.1.2.1/32,false", //
+            "source-address,172.1/16,false", //
+            "verify-required,,false", //
+            "verify-required,bogus,false", //
+            "force-command,,false", //
+            "force-command,exit,false", //
+            "unknown-option,,false", //
+            "unknown-option,unknown,false" //
+    })
+    void testCertOptions(String criticalOption, String optionValue, boolean 
expectSuccess) throws Exception {
+        KeyPairGenerator generator = SecurityUtils.getKeyPairGenerator("EC");
+        generator.initialize(256);
+        KeyPair pair = generator.generateKeyPair();
+
+        // Create a CA key
+        KeyPair caKeypair = generator.generateKeyPair();
+        // Create a certificate for the above key
+        OpenSshCertificateBuilder builder = 
OpenSshCertificateBuilder.userCertificate() //
+                .serial(0L) //
+                .publicKey(pair.getPublic()) //
+                .id("testuser") //
+                .principals(Collections.singletonList("testuser"));
+        if (criticalOption != null && !criticalOption.isEmpty()) {
+            builder = builder.criticalOption(criticalOption, optionValue);
+        }
+        OpenSshCertificate cert = builder.sign(caKeypair);
+
+        MockSession session = createSession();
+        // Give it a session ID since it is part of the signed data.
+        byte[] id = new byte[32];
+        ThreadLocalRandom.current().nextBytes(id);
+        session.setSessionId(id);
+        // We don't care about the authenticator in this test.
+        
session.setPublickeyAuthenticator(AcceptAllPublickeyAuthenticator.INSTANCE);
+        // Create the UserAuthPublickey object under test; give it the 
signature factories we need.
+        UserAuthPublicKey pubkeyAuth = new UserAuthPublicKey(
+                Arrays.asList(BuiltinSignatures.nistp256_cert, 
BuiltinSignatures.nistp256));
+
+        // Create a buffer with a full properly signed authentication request.
+        ByteArrayBuffer buffer = createRequest(id, cert, pair.getPrivate());
+
+        String userName = buffer.getString();
+        String serviceName = buffer.getString();
+        buffer.getString(); // Skip method name
+
+        // Finally try to authenticate and check the result.
+        try {
+            Boolean result = pubkeyAuth.auth(session, userName, serviceName, 
buffer);
+            assertEquals(expectSuccess, result.booleanValue());
+        } catch (SignatureException e) {
+            if (!"Key verification failed".equals(e.getMessage())) {
+                throw e;
+            }
+            assertFalse(expectSuccess);
+        }
+    }
+
+    @ParameterizedTest(name = "auth {0} {1}")
+    @CsvSource({ //
+            ",,true", //
+            "verify-required,,false", //
+            "verify-required,bogus,false", //
+            "force-command,,false", //
+            "force-command,exit,false", //
+            "unknown-option,,false", //
+            "unknown-option,unknown,false" //
+    })
+    void testMultipleCertOptions(String criticalOption, String optionValue, 
boolean expectSuccess) throws Exception {
+        KeyPairGenerator generator = SecurityUtils.getKeyPairGenerator("EC");
+        generator.initialize(256);
+        KeyPair pair = generator.generateKeyPair();
+
+        // Create a CA key
+        KeyPair caKeypair = generator.generateKeyPair();
+        // Create a certificate for the above key
+        OpenSshCertificateBuilder builder = 
OpenSshCertificateBuilder.userCertificate() //
+                .serial(0L) //
+                .publicKey(pair.getPublic()) //
+                .id("testuser") //
+                .principals(Collections.singletonList("testuser"));
+        builder.criticalOption(OpenSshCertificate.SOURCE_ADDRESS, 
"10.1.2.3/24");
+        if (criticalOption != null && !criticalOption.isEmpty()) {
+            builder = builder.criticalOption(criticalOption, optionValue);
+        }
+        OpenSshCertificate cert = builder.sign(caKeypair);
+
+        MockSession session = createSession();
+        // Give it a session ID since it is part of the signed data.
+        byte[] id = new byte[32];
+        ThreadLocalRandom.current().nextBytes(id);
+        session.setSessionId(id);
+        // We don't care about the authenticator in this test.
+        
session.setPublickeyAuthenticator(AcceptAllPublickeyAuthenticator.INSTANCE);
+        // Create the UserAuthPublickey object under test; give it the 
signature factories we need.
+        UserAuthPublicKey pubkeyAuth = new UserAuthPublicKey(
+                Arrays.asList(BuiltinSignatures.nistp256_cert, 
BuiltinSignatures.nistp256));
+
+        // Create a buffer with a full properly signed authentication request.
+        ByteArrayBuffer buffer = createRequest(id, cert, pair.getPrivate());
+
+        String userName = buffer.getString();
+        String serviceName = buffer.getString();
+        buffer.getString(); // Skip method name
+
+        // Finally try to authenticate and check the result.
+        try {
+            Boolean result = pubkeyAuth.auth(session, userName, serviceName, 
buffer);
+            assertEquals(expectSuccess, result.booleanValue());
+        } catch (SignatureException e) {
+            if (!"Key verification failed".equals(e.getMessage())) {
+                throw e;
+            }
+            assertFalse(expectSuccess);
+        }
+    }
+
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    private MockSession createSession() throws Exception {
+        // Create a mock ServerSession. We can't simply mock the server 
session since the authenticator might want to
+        // set an attribute on it.
+        ServerFactoryManager manager = 
Mockito.mock(ServerFactoryManager.class);
+        Factory<? extends Random> randomFactory = new 
SingletonRandomFactory(JceRandomFactory.INSTANCE);
+        Mockito.when(manager.getRandomFactory()).thenReturn((Factory) 
randomFactory);
+        IoSession ioSession = Mockito.mock(IoSession.class);
+        InetSocketAddress fakeClientAddress = new 
InetSocketAddress(InetAddress.getByAddress(new byte[] { 10, 1, 2, 3 }), 1234);
+        
Mockito.when(ioSession.getRemoteAddress()).thenReturn(fakeClientAddress);
+        return new MockSession(manager, ioSession);
+    }
+
+    private ByteArrayBuffer createRequest(byte[] sessionId, OpenSshCertificate 
cert, PrivateKey priv)
+            throws Exception {
+        ByteArrayBuffer payload = new ByteArrayBuffer();
+        payload.putString("testuser");
+        payload.putString(AbstractUserAuthServiceFactory.DEFAULT_NAME);
+        payload.putString(UserAuthPublicKey.NAME);
+        payload.putBoolean(true); // With signature
+        payload.putString(cert.getKeyType()); // Algorithm
+        payload.putPublicKey(cert);
+
+        Signature signer = BuiltinSignatures.nistp256_cert.create();
+        signer.initSigner(null, priv);
+        byte[] uint = new byte[4];
+        BufferUtils.putUInt(sessionId.length, uint);
+        signer.update(null, uint);
+        signer.update(null, sessionId);
+        signer.update(null, new byte[] { SshConstants.SSH_MSG_USERAUTH_REQUEST 
});
+        signer.update(null, payload.getCompactData());
+        byte[] rawSignature = signer.sign(null);
+
+        ByteArrayBuffer sig = new ByteArrayBuffer();
+        sig.putString(cert.getRawKeyType());
+        sig.putBytes(rawSignature);
+        payload.putBytes(sig.getCompactData());
+        return payload;
+    }
+
+    private static class MockSession extends ServerSessionImpl {
+
+        MockSession(ServerFactoryManager server, IoSession ioSession) throws 
Exception {
+            super(server, ioSession);
+        }
+
+        void setSessionId(byte[] id) {
+            sessionId = id.clone();
+        }
+    }
+}

Reply via email to