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()); + } + } +}
