This is an automated email from the ASF dual-hosted git repository. lgoldstein pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/mina-sshd.git
commit 7ebb469ea86460878abd009f357992378e03bde8 Author: Lyor Goldstein <lgoldst...@apache.org> AuthorDate: Sun Apr 19 18:44:18 2020 +0300 [SSHD-660] Added some code improvements and logging for certificates handling code --- CHANGES.md | 4 +- README.md | 3 ++ docs/server-setup.md | 3 ++ .../org/apache/sshd/cli/server/SshServerMain.java | 4 +- .../common/config/keys/OpenSshCertificate.java | 16 ++++++ .../common/config/keys/OpenSshCertificateImpl.java | 19 +++++-- .../keys/impl/OpenSSHCertificateDecoder.java | 2 +- .../FileHostKeyCertificateProvider.java | 29 ++++++----- .../apache/sshd/common/signature/SignatureRSA.java | 3 +- .../buffer/keys/OpenSSHCertPublicKeyParser.java | 46 +++++++++-------- .../apache/sshd/client/ClientFactoryManager.java | 6 ++- .../java/org/apache/sshd/client/kex/DHGClient.java | 50 ++++++++++++------- .../sshd/server/ServerAuthenticationManager.java | 8 +++ .../apache/sshd/server/ServerFactoryManager.java | 6 --- .../java/org/apache/sshd/server/SshServer.java | 1 + .../sshd/server/session/AbstractServerSession.java | 33 ++++++++---- .../common/signature/OpenSSHCertificateTest.java | 58 ++++++++++++++-------- .../server/ServerAuthenticationManagerTest.java | 11 ++++ 18 files changed, 201 insertions(+), 101 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 7c0747d..7022e45 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -31,4 +31,6 @@ where the former validate the messages and deal with the idle timeout, and the l * [SSHD-972](https://issues.apache.org/jira/browse/SSHD-972) - Add support for peers using OpenSSH "security key" key types -* [SSHD-977](https://issues.apache.org/jira/browse/SSHD-977) - Apply consistent logging policy to caught exceptions \ No newline at end of file +* [SSHD-977](https://issues.apache.org/jira/browse/SSHD-977) - Apply consistent logging policy to caught exceptions + +* [SSHD-660](https://issues.apache.org/jira/browse/SSHD-660) - Added support for server-side signed certificate keys \ No newline at end of file diff --git a/README.md b/README.md index 11d6811..2a67a34 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ based applications requiring SSH support. * [Key Exchange (KEX) Method Updates and Recommendations for Secure Shell](https://tools.ietf.org/html/draft-ietf-curdle-ssh-kex-sha2-03) * [OpenSSH support for U2F/FIDO security keys](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.u2f) * **Note:** the server side supports these keys by default. The client side requires specific initialization +* [OpenSSH public-key certificate authentication system for use by SSH](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.certkeys) * SFTP version 3-6 + extensions * `supported` - [DRAFT 05 - section 4.4](http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-05.tx) * `supported2` - [DRAFT 13 section 5.4](https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#page-10) @@ -60,6 +61,8 @@ based applications requiring SSH support. * **Compressions**: none, zlib, z...@openssh.com * **Signatures/Keys**: ssh-dss, ssh-rsa, rsa-sha2-256, rsa-sha2-512, nistp256, nistp384, nistp521 , ed25519 (requires `eddsa` optional module), sk-ecdsa-sha2-nistp...@openssh.com, sk-ssh-ed25...@openssh.com +, ssh-rsa-cert-...@openssh.com, ssh-dss-cert-...@openssh.com, ssh-ed25519-cert-...@openssh.com +, ecdsa-sha2-nistp256-cert-...@openssh.com, ecdsa-sha2-nistp384-cert-...@openssh.com, ecdsa-sha2-nistp521-cert-...@openssh.com # [Release notes](./CHANGES.md) diff --git a/docs/server-setup.md b/docs/server-setup.md index 8c89f56..3d46fbe 100644 --- a/docs/server-setup.md +++ b/docs/server-setup.md @@ -37,6 +37,9 @@ is restarted, the same keys will be used to authenticate the server and avoid th the host keys are modified. **Note**: saving key files in PEM format requires that the [Bouncy Castle](https://www.bouncycastle.org/) supporting artifacts be available in the code's classpath. +* `HostKeyCertificateProvider` - used for OpenSSH public-key certificate authentication system +as defined in [this document](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.certkeys) + * `ShellFactory` - That's the part one usually has to write to customize the SSHD server. The shell factory will be used to create a new shell each time a user logs in and wants to run an interactive shell. SSHD provides a simple implementation that you can use if you want. This implementation will create a process and delegate everything to it, diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerMain.java b/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerMain.java index c0f42ac..f79ea84 100644 --- a/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerMain.java +++ b/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerMain.java @@ -181,9 +181,9 @@ public class SshServerMain extends SshServerCliSupport { KeyPairProvider hostKeyProvider = resolveServerKeys(System.err, hostKeyType, hostKeySize, keyFiles); sshd.setKeyPairProvider(hostKeyProvider); - if (certFiles != null) { + if (GenericUtils.isNotEmpty(certFiles)) { HostKeyCertificateProvider certProvider = new FileHostKeyCertificateProvider( - certFiles.stream().map(Paths::get).collect(Collectors.toList())); + certFiles.stream().map(Paths::get).collect(Collectors.toList())); sshd.setHostKeyCertificateProvider(certProvider); } // Should come AFTER key pair provider setup so auto-welcome can be generated if needed 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 9fa7d5a..d51db78 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 @@ -21,7 +21,9 @@ package org.apache.sshd.common.config.keys; import java.security.PrivateKey; import java.security.PublicKey; import java.util.Collection; +import java.util.Date; import java.util.List; +import java.util.concurrent.TimeUnit; public interface OpenSshCertificate extends PublicKey, PrivateKey { int SSH_CERT_TYPE_USER = 1; @@ -43,10 +45,20 @@ public interface OpenSshCertificate extends PublicKey, PrivateKey { Collection<String> getPrincipals(); + // Seconds after epoch long getValidAfter(); + default Date getValidAfterDate() { + return getValidDate(getValidAfter()); + } + + // Seconds after epoch long getValidBefore(); + default Date getValidBeforeDate() { + return getValidDate(getValidBefore()); + } + List<String> getCriticalOptions(); List<String> getExtensions(); @@ -60,4 +72,8 @@ public interface OpenSshCertificate extends PublicKey, PrivateKey { byte[] getSignature(); String getSignatureAlg(); + + static Date getValidDate(long timestamp) { + return (timestamp == 0L) ? null : new Date(TimeUnit.SECONDS.toMillis(timestamp)); + } } 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 3af92ea..9648d9d 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 @@ -22,6 +22,8 @@ import java.security.PublicKey; import java.util.Collection; import java.util.List; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.NumberUtils; import org.apache.sshd.common.util.buffer.ByteArrayBuffer; public class OpenSshCertificateImpl implements OpenSshCertificate { @@ -50,7 +52,7 @@ public class OpenSshCertificateImpl implements OpenSshCertificate { @Override public String getRawKeyType() { - return keyType.split("@")[0].substring(0, keyType.indexOf("-cert")); + return GenericUtils.isEmpty(keyType) ? null : keyType.split("@")[0].substring(0, keyType.indexOf("-cert")); } @Override @@ -130,7 +132,7 @@ public class OpenSshCertificateImpl implements OpenSshCertificate { @Override public String getSignatureAlg() { - return new ByteArrayBuffer(signature).getString(); + return NumberUtils.isEmpty(signature) ? null : new ByteArrayBuffer(signature).getString(); } @Override @@ -145,7 +147,7 @@ public class OpenSshCertificateImpl implements OpenSshCertificate { @Override public byte[] getEncoded() { - return new byte[0]; + return GenericUtils.EMPTY_BYTE_ARRAY; } public void setKeyType(String keyType) { @@ -207,4 +209,15 @@ public class OpenSshCertificateImpl implements OpenSshCertificate { public void setSignature(byte[] signature) { this.signature = signature; } + + @Override + public String toString() { + return getKeyType() + + "[id=" + getId() + + ", serial=" + getSerial() + + ", type=" + getType() + + ", validAfter=" + getValidAfterDate() + + ", validBefore=" + getValidBeforeDate() + + "]"; + } } diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/OpenSSHCertificateDecoder.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/OpenSSHCertificateDecoder.java index 1e26aa8..26514b3 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/OpenSSHCertificateDecoder.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/OpenSSHCertificateDecoder.java @@ -97,7 +97,7 @@ public class OpenSSHCertificateDecoder extends AbstractPublicKeyEntryDecoder<Ope return decodePublicKey(null, keyType, inStream, null); } } catch (IOException e) { - throw new GeneralSecurityException("Unable to clone key.", e); + throw new GeneralSecurityException("Unable to clone key ID=" + key.getId(), e); } } diff --git a/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/FileHostKeyCertificateProvider.java b/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/FileHostKeyCertificateProvider.java index 0c9b8b1..885cea7 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/FileHostKeyCertificateProvider.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/FileHostKeyCertificateProvider.java @@ -41,22 +41,18 @@ import org.apache.sshd.common.util.ValidateUtils; import org.apache.sshd.common.util.logging.AbstractLoggingBean; public class FileHostKeyCertificateProvider extends AbstractLoggingBean implements HostKeyCertificateProvider { - private Collection<? extends Path> files; - - public FileHostKeyCertificateProvider() { - super(); - } + private final Collection<? extends Path> files; public FileHostKeyCertificateProvider(Path path) { - this(Collections.singletonList(Objects.requireNonNull(path, "No path provided"))); + this((path == null) ? Collections.emptyList() : Collections.singletonList(path)); } public FileHostKeyCertificateProvider(Path... files) { - this(Arrays.asList(ValidateUtils.checkNotNullAndNotEmpty(files, "No path provided"))); + this(GenericUtils.isEmpty(files) ? Collections.emptyList() : Arrays.asList(files)); } public FileHostKeyCertificateProvider(Collection<? extends Path> files) { - this.files = files; + this.files = ValidateUtils.checkNotNullAndNotEmpty(files, "No paths provided"); } public Collection<? extends Path> getPaths() { @@ -64,14 +60,14 @@ public class FileHostKeyCertificateProvider extends AbstractLoggingBean implemen } @Override - public Iterable<OpenSshCertificate> loadCertificates(SessionContext session) throws IOException, GeneralSecurityException { - + public Iterable<OpenSshCertificate> loadCertificates(SessionContext session) + throws IOException, GeneralSecurityException { List<OpenSshCertificate> certificates = new ArrayList<>(); - for (Path file : files) { + for (Path file : getPaths()) { List<String> lines = Files.readAllLines(file, StandardCharsets.UTF_8); for (String line : lines) { line = GenericUtils.replaceWhitespaceAndTrim(line); - if (line.isEmpty() || line.startsWith("#")) { + if (GenericUtils.isEmpty(line) || line.startsWith("#")) { continue; } @@ -94,9 +90,12 @@ public class FileHostKeyCertificateProvider extends AbstractLoggingBean implemen } @Override - public OpenSshCertificate loadCertificate(SessionContext session, String keyType) throws IOException, GeneralSecurityException { - return StreamSupport.stream(loadCertificates(session).spliterator(), false) + public OpenSshCertificate loadCertificate(SessionContext session, String keyType) + throws IOException, GeneralSecurityException { + Iterable<OpenSshCertificate> certificates = loadCertificates(session); + return StreamSupport.stream(certificates.spliterator(), false) .filter(pubKey -> Objects.equals(pubKey.getKeyType(), keyType)) - .findFirst().orElse(null); + .findFirst() + .orElse(null); } } diff --git a/sshd-common/src/main/java/org/apache/sshd/common/signature/SignatureRSA.java b/sshd-common/src/main/java/org/apache/sshd/common/signature/SignatureRSA.java index ff84404..ae46e88 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/signature/SignatureRSA.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/signature/SignatureRSA.java @@ -78,7 +78,8 @@ public abstract class SignatureRSA extends AbstractSignature { * corresponds to a good-faith implementation and is considered safe to accept. */ String canonicalName = KeyUtils.getCanonicalKeyType(keyType); - if (!KeyPairProvider.SSH_RSA.equals(canonicalName) && !KeyPairProvider.SSH_RSA_CERT.equals(canonicalName)) { + if ((!KeyPairProvider.SSH_RSA.equals(canonicalName)) + && (!KeyPairProvider.SSH_RSA_CERT.equals(canonicalName))) { throw new IllegalArgumentException("Mismatched key type: " + keyType); } data = encoding.getValue(); diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/keys/OpenSSHCertPublicKeyParser.java b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/keys/OpenSSHCertPublicKeyParser.java index 1c9823a..e34e6e6 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/keys/OpenSSHCertPublicKeyParser.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/keys/OpenSSHCertPublicKeyParser.java @@ -19,49 +19,55 @@ package org.apache.sshd.common.util.buffer.keys; import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; import java.security.PublicKey; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; +import java.util.List; import org.apache.sshd.common.SshException; +import org.apache.sshd.common.config.keys.OpenSshCertificate; import org.apache.sshd.common.config.keys.OpenSshCertificateImpl; import org.apache.sshd.common.keyprovider.KeyPairProvider; import org.apache.sshd.common.util.buffer.Buffer; import org.apache.sshd.common.util.buffer.ByteArrayBuffer; -public class OpenSSHCertPublicKeyParser extends AbstractBufferPublicKeyParser<PublicKey> { - - public static final OpenSSHCertPublicKeyParser INSTANCE = new OpenSSHCertPublicKeyParser(PublicKey.class, - Arrays.asList( - KeyPairProvider.SSH_RSA_CERT, - KeyPairProvider.SSH_DSS_CERT, - KeyPairProvider.SSH_ECDSA_SHA2_NISTP256_CERT, - KeyPairProvider.SSH_ECDSA_SHA2_NISTP384_CERT, - KeyPairProvider.SSH_ECDSA_SHA2_NISTP521_CERT, - KeyPairProvider.SSH_ED25519_CERT - )); - - public OpenSSHCertPublicKeyParser(Class<PublicKey> keyClass, Collection<String> supported) { - super(keyClass, supported); +public class OpenSSHCertPublicKeyParser extends AbstractBufferPublicKeyParser<OpenSshCertificate> { + public static final List<String> KEY_TYPES = + Collections.unmodifiableList( + Arrays.asList( + KeyPairProvider.SSH_RSA_CERT, + KeyPairProvider.SSH_DSS_CERT, + KeyPairProvider.SSH_ECDSA_SHA2_NISTP256_CERT, + KeyPairProvider.SSH_ECDSA_SHA2_NISTP384_CERT, + KeyPairProvider.SSH_ECDSA_SHA2_NISTP521_CERT, + KeyPairProvider.SSH_ED25519_CERT)); + + public static final OpenSSHCertPublicKeyParser INSTANCE = new OpenSSHCertPublicKeyParser(); + + public OpenSSHCertPublicKeyParser() { + super(OpenSshCertificate.class, KEY_TYPES); } @Override - public PublicKey getRawPublicKey(String keyType, Buffer buffer) throws GeneralSecurityException { - + public OpenSshCertificate getRawPublicKey(String keyType, Buffer buffer) throws GeneralSecurityException { OpenSshCertificateImpl certificate = new OpenSshCertificateImpl(); certificate.setKeyType(keyType); certificate.setNonce(buffer.getBytes()); String rawKeyType = certificate.getRawKeyType(); - certificate.setServerHostKey(DEFAULT.getRawPublicKey(rawKeyType, buffer)); + PublicKey serverHostKey = DEFAULT.getRawPublicKey(rawKeyType, buffer); + certificate.setServerHostKey(serverHostKey); certificate.setSerial(buffer.getLong()); certificate.setType(buffer.getInt()); certificate.setId(buffer.getString()); - certificate.setPrincipals(new ByteArrayBuffer(buffer.getBytes()).getStringList(false)); + Collection<String> principals = new ByteArrayBuffer(buffer.getBytes()).getStringList(false); + certificate.setPrincipals(principals); certificate.setValidAfter(buffer.getLong()); certificate.setValidBefore(buffer.getLong()); @@ -73,14 +79,14 @@ public class OpenSSHCertPublicKeyParser extends AbstractBufferPublicKeyParser<Pu try { certificate.setCaPubKey(buffer.getPublicKey()); } catch (SshException ex) { - throw new GeneralSecurityException("Could not parse public CA key with ID: " + certificate.getId(), ex); + throw new InvalidKeyException("Could not parse public CA key with ID: " + certificate.getId(), ex); } certificate.setMessage(buffer.getBytesConsumed()); certificate.setSignature(buffer.getBytes()); if (buffer.rpos() != buffer.wpos()) { - throw new GeneralSecurityException("KeyExchange signature verification failed, got more data than expected: " + throw new InvalidKeyException("KeyExchange signature verification failed, got more data than expected: " + buffer.rpos() + ", actual: " + buffer.wpos() + ". ID of the ca certificate: " + certificate.getId()); } diff --git a/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java b/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java index 017cf7a..4eb1a62 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java @@ -115,11 +115,15 @@ public interface ClientFactoryManager /** * Defines if we should abort in case we encounter an invalid (e.g. expired) openssh certificate. - * The default is to ignore the certificate and proceed with the plain host key. */ String ABORT_ON_INVALID_CERTIFICATE = "abort-on-invalid-certificate"; /** + * The default is to ignore the certificate and proceed with the plain host key. + */ + boolean DEFAULT_ABORT_ON_INVALID_CERTIFICATE = false; + + /** * @return The {@link HostConfigEntryResolver} to use in order to resolve the * effective session parameters - never {@code null} */ diff --git a/sshd-core/src/main/java/org/apache/sshd/client/kex/DHGClient.java b/sshd-core/src/main/java/org/apache/sshd/client/kex/DHGClient.java index 4f1a4f0..c27ff1e 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/kex/DHGClient.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/kex/DHGClient.java @@ -23,6 +23,7 @@ import java.net.SocketAddress; import java.security.PublicKey; import java.util.Collection; import java.util.Objects; +import java.util.concurrent.TimeUnit; import org.apache.sshd.client.ClientFactoryManager; import org.apache.sshd.common.NamedFactory; @@ -142,7 +143,9 @@ public class DHGClient extends AbstractDHClientKeyExchange { try { verifyCertificate(session, openSshKey); } catch (SshException e) { - if (session.getBooleanProperty(ClientFactoryManager.ABORT_ON_INVALID_CERTIFICATE, false)) { + if (session.getBooleanProperty( + ClientFactoryManager.ABORT_ON_INVALID_CERTIFICATE, + ClientFactoryManager.DEFAULT_ABORT_ON_INVALID_CERTIFICATE)) { throw e; } else { // ignore certificate @@ -186,41 +189,48 @@ public class DHGClient extends AbstractDHClientKeyExchange { protected void verifyCertificate(Session session, OpenSshCertificate openSshKey) throws Exception { PublicKey signatureKey = openSshKey.getCaPubKey(); String keyAlg = KeyUtils.getKeyType(signatureKey); + String keyId = openSshKey.getId(); if (KeyPairProvider.SSH_RSA_CERT.equals(openSshKey.getKeyType())) { // allow sha2 signatures for legacy reasons String variant = openSshKey.getSignatureAlg(); - if (!GenericUtils.isEmpty(variant) && KeyPairProvider.SSH_RSA.equals(KeyUtils.getCanonicalKeyType(variant))) { - log.debug("Allowing to use variant {} instead of {}", variant, keyAlg); + if ((!GenericUtils.isEmpty(variant)) + && KeyPairProvider.SSH_RSA.equals(KeyUtils.getCanonicalKeyType(variant))) { + if (log.isDebugEnabled()) { + log.debug("verifyCertificate({})[id={}] Allowing to use variant {} instead of {}", + session, keyId, variant, keyAlg); + } keyAlg = variant; } else { throw new SshException(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED, - "Found invalid signature alg " + variant); + "Found invalid signature alg " + variant + " for key ID=" + keyId); } } Signature verif = ValidateUtils.checkNotNull( - NamedFactory.create(session.getSignatureFactories(), keyAlg), - "No verifier located for algorithm=%s", keyAlg); + NamedFactory.create(session.getSignatureFactories(), keyAlg), + "No KeyExchange CA verifier located for algorithm=%s of key ID=%s", keyAlg, keyId); verif.initVerifier(session, signatureKey); verif.update(session, openSshKey.getMessage()); + if (!verif.verify(session, openSshKey.getSignature())) { throw new SshException(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED, - "KeyExchange CA signature verification failed for key type=" + keyAlg); + "KeyExchange CA signature verification failed for key type=" + keyAlg + " of key ID=" + keyId); } if (openSshKey.getType() != OpenSshCertificate.SSH_CERT_TYPE_HOST) { throw new SshException(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED, - "KeyExchange signature verification failed, not a host key (2): " - + openSshKey.getType()); + "KeyExchange signature verification failed, not a host key (2) " + + openSshKey.getType() + " for key ID=" + keyId); } - long now = System.currentTimeMillis() / 1000; + long now = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()); // valid after <= current time < valid before - if (!(openSshKey.getValidAfter() <= now && now < openSshKey.getValidBefore())) { + if (!((openSshKey.getValidAfter() <= now) && (now < openSshKey.getValidBefore()))) { throw new SshException(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED, - "KeyExchange signature verification failed, CA expired: " - + openSshKey.getValidAfter() + "-" + openSshKey.getValidBefore()); + "KeyExchange signature verification failed, CA expired " + + openSshKey.getValidAfterDate() + " - " + openSshKey.getValidBeforeDate() + + " for key ID=" + keyId); } /* @@ -231,24 +241,26 @@ public class DHGClient extends AbstractDHClientKeyExchange { if (connectSocketAddress instanceof SshdSocketAddress) { connectSocketAddress = ((SshdSocketAddress) connectSocketAddress).toInetSocketAddress(); } + if (connectSocketAddress instanceof InetSocketAddress) { String hostName = ((InetSocketAddress) connectSocketAddress).getHostString(); Collection<String> principals = openSshKey.getPrincipals(); - if (GenericUtils.isEmpty(principals) || !principals.contains(hostName)) { + if (GenericUtils.isEmpty(principals) || (!principals.contains(hostName))) { throw new SshException(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED, - "KeyExchange signature verification failed, invalid principal: " - + principals); + "KeyExchange signature verification failed, invalid principal " + + hostName + " for key ID=" + keyId + + " - allowed=" + principals); } } else { throw new SshException(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED, - "KeyExchange signature verification failed, could not determine connect host."); + "KeyExchange signature verification failed, could not determine connect host for key ID=" + keyId); } if (!GenericUtils.isEmpty(openSshKey.getCriticalOptions())) { // no critical option defined for host keys yet throw new SshException(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED, - "KeyExchange signature verification failed, unrecognized critical option: " - + openSshKey.getCriticalOptions()); + "KeyExchange signature verification failed, unrecognized critical options " + + openSshKey.getCriticalOptions() + " for key ID=" + keyId); } } } diff --git a/sshd-core/src/main/java/org/apache/sshd/server/ServerAuthenticationManager.java b/sshd-core/src/main/java/org/apache/sshd/server/ServerAuthenticationManager.java index 72877e6..84103fa 100644 --- a/sshd-core/src/main/java/org/apache/sshd/server/ServerAuthenticationManager.java +++ b/sshd-core/src/main/java/org/apache/sshd/server/ServerAuthenticationManager.java @@ -26,6 +26,7 @@ import java.util.List; import org.apache.sshd.common.NamedFactory; import org.apache.sshd.common.auth.UserAuthFactoriesManager; +import org.apache.sshd.common.keyprovider.HostKeyCertificateProvider; import org.apache.sshd.common.keyprovider.KeyPairProviderHolder; import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.common.util.ValidateUtils; @@ -228,6 +229,13 @@ public interface ServerAuthenticationManager void setHostBasedAuthenticator(HostBasedAuthenticator hostBasedAuthenticator); /** + * @return a {@link HostKeyCertificateProvider} if available, null as default + */ + HostKeyCertificateProvider getHostKeyCertificateProvider(); + + void setHostKeyCertificateProvider(HostKeyCertificateProvider provider); + + /** * If user authentication factories already set, then simply returns them. Otherwise, * builds the factories list from the individual authenticators available for * the manager - password public key, keyboard-interactive, GSS, etc... diff --git a/sshd-core/src/main/java/org/apache/sshd/server/ServerFactoryManager.java b/sshd-core/src/main/java/org/apache/sshd/server/ServerFactoryManager.java index 83f7770..10e23b0 100644 --- a/sshd-core/src/main/java/org/apache/sshd/server/ServerFactoryManager.java +++ b/sshd-core/src/main/java/org/apache/sshd/server/ServerFactoryManager.java @@ -22,7 +22,6 @@ import java.util.List; import java.util.concurrent.TimeUnit; import org.apache.sshd.common.FactoryManager; -import org.apache.sshd.common.keyprovider.HostKeyCertificateProvider; import org.apache.sshd.server.command.CommandFactory; import org.apache.sshd.server.session.ServerProxyAcceptorHolder; import org.apache.sshd.server.shell.ShellFactory; @@ -111,9 +110,4 @@ public interface ServerFactoryManager * or {@code null}/empty if subsystems are not supported on this server */ List<SubsystemFactory> getSubsystemFactories(); - - /** - * @return a {@link HostKeyCertificateProvider} if available, null as default - */ - HostKeyCertificateProvider getHostKeyCertificateProvider(); } diff --git a/sshd-core/src/main/java/org/apache/sshd/server/SshServer.java b/sshd-core/src/main/java/org/apache/sshd/server/SshServer.java index be5d946..38c3f72 100644 --- a/sshd-core/src/main/java/org/apache/sshd/server/SshServer.java +++ b/sshd-core/src/main/java/org/apache/sshd/server/SshServer.java @@ -267,6 +267,7 @@ public class SshServer extends AbstractFactoryManager implements ServerFactoryMa return hostKeyCertificateProvider; } + @Override public void setHostKeyCertificateProvider(HostKeyCertificateProvider hostKeyCertificateProvider) { this.hostKeyCertificateProvider = hostKeyCertificateProvider; } diff --git a/sshd-core/src/main/java/org/apache/sshd/server/session/AbstractServerSession.java b/sshd-core/src/main/java/org/apache/sshd/server/session/AbstractServerSession.java index 5f4ddcf..f8c4721 100644 --- a/sshd-core/src/main/java/org/apache/sshd/server/session/AbstractServerSession.java +++ b/sshd-core/src/main/java/org/apache/sshd/server/session/AbstractServerSession.java @@ -28,7 +28,6 @@ import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Set; import java.util.stream.Collectors; import org.apache.sshd.common.FactoryManager; @@ -194,11 +193,14 @@ public abstract class AbstractServerSession extends AbstractSession implements S (parent == null) ? null : ((ServerAuthenticationManager) parent).getKeyPairProvider()); } + @Override public HostKeyCertificateProvider getHostKeyCertificateProvider() { ServerFactoryManager manager = getFactoryManager(); - return resolveEffectiveProvider(HostKeyCertificateProvider.class, hostKeyCertificateProvider, manager.getHostKeyCertificateProvider()); + return resolveEffectiveProvider(HostKeyCertificateProvider.class, + hostKeyCertificateProvider, manager.getHostKeyCertificateProvider()); } + @Override public void setHostKeyCertificateProvider(HostKeyCertificateProvider hostKeyCertificateProvider) { this.hostKeyCertificateProvider = hostKeyCertificateProvider; } @@ -381,21 +383,23 @@ public abstract class AbstractServerSession extends AbstractSession implements S KeyPairProvider kpp = getKeyPairProvider(); boolean debugEnabled = log.isDebugEnabled(); - Set<String> provided = null; + Collection<String> provided = null; try { if (kpp != null) { provided = GenericUtils.stream(kpp.getKeyTypes(this)).collect(Collectors.toSet()); HostKeyCertificateProvider hostKeyCertificateProvider = getHostKeyCertificateProvider(); if (hostKeyCertificateProvider != null) { - Iterable<OpenSshCertificate> certificates = hostKeyCertificateProvider.loadCertificates(this); + Iterable<OpenSshCertificate> certificates = + hostKeyCertificateProvider.loadCertificates(this); for (OpenSshCertificate certificate : certificates) { // Add the certificate alg only if the corresponding keyPair type is available - if (provided.contains(certificate.getRawKeyType())) { + String rawKeyType = certificate.getRawKeyType(); + if (provided.contains(rawKeyType)) { provided.add(certificate.getKeyType()); } else { - log.info("No private key for provided certificate available. Missing private key type: {}", - certificate.getRawKeyType()); + log.info("resolveAvailableSignaturesProposal({}) No private key of type={} available in provided certificate", + this, rawKeyType); } } } @@ -530,14 +534,23 @@ public abstract class AbstractServerSession extends AbstractSession implements S try { HostKeyCertificateProvider hostKeyCertificateProvider = getHostKeyCertificateProvider(); if (hostKeyCertificateProvider != null) { - OpenSshCertificate publicKey = hostKeyCertificateProvider.loadCertificate(this, keyType); + OpenSshCertificate publicKey = + hostKeyCertificateProvider.loadCertificate(this, keyType); if (publicKey != null) { - KeyPair keyPair = provider.loadKey(this, publicKey.getRawKeyType()); + String rawKeyType = publicKey.getRawKeyType(); + + if (log.isDebugEnabled()) { + log.debug("getHostKey({}) using certified key {}/{} with ID={}", + this, keyType, rawKeyType, publicKey.getId()); + } + + KeyPair keyPair = provider.loadKey(this, rawKeyType); + ValidateUtils.checkNotNull(keyPair, "No certified private key of type=%s available", rawKeyType); return new KeyPair(publicKey, keyPair.getPrivate()); } } - return provider.loadKey(this, keyType); + return provider.loadKey(this, keyType); } catch (IOException | GeneralSecurityException | Error e) { log.warn("getHostKey({}) failed ({}) to load key of type={}[{}]: {}", this, e.getClass().getSimpleName(), proposedKey, keyType, e.getMessage()); diff --git a/sshd-core/src/test/java/org/apache/sshd/common/signature/OpenSSHCertificateTest.java b/sshd-core/src/test/java/org/apache/sshd/common/signature/OpenSSHCertificateTest.java index 50bd39d..7259632 100644 --- a/sshd-core/src/test/java/org/apache/sshd/common/signature/OpenSSHCertificateTest.java +++ b/sshd-core/src/test/java/org/apache/sshd/common/signature/OpenSSHCertificateTest.java @@ -19,6 +19,7 @@ package org.apache.sshd.common.signature; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -38,29 +39,33 @@ import org.apache.sshd.util.test.BaseTestSupport; import org.apache.sshd.util.test.CoreTestSupportUtils; import org.apache.sshd.util.test.JUnit4ClassRunnerWithParametersFactory; import org.junit.AfterClass; -import org.junit.Assert; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.FixMethodOrder; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.MethodSorters; import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.junit.runners.Parameterized.UseParametersRunnerFactory; @FixMethodOrder(MethodSorters.NAME_ASCENDING) @RunWith(Parameterized.class) // see https://github.com/junit-team/junit/wiki/Parameterized-tests -@Parameterized.UseParametersRunnerFactory(JUnit4ClassRunnerWithParametersFactory.class) +@UseParametersRunnerFactory(JUnit4ClassRunnerWithParametersFactory.class) public class OpenSSHCertificateTest extends BaseTestSupport { private static SshServer sshd; private static SshClient client; private static int port; + private static List<NamedFactory<Signature>> defaultSignatureFactories; private final FileHostKeyCertificateProvider certificateProvider; private final FileKeyPairProvider keyPairProvider; private final List<NamedFactory<Signature>> signatureFactory; public OpenSSHCertificateTest(String keyPath, String certPath, List<NamedFactory<Signature>> signatureFactory) { - this.keyPairProvider = new FileKeyPairProvider(getTestResourcesFolder().resolve(keyPath)); - this.certificateProvider = new FileHostKeyCertificateProvider(getTestResourcesFolder().resolve(certPath)); + Path testResourcesFolder = getTestResourcesFolder(); + this.keyPairProvider = new FileKeyPairProvider(testResourcesFolder.resolve(keyPath)); + this.certificateProvider = new FileHostKeyCertificateProvider(testResourcesFolder.resolve(certPath)); this.signatureFactory = signatureFactory; } @@ -72,6 +77,7 @@ public class OpenSSHCertificateTest extends BaseTestSupport { client = CoreTestSupportUtils.setupTestClient(OpenSSHCertificateTest.class); client.start(); + defaultSignatureFactories = client.getSignatureFactories(); } @AfterClass @@ -93,7 +99,7 @@ public class OpenSSHCertificateTest extends BaseTestSupport { } } - @Parameterized.Parameters(name = "type={2}") + @Parameters(name = "type={2}") public static List<Object[]> parameters() { List<Object[]> list = new ArrayList<>(); @@ -113,49 +119,57 @@ public class OpenSSHCertificateTest extends BaseTestSupport { return Collections.unmodifiableList(list); } - @Test - public void testOpenSshCertificates() throws Exception { + @Before + public void setUp() { sshd.setKeyPairProvider(keyPairProvider); sshd.setHostKeyCertificateProvider(certificateProvider); + + PropertyResolverUtils.updateProperty(client, + ClientFactoryManager.ABORT_ON_INVALID_CERTIFICATE, + ClientFactoryManager.DEFAULT_ABORT_ON_INVALID_CERTIFICATE); + if (signatureFactory != null) { client.setSignatureFactories(signatureFactory); + } else { + client.setSignatureFactories(defaultSignatureFactories); } + } + @Test + public void testOpenSshCertificates() throws Exception { // default client try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port) - .verify(CONNECT_TIMEOUT).getSession()) { + .verify(CONNECT_TIMEOUT) + .getSession()) { s.addPasswordIdentity(getCurrentTestName()); s.auth().verify(AUTH_TIMEOUT); } } - @Test - public void testPrincipal() throws Exception { - sshd.setKeyPairProvider(keyPairProvider); - sshd.setHostKeyCertificateProvider(certificateProvider); - if (signatureFactory != null) { - client.setSignatureFactories(signatureFactory); - } - - // invalid principal, but continue + @Test // invalid principal, but continue + public void testContinueOnInvalidPrincipal() throws Exception { PropertyResolverUtils.updateProperty(client, ClientFactoryManager.ABORT_ON_INVALID_CERTIFICATE, false); try (ClientSession s = client.connect(getCurrentTestName(), "localhost", port) - .verify(CONNECT_TIMEOUT).getSession()) { + .verify(CONNECT_TIMEOUT) + .getSession()) { s.addPasswordIdentity(getCurrentTestName()); s.auth().verify(AUTH_TIMEOUT); } + } - // invalid principal, abort + @Test // invalid principal, abort + public void testAbortOnInvalidPrincipal() throws Exception { PropertyResolverUtils.updateProperty(client, ClientFactoryManager.ABORT_ON_INVALID_CERTIFICATE, true); try (ClientSession s = client.connect(getCurrentTestName(), "localhost", port) - .verify(CONNECT_TIMEOUT).getSession()) { + .verify(CONNECT_TIMEOUT) + .getSession()) { s.addPasswordIdentity(getCurrentTestName()); s.auth().verify(AUTH_TIMEOUT); // in case client does not support cert, no exception should be thrown - Assert.assertFalse(client.getSignatureFactories().contains(BuiltinSignatures.rsa_cert)); + assertFalse(client.getSignatureFactories().contains(BuiltinSignatures.rsa_cert)); } catch (SshException e) { - Assert.assertEquals(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED, e.getDisconnectCode()); + assertEquals(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED, e.getDisconnectCode()); } } } diff --git a/sshd-core/src/test/java/org/apache/sshd/server/ServerAuthenticationManagerTest.java b/sshd-core/src/test/java/org/apache/sshd/server/ServerAuthenticationManagerTest.java index 40eebac..9215b20 100644 --- a/sshd-core/src/test/java/org/apache/sshd/server/ServerAuthenticationManagerTest.java +++ b/sshd-core/src/test/java/org/apache/sshd/server/ServerAuthenticationManagerTest.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.concurrent.atomic.AtomicReference; import org.apache.sshd.common.NamedResource; +import org.apache.sshd.common.keyprovider.HostKeyCertificateProvider; import org.apache.sshd.common.keyprovider.KeyPairProvider; import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.server.auth.BuiltinUserAuthFactories; @@ -123,6 +124,16 @@ public class ServerAuthenticationManagerTest extends BaseTestSupport { public void setKeyPairProvider(KeyPairProvider keyPairProvider) { throw new UnsupportedOperationException("setKeyPairProvider(" + keyPairProvider + ")"); } + + @Override + public HostKeyCertificateProvider getHostKeyCertificateProvider() { + return null; + } + + @Override + public void setHostKeyCertificateProvider(HostKeyCertificateProvider provider) { + throw new UnsupportedOperationException("setHostKeyCertificateProvider(" + provider + ")"); + } }; assertEquals("Mismatched initial factories list", "", manager.getUserAuthFactoriesNameList());