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

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

commit 990e057e26dc602e9887e2df53bc87c7b316a571
Author: Thomas Wolf <[email protected]>
AuthorDate: Thu Sep 17 23:54:33 2026 +0200

    Multi-pubkey auth: enforce different keys on the server side
    
    A "publickey,publickey" authentication policy must not be satisfied by
    presenting the same key twice. Remember authenticated keys in an
    attribute on the SSH ServerSession. Refuse to authenticate a key
    multiple times.
---
 .../sshd/server/auth/pubkey/UserAuthPublicKey.java |  25 +++++
 .../server/auth/CertificateReuseMultiAuthTest.java | 112 +++++++++++++++++++
 .../sshd/server/auth/PublicKeyMultiAuthTest.java   | 124 +++++++++++++++++++++
 3 files changed, 261 insertions(+)

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 9b4b4b40d..18cd5990b 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
@@ -24,10 +24,12 @@ import java.net.SocketAddress;
 import java.security.PublicKey;
 import java.security.SignatureException;
 import java.security.cert.CertificateException;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 
+import org.apache.sshd.common.AttributeRepository;
 import org.apache.sshd.common.NamedFactory;
 import org.apache.sshd.common.NamedResource;
 import org.apache.sshd.common.RuntimeSshException;
@@ -56,6 +58,9 @@ import org.apache.sshd.server.session.ServerSession;
 public class UserAuthPublicKey extends AbstractUserAuth implements 
SignatureFactoriesManager {
     public static final String NAME = UserAuthPublicKeyFactory.NAME;
 
+    public static final AttributeRepository.AttributeKey<List<PublicKey>> 
AUTHENTICATED_KEYS
+            = new AttributeRepository.AttributeKey<>();
+
     private List<NamedFactory<Signature>> factories;
 
     public UserAuthPublicKey() {
@@ -99,6 +104,7 @@ public class UserAuthPublicKey extends AbstractUserAuth 
implements SignatureFact
         buffer.wpos(buffer.rpos() + len);
 
         PublicKey key = buffer.getRawPublicKey();
+
         PublicKey verifyKey = key;
 
         if (key instanceof OpenSshCertificate) {
@@ -121,6 +127,11 @@ public class UserAuthPublicKey extends AbstractUserAuth 
implements SignatureFact
             verifyKey = cert.getCertPubKey();
         }
 
+        List<PublicKey> previouslyAuthenticatedKeys = 
session.getAttribute(AUTHENTICATED_KEYS);
+        if (alreadyAuthenticated(verifyKey, previouslyAuthenticatedKeys)) {
+            return Boolean.FALSE;
+        }
+
         Collection<NamedFactory<Signature>> factories = 
ValidateUtils.checkNotNullAndNotEmpty(
                 SignatureFactoriesManager.resolveSignatureFactories(this, 
session),
                 "No signature factories for session=%s",
@@ -173,6 +184,7 @@ public class UserAuthPublicKey extends AbstractUserAuth 
implements SignatureFact
             return null;
         }
 
+        PublicKey presentedKey = verifyKey;
         if (verifyKey instanceof SecurityKeyPublicKey<?>) {
             AuthorizedKeyEntry entry = 
session.getAttribute(AuthorizedKeyEntriesPublickeyAuthenticator.AUTHORIZED_KEY);
             if (entry != null) {
@@ -188,6 +200,12 @@ public class UserAuthPublicKey extends AbstractUserAuth 
implements SignatureFact
             throw new SignatureException("Key verification failed");
         }
 
+        if (previouslyAuthenticatedKeys == null) {
+            previouslyAuthenticatedKeys = new ArrayList<>();
+            session.setAttribute(AUTHENTICATED_KEYS, 
previouslyAuthenticatedKeys);
+        }
+        previouslyAuthenticatedKeys.add(presentedKey);
+
         if (debugEnabled) {
             log.debug("doAuth({}@{}) key type={}, fingerprint={} - verified",
                     username, session, alg, KeyUtils.getFingerPrint(key));
@@ -196,6 +214,13 @@ public class UserAuthPublicKey extends AbstractUserAuth 
implements SignatureFact
         return Boolean.TRUE;
     }
 
+    protected boolean alreadyAuthenticated(PublicKey key, List<PublicKey> 
previouslyAuthenticatedKeys) {
+        if (GenericUtils.isEmpty(previouslyAuthenticatedKeys)) {
+            return false;
+        }
+        return previouslyAuthenticatedKeys.stream().anyMatch(k -> 
KeyUtils.compareKeys(key, k));
+    }
+
     protected void verifyCertificateSignature(ServerSession session, 
OpenSshCertificate cert) throws Exception {
         PublicKey signatureKey = cert.getCaPubKey();
         String keyAlg = KeyUtils.getKeyType(signatureKey);
diff --git 
a/sshd-core/src/test/java/org/apache/sshd/server/auth/CertificateReuseMultiAuthTest.java
 
b/sshd-core/src/test/java/org/apache/sshd/server/auth/CertificateReuseMultiAuthTest.java
new file mode 100644
index 000000000..19eba24be
--- /dev/null
+++ 
b/sshd-core/src/test/java/org/apache/sshd/server/auth/CertificateReuseMultiAuthTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PublicKey;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.sshd.certificate.OpenSshCertificateBuilder;
+import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.SshException;
+import org.apache.sshd.common.auth.UserAuthMethodFactory;
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.config.keys.OpenSshCertificate;
+import org.apache.sshd.common.config.keys.PublicKeyEntry;
+import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
+import org.apache.sshd.core.CoreModuleProperties;
+import org.apache.sshd.server.SshServer;
+import 
org.apache.sshd.server.auth.pubkey.AuthorizedKeyEntriesPublickeyAuthenticator;
+import org.apache.sshd.util.test.BaseTestSupport;
+import org.apache.sshd.util.test.CoreTestSupportUtils;
+import org.junit.jupiter.api.Test;
+
+class CertificateReuseMultiAuthTest extends BaseTestSupport {
+
+    @Test
+    void twoCertificatesForOnePrivateKeyMustNotSatisfyTwoPublicKeySteps() 
throws Exception {
+        KeyPairGenerator generator = 
KeyPairGenerator.getInstance(KeyUtils.RSA_ALGORITHM);
+        generator.initialize(2048);
+        KeyPair ca = generator.generateKeyPair();
+        KeyPair user = generator.generateKeyPair();
+        KeyPair independentUser = generator.generateKeyPair();
+        String username = "single-key-user";
+        long expiry = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1);
+
+        OpenSshCertificate cert1 = OpenSshCertificateBuilder.userCertificate()
+                .serial(1L).publicKey(user.getPublic()).id("first")
+                
.validBefore(expiry).principals(Collections.singletonList(username))
+                .sign(ca, "rsa-sha2-256");
+        OpenSshCertificate cert2 = OpenSshCertificateBuilder.userCertificate()
+                .serial(2L).publicKey(user.getPublic()).id("second")
+                
.validBefore(expiry).principals(Collections.singletonList(username))
+                .sign(ca, "rsa-sha2-256");
+        OpenSshCertificate independentCert = 
OpenSshCertificateBuilder.userCertificate()
+                
.serial(3L).publicKey(independentUser.getPublic()).id("independent")
+                
.validBefore(expiry).principals(Collections.singletonList(username))
+                .sign(ca, "rsa-sha2-256");
+        assertFalse(KeyUtils.compareKeys((PublicKey) cert1, (PublicKey) 
cert2));
+        assertTrue(KeyUtils.compareKeys(cert1.getCertPubKey(), 
cert2.getCertPubKey()));
+
+        SshServer sshd = 
CoreTestSupportUtils.setupTestServer(CertificateReuseMultiAuthTest.class);
+        SshClient client = 
CoreTestSupportUtils.setupTestClient(CertificateReuseMultiAuthTest.class);
+        try {
+            CoreTestSupportUtils.setupFullSignaturesSupport(sshd);
+            CoreTestSupportUtils.setupFullSignaturesSupport(client);
+            AuthorizedKeyEntry caEntry = 
AuthorizedKeyEntry.parseAuthorizedKeyEntry(
+                    "cert-authority " + 
PublicKeyEntry.toString(ca.getPublic()));
+            sshd.setPublickeyAuthenticator(new 
AuthorizedKeyEntriesPublickeyAuthenticator(
+                    "test-ca", null, Collections.singletonList(caEntry), 
PublicKeyEntryResolver.FAILING));
+            CoreModuleProperties.AUTH_METHODS.set(sshd, "publickey,publickey");
+            sshd.start();
+            client.setUserAuthFactoriesNames(UserAuthMethodFactory.PUBLIC_KEY);
+            client.start();
+
+            try (ClientSession session = createClientSession(username, client, 
sshd.getPort())) {
+                session.setKeyIdentityProvider(context -> 
Collections.singletonList(
+                        new KeyPair(cert1, user.getPrivate())));
+                assertThrows(SshException.class, () -> 
session.auth().verify(AUTH_TIMEOUT),
+                        "one certificate alone should not meet a two-key 
policy");
+                assertFalse(session.isAuthenticated());
+            }
+            try (ClientSession session = createClientSession(username, client, 
sshd.getPort())) {
+                session.setKeyIdentityProvider(context -> Arrays.asList(
+                        new KeyPair(cert1, user.getPrivate()),
+                        new KeyPair(independentCert, 
independentUser.getPrivate())));
+                session.auth().verify(AUTH_TIMEOUT);
+                assertTrue(session.isAuthenticated(), "two independent private 
keys should authenticate");
+            }
+            try (ClientSession session = createClientSession(username, client, 
sshd.getPort())) {
+                session.setKeyIdentityProvider(context -> Arrays.asList(
+                        new KeyPair(cert1, user.getPrivate()),
+                        new KeyPair(cert2, user.getPrivate())));
+                assertThrows(SshException.class, () -> 
session.auth().verify(AUTH_TIMEOUT),
+                        "two certificates for the same private key must not 
count as two keys");
+            }
+        } finally {
+            client.stop();
+            sshd.stop(true);
+        }
+    }
+}
diff --git 
a/sshd-core/src/test/java/org/apache/sshd/server/auth/PublicKeyMultiAuthTest.java
 
b/sshd-core/src/test/java/org/apache/sshd/server/auth/PublicKeyMultiAuthTest.java
new file mode 100644
index 000000000..cb672257f
--- /dev/null
+++ 
b/sshd-core/src/test/java/org/apache/sshd/server/auth/PublicKeyMultiAuthTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.SshException;
+import org.apache.sshd.common.auth.UserAuthMethodFactory;
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.config.keys.PublicKeyEntry;
+import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
+import org.apache.sshd.core.CoreModuleProperties;
+import org.apache.sshd.server.SshServer;
+import org.apache.sshd.server.auth.hostbased.RejectAllHostBasedAuthenticator;
+import 
org.apache.sshd.server.auth.pubkey.AuthorizedKeyEntriesPublickeyAuthenticator;
+import org.apache.sshd.util.test.BaseTestSupport;
+import org.apache.sshd.util.test.CoreTestSupportUtils;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class PublicKeyMultiAuthTest extends BaseTestSupport {
+
+    private static final String USER_NAME = "single-key-user";
+
+    private SshServer sshd;
+    private SshClient client;
+    private KeyPair firstAuthorizedKey;
+    private KeyPair secondAuthorizedKey;
+    private int port;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        KeyPairGenerator generator = 
KeyPairGenerator.getInstance(KeyUtils.RSA_ALGORITHM);
+        generator.initialize(2048);
+        firstAuthorizedKey = generator.generateKeyPair();
+        secondAuthorizedKey = generator.generateKeyPair();
+
+        sshd = 
CoreTestSupportUtils.setupTestServer(PublicKeyMultiAuthTest.class);
+        
sshd.setHostBasedAuthenticator(RejectAllHostBasedAuthenticator.INSTANCE);
+        AuthorizedKeyEntry firstEntry = 
AuthorizedKeyEntry.parseAuthorizedKeyEntry(
+                PublicKeyEntry.toString(firstAuthorizedKey.getPublic()));
+        AuthorizedKeyEntry secondEntry = 
AuthorizedKeyEntry.parseAuthorizedKeyEntry(
+                PublicKeyEntry.toString(secondAuthorizedKey.getPublic()));
+        sshd.setPublickeyAuthenticator(new 
AuthorizedKeyEntriesPublickeyAuthenticator(
+                "two-authorized-keys", null, Arrays.asList(firstEntry, 
secondEntry), PublicKeyEntryResolver.FAILING));
+        CoreModuleProperties.AUTH_METHODS.set(sshd, "publickey,publickey");
+        sshd.start();
+        port = sshd.getPort();
+
+        client = 
CoreTestSupportUtils.setupTestClient(PublicKeyMultiAuthTest.class);
+        client.setUserAuthFactoriesNames(UserAuthMethodFactory.PUBLIC_KEY);
+        client.start();
+    }
+
+    @AfterEach
+    void tearDown() throws Exception {
+        if (client != null) {
+            client.stop();
+        }
+        if (sshd != null) {
+            sshd.stop(true);
+        }
+    }
+
+    @Test
+    void singleOfferedIdentityDoesNotSatisfyTwoPublicKeySteps() throws 
Exception {
+        try (ClientSession session = createClientSession(USER_NAME, client, 
port)) {
+            session.setKeyIdentityProvider(context -> 
Collections.singletonList(firstAuthorizedKey));
+            assertThrows(SshException.class, () -> 
session.auth().verify(AUTH_TIMEOUT));
+            assertFalse(session.isAuthenticated());
+        }
+    }
+
+    @Test
+    void repeatedSameKeyDoesNotSatisfyTwoPublicKeySteps() throws Exception {
+        try (ClientSession session = createClientSession(USER_NAME, client, 
port)) {
+            session.setKeyIdentityProvider(context -> {
+                List<KeyPair> maliciousIdentities = new ArrayList<>();
+                maliciousIdentities.add(firstAuthorizedKey);
+                maliciousIdentities.add(firstAuthorizedKey);
+                return maliciousIdentities;
+            });
+
+            SshException e = assertThrows(SshException.class, () -> 
session.auth().verify(AUTH_TIMEOUT),
+                    "one key must not satisfy two-key authentication policy");
+            assertEquals(e.getMessage(), "No more authentication methods 
available");
+        }
+    }
+
+    @Test
+    void twoDistinctAuthorizedKeysSatisfyTwoPublicKeySteps() throws Exception {
+        try (ClientSession session = createClientSession(USER_NAME, client, 
port)) {
+            session.setKeyIdentityProvider(context -> 
Arrays.asList(firstAuthorizedKey, secondAuthorizedKey));
+
+            session.auth().verify(AUTH_TIMEOUT);
+            assertTrue(session.isAuthenticated());
+        }
+    }
+}

Reply via email to