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 18e30f69174bb693a6cc622bcc8d9fdb033546f0 Author: Thomas Wolf <tw...@apache.org> AuthorDate: Sun Feb 16 21:50:37 2025 +0100 [SSHD-1167] KEX: handle "@cert-authority" in known_hosts files Implement the OpenSSH behavior for checking of certificates: if the server key is a certificate, only entries with markers @revoked or @cert-authority are considered. There must be an entry marked as @cert-authority matching the certificate's CA public key, otherwise the server key is rejected. Unknown CA keys are never added to the file. Move the responsibility for this check from AbstractClientSession to the ServerKeyVerifier. Note that this means that the provided implementation does *not* fall back to checking the certificate's public key (i.e., the one that was signed by the CA key). If users want that (insecure) behavior, they have to provide their own ServerKeyVerifier that implements it. --- CHANGES.md | 1 + .../keyverifier/KnownHostsServerKeyVerifier.java | 20 +++++-- .../keyverifier/RequiredServerKeyVerifier.java | 14 ++++- .../sshd/client/session/AbstractClientSession.java | 26 ++------- ...ateTest.java => KnownHostsCertificateTest.java} | 68 +++++++++++++++------- .../signature/OpenSshHostCertificateTest.java | 5 +- 6 files changed, 83 insertions(+), 51 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 323116c15..60c3694c2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -34,6 +34,7 @@ * [SSHD-1161](https://issues.apache.org/jira/projects/SSHD/issues/SSHD-1161) Support pubkey auth with user certificates (server-side) * Client-side support was introduced in version 2.8.0 already +* [SSHD-1167](https://issues.apache.org/jira/projects/SSHD/issues/SSHD-1167) Check host certificates against known_hosts file (implements @<!-- -->cert-authority) ## Potential Compatibility Issues diff --git a/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/KnownHostsServerKeyVerifier.java b/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/KnownHostsServerKeyVerifier.java index 88699f503..10d97b1d6 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/KnownHostsServerKeyVerifier.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/KnownHostsServerKeyVerifier.java @@ -51,6 +51,7 @@ import org.apache.sshd.common.NamedFactory; import org.apache.sshd.common.config.ConfigFileReaderSupport; 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.common.config.keys.UnsupportedSshPublicKey; @@ -276,11 +277,15 @@ public class KnownHostsServerKeyVerifier return acceptUnknownHostKey(clientSession, remoteAddress, serverKey); } - String serverKeyType = KeyUtils.getKeyType(serverKey); + PublicKey keyToCheck = serverKey instanceof OpenSshCertificate + ? ((OpenSshCertificate) serverKey).getCaPubKey() + : serverKey; + boolean isCert = serverKey instanceof OpenSshCertificate; + String keyType = KeyUtils.getKeyType(keyToCheck); List<HostEntryPair> keyMatches = hostMatches.stream() - .filter(entry -> serverKeyType.equals(entry.getHostEntry().getKeyEntry().getKeyType())) - .filter(k -> KeyUtils.compareKeys(k.getServerKey(), serverKey)) + .filter(entry -> keyType.equals(entry.getHostEntry().getKeyEntry().getKeyType())) + .filter(k -> KeyUtils.compareKeys(k.getServerKey(), keyToCheck)) .collect(Collectors.toList()); if (keyMatches.stream() @@ -289,10 +294,17 @@ public class KnownHostsServerKeyVerifier return false; } + keyMatches = keyMatches.stream() + .filter(e -> isCert == "cert-authority".equals(e.getHostEntry().getMarker())) + .collect(Collectors.toList()); if (!keyMatches.isEmpty()) { return true; } + if (isCert) { + return false; + } + Optional<HostEntryPair> anyNonRevokedMatch = hostMatches.stream() .filter(k -> !"revoked".equals(k.getHostEntry().getMarker())) .findAny(); @@ -583,7 +595,7 @@ public class KnownHostsServerKeyVerifier clientSession, remoteAddress, KeyUtils.getFingerPrint(serverKey)); } - if (delegate.verifyServerKey(clientSession, remoteAddress, serverKey)) { + if (delegate.verifyServerKey(clientSession, remoteAddress, serverKey) && !(serverKey instanceof OpenSshCertificate)) { Path file = getPath(); Collection<HostEntryPair> keys = keysSupplier.get().get(); try { diff --git a/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/RequiredServerKeyVerifier.java b/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/RequiredServerKeyVerifier.java index bbdc5c286..47d8c7af1 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/RequiredServerKeyVerifier.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/RequiredServerKeyVerifier.java @@ -22,6 +22,8 @@ import java.net.SocketAddress; import java.security.PublicKey; import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.config.keys.OpenSshCertificate; import org.apache.sshd.common.util.buffer.BufferUtils; import org.apache.sshd.common.util.logging.AbstractLoggingBean; @@ -43,7 +45,17 @@ public class RequiredServerKeyVerifier extends AbstractLoggingBean implements Se @Override public boolean verifyServerKey(ClientSession sshClientSession, SocketAddress remoteAddress, PublicKey serverKey) { - if (requiredKey.equals(serverKey)) { + boolean sameKey = KeyUtils.compareKeys(requiredKey, serverKey); + if (!sameKey) { + PublicKey req = (requiredKey instanceof OpenSshCertificate) + ? ((OpenSshCertificate) requiredKey).getCaPubKey() + : requiredKey; + PublicKey srv = (serverKey instanceof OpenSshCertificate) + ? ((OpenSshCertificate) serverKey).getCaPubKey() + : serverKey; + sameKey = KeyUtils.compareKeys(req, srv); + } + if (sameKey) { if (log.isDebugEnabled()) { log.debug("Server at {} presented expected key: {}", remoteAddress, BufferUtils.toHex(serverKey.getEncoded())); } diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java index 5bde72610..62513c847 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java @@ -58,7 +58,6 @@ import org.apache.sshd.common.channel.PtyChannelConfigurationHolder; import org.apache.sshd.common.cipher.BuiltinCiphers; import org.apache.sshd.common.cipher.CipherNone; import org.apache.sshd.common.config.keys.KeyUtils; -import org.apache.sshd.common.config.keys.OpenSshCertificate; import org.apache.sshd.common.forward.Forwarder; import org.apache.sshd.common.future.DefaultKeyExchangeFuture; import org.apache.sshd.common.future.KeyExchangeFuture; @@ -618,27 +617,10 @@ public abstract class AbstractClientSession extends AbstractSession implements C remoteAddress = targetServerAddress.toInetSocketAddress(); } - boolean verified = false; - if (serverKey instanceof OpenSshCertificate) { - // check if we trust the CA - verified = serverKeyVerifier.verifyServerKey(this, remoteAddress, ((OpenSshCertificate) serverKey).getCaPubKey()); - if (log.isDebugEnabled()) { - log.debug("checkCA({}) key={}-{}, verified={}", - this, KeyUtils.getKeyType(serverKey), KeyUtils.getFingerPrint(serverKey), verified); - } - - if (!verified) { - // fallback to actual public host key - serverKey = ((OpenSshCertificate) serverKey).getCertPubKey(); - } - } - - if (!verified) { - verified = serverKeyVerifier.verifyServerKey(this, remoteAddress, serverKey); - if (log.isDebugEnabled()) { - log.debug("checkKeys({}) key={}-{}, verified={}", - this, KeyUtils.getKeyType(serverKey), KeyUtils.getFingerPrint(serverKey), verified); - } + boolean verified = serverKeyVerifier.verifyServerKey(this, remoteAddress, serverKey); + if (log.isDebugEnabled()) { + log.debug("checkKeys({}) key={}-{}, verified={}", this, KeyUtils.getKeyType(serverKey), + KeyUtils.getFingerPrint(serverKey), verified); } if (!verified) { diff --git a/sshd-core/src/test/java/org/apache/sshd/common/signature/OpenSshHostCertificateTest.java b/sshd-core/src/test/java/org/apache/sshd/common/signature/KnownHostsCertificateTest.java similarity index 62% copy from sshd-core/src/test/java/org/apache/sshd/common/signature/OpenSshHostCertificateTest.java copy to sshd-core/src/test/java/org/apache/sshd/common/signature/KnownHostsCertificateTest.java index 74369d933..7e40f69c4 100644 --- a/sshd-core/src/test/java/org/apache/sshd/common/signature/OpenSshHostCertificateTest.java +++ b/sshd-core/src/test/java/org/apache/sshd/common/signature/KnownHostsCertificateTest.java @@ -18,6 +18,8 @@ */ package org.apache.sshd.common.signature; +import java.nio.file.Files; +import java.nio.file.Path; import java.security.KeyPair; import java.util.ArrayList; import java.util.Collections; @@ -27,39 +29,48 @@ import java.util.stream.Stream; import org.apache.sshd.certificate.OpenSshCertificateBuilder; import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.keyverifier.AcceptAllServerKeyVerifier; +import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier; import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.SshException; 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.keyprovider.KeyPairProvider; +import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.server.SshServer; import org.apache.sshd.util.test.BaseTestSupport; import org.apache.sshd.util.test.CommonTestSupportUtils; import org.apache.sshd.util.test.CoreTestSupportUtils; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; /** - * Tests for KEX with host certificates. + * Tests for KEX with host certificates with host key validation through a {@link KnownHostsServerKeyVerifier}. */ -class OpenSshHostCertificateTest extends BaseTestSupport { +class KnownHostsCertificateTest extends BaseTestSupport { private static SshServer sshd; private static SshClient client; private static int port; + @TempDir + private Path tmp; + private KeyPair hostKey; private KeyPair caKey; @BeforeAll private static void setupClientAndServer() throws Exception { - sshd = CoreTestSupportUtils.setupTestFullSupportServer(OpenSshHostCertificateTest.class); + sshd = CoreTestSupportUtils.setupTestFullSupportServer(KnownHostsCertificateTest.class); sshd.start(); port = sshd.getPort(); - client = CoreTestSupportUtils.setupTestFullSupportClient(OpenSshHostCertificateTest.class); + client = CoreTestSupportUtils.setupTestFullSupportClient(KnownHostsCertificateTest.class); client.start(); } @@ -82,15 +93,12 @@ class OpenSshHostCertificateTest extends BaseTestSupport { } } - private static Stream<Arguments> certificateAlgorithms() { - return Stream.of( // - Arguments.of(KeyUtils.RSA_ALGORITHM, 2048, KeyUtils.RSA_ALGORITHM, 2048, "rsa-sha2-512"), - Arguments.of(KeyUtils.EC_ALGORITHM, 256, KeyUtils.EC_ALGORITHM, 256, "ecdsa-sha2-nistp256"), - Arguments.of(KeyUtils.RSA_ALGORITHM, 2048, KeyUtils.EC_ALGORITHM, 256, "ecdsa-sha2-nistp256"), - Arguments.of(KeyUtils.EC_ALGORITHM, 256, KeyUtils.RSA_ALGORITHM, 2048, "rsa-sha2-512")); + private static Stream<String> markers() { + return Stream.of("rejected", "", null); } - private void initKeys(String keyType, int keySize, String caType, int caSize, String signatureAlgorithm) throws Exception { + private void initKeys(String keyType, int keySize, String caType, int caSize, String signatureAlgorithm, String marker) + throws Exception { caKey = CommonTestSupportUtils.generateKeyPair(caType, caSize); KeyPair hostKeyPair = CommonTestSupportUtils.generateKeyPair(keyType, keySize); // Generate a host certificate. @@ -108,18 +116,36 @@ class OpenSshHostCertificateTest extends BaseTestSupport { // new KeyPair(signedCert, hostKeyPair.getPrivate()); sshd.setKeyPairProvider(KeyPairProvider.wrap(hostKey)); sshd.setHostKeyCertificateProvider(session -> Collections.singletonList(signedCert)); - client.setServerKeyVerifier((session, address, key) -> { - // TODO: we get the CA key here. This is wrong; the serverKeyVerifier should get the OpenSshCertificate and - // be responsible itself for checking its CA key against registered trusted CA keys. - return KeyUtils.compareKeys(caKey.getPublic(), key); + Path knownHosts = tmp.resolve("known_hosts"); + if (marker != null) { + StringBuilder line = new StringBuilder(); + if (!GenericUtils.isEmpty(marker)) { + line.append('@').append(marker).append(' '); + } + line.append("[localhost]:").append(port).append(",[127.0.0.1]:").append(port).append(' '); + line.append(PublicKeyEntry.toString(caKey.getPublic())); + line.append('\n'); + Files.write(knownHosts, Collections.singletonList(line.toString())); + } + client.setServerKeyVerifier(new KnownHostsServerKeyVerifier(AcceptAllServerKeyVerifier.INSTANCE, knownHosts)); + } + + @ParameterizedTest(name = "test {0} CA key") + @MethodSource("markers") + void testHostCertificateFails(String marker) throws Exception { + initKeys(KeyUtils.EC_ALGORITHM, 256, KeyUtils.EC_ALGORITHM, 256, "ecdsa-sha2-nistp256", marker); + assertThrows(SshException.class, () -> { + try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(CONNECT_TIMEOUT) + .getSession()) { + s.addPasswordIdentity(getCurrentTestName()); + s.auth().verify(AUTH_TIMEOUT); + } }); } - @ParameterizedTest(name = "test host certificate {0} signed with {2}") - @MethodSource("certificateAlgorithms") - void testHostCertificate(String keyType, int keySize, String caType, int caSize, String signatureAlgorithm) - throws Exception { - initKeys(keyType, keySize, caType, caSize, signatureAlgorithm); + @Test + void testHostCertificateSucceeds() throws Exception { + initKeys(KeyUtils.EC_ALGORITHM, 256, KeyUtils.EC_ALGORITHM, 256, "ecdsa-sha2-nistp256", "cert-authority"); try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(CONNECT_TIMEOUT) .getSession()) { s.addPasswordIdentity(getCurrentTestName()); diff --git a/sshd-core/src/test/java/org/apache/sshd/common/signature/OpenSshHostCertificateTest.java b/sshd-core/src/test/java/org/apache/sshd/common/signature/OpenSshHostCertificateTest.java index 74369d933..8aa52a2cd 100644 --- a/sshd-core/src/test/java/org/apache/sshd/common/signature/OpenSshHostCertificateTest.java +++ b/sshd-core/src/test/java/org/apache/sshd/common/signature/OpenSshHostCertificateTest.java @@ -109,9 +109,8 @@ class OpenSshHostCertificateTest extends BaseTestSupport { sshd.setKeyPairProvider(KeyPairProvider.wrap(hostKey)); sshd.setHostKeyCertificateProvider(session -> Collections.singletonList(signedCert)); client.setServerKeyVerifier((session, address, key) -> { - // TODO: we get the CA key here. This is wrong; the serverKeyVerifier should get the OpenSshCertificate and - // be responsible itself for checking its CA key against registered trusted CA keys. - return KeyUtils.compareKeys(caKey.getPublic(), key); + return (key instanceof OpenSshCertificate) + && KeyUtils.compareKeys(caKey.getPublic(), ((OpenSshCertificate) key).getCaPubKey()); }); }