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 47f779f06cb345c7cb706cb81f1214c37dab1fda Author: FliegenKLATSCH <ch...@koras.de> AuthorDate: Wed Apr 8 21:18:40 2020 +0200 [SSHD-660] Add support for serer side openssh host certkeys --- .../org/apache/sshd/cli/server/SshServerMain.java | 15 ++ .../apache/sshd/common/config/keys/KeyUtils.java | 8 + .../common/config/keys/OpenSshCertificate.java | 63 +++++++ .../common/config/keys/OpenSshCertificateImpl.java | 210 +++++++++++++++++++++ .../keys/impl/OpenSSHCertificateDecoder.java | 118 ++++++++++++ .../FileHostKeyCertificateProvider.java | 102 ++++++++++ .../keyprovider/HostKeyCertificateProvider.java | 32 ++++ .../sshd/common/keyprovider/KeyPairProvider.java | 14 +- .../sshd/common/signature/BuiltinSignatures.java | 88 +++++++++ .../sshd/common/signature/SignatureFactory.java | 2 +- .../apache/sshd/common/signature/SignatureRSA.java | 4 +- .../org/apache/sshd/common/util/buffer/Buffer.java | 34 +++- .../sshd/common/util/buffer/ByteArrayBuffer.java | 7 + .../util/buffer/keys/BufferPublicKeyParser.java | 1 + .../buffer/keys/OpenSSHCertPublicKeyParser.java | 89 +++++++++ .../security/eddsa/EdDSASecurityProviderUtils.java | 2 - .../util/security/eddsa/EDDSAProviderTest.java | 4 +- .../java/org/apache/sshd/client/ClientBuilder.java | 6 + .../apache/sshd/client/ClientFactoryManager.java | 6 + .../java/org/apache/sshd/client/kex/DHGClient.java | 105 ++++++++++- .../sshd/client/session/AbstractClientSession.java | 27 ++- .../java/org/apache/sshd/server/ServerBuilder.java | 8 + .../apache/sshd/server/ServerFactoryManager.java | 6 + .../java/org/apache/sshd/server/SshServer.java | 11 ++ .../sshd/server/config/keys/ServerIdentity.java | 23 ++- .../java/org/apache/sshd/server/kex/DHGServer.java | 1 + .../sshd/server/session/AbstractServerSession.java | 43 ++++- .../common/signature/OpenSSHCertificateTest.java | 161 ++++++++++++++++ .../org/apache/sshd/common/signature/example-ca | 49 +++++ .../apache/sshd/common/signature/example-ca.pub | 1 + .../apache/sshd/common/signature/ssh_host_rsa_key | 38 ++++ .../common/signature/ssh_host_rsa_key-cert.pub | 1 + .../sshd/common/signature/ssh_host_rsa_key.pub | 1 + .../signature/ssh_host_rsa_key_sha1-cert.pub | 1 + 34 files changed, 1259 insertions(+), 22 deletions(-) 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 321ae1e..c0f42ac 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 @@ -19,6 +19,7 @@ package org.apache.sshd.cli.server; +import java.nio.file.Paths; import java.util.Collection; import java.util.LinkedList; import java.util.List; @@ -26,12 +27,15 @@ import java.util.Map; import java.util.Objects; import java.util.TreeMap; import java.util.logging.Level; +import java.util.stream.Collectors; import org.apache.sshd.common.NamedResource; import org.apache.sshd.common.PropertyResolver; import org.apache.sshd.common.PropertyResolverUtils; import org.apache.sshd.common.config.ConfigFileReaderSupport; import org.apache.sshd.common.config.SshConfigFileReader; +import org.apache.sshd.common.keyprovider.FileHostKeyCertificateProvider; +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.SshServer; @@ -62,6 +66,7 @@ public class SshServerMain extends SshServerCliSupport { String hostKeyType = AbstractGeneratorHostKeyProvider.DEFAULT_ALGORITHM; int hostKeySize = 0; Collection<String> keyFiles = null; + Collection<String> certFiles = null; Map<String, Object> options = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); int numArgs = GenericUtils.length(args); @@ -140,6 +145,11 @@ public class SshServerMain extends SshServerCliSupport { keyFiles = new LinkedList<>(); } keyFiles.add(optValue); + } else if (ServerIdentity.HOST_CERT_CONFIG_PROP.equals(optName)) { + if (certFiles == null) { + certFiles = new LinkedList<>(); + } + certFiles.add(optValue); } else if (ConfigFileReaderSupport.PORT_CONFIG_PROP.equals(optName)) { port = Integer.parseInt(optValue); } else { @@ -171,6 +181,11 @@ public class SshServerMain extends SshServerCliSupport { KeyPairProvider hostKeyProvider = resolveServerKeys(System.err, hostKeyType, hostKeySize, keyFiles); sshd.setKeyPairProvider(hostKeyProvider); + if (certFiles != null) { + HostKeyCertificateProvider certProvider = new FileHostKeyCertificateProvider( + 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 setupServerBanner(sshd, resolver); sshd.setPort(port); diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java index 37f4051..fd89f9e 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java @@ -68,6 +68,7 @@ import org.apache.sshd.common.Factory; import org.apache.sshd.common.cipher.ECCurves; import org.apache.sshd.common.config.keys.impl.DSSPublicKeyEntryDecoder; import org.apache.sshd.common.config.keys.impl.ECDSAPublicKeyEntryDecoder; +import org.apache.sshd.common.config.keys.impl.OpenSSHCertificateDecoder; import org.apache.sshd.common.config.keys.impl.RSAPublicKeyDecoder; import org.apache.sshd.common.config.keys.impl.SkECDSAPublicKeyEntryDecoder; import org.apache.sshd.common.config.keys.impl.SkED25519PublicKeyEntryDecoder; @@ -140,6 +141,8 @@ public final class KeyUtils { /** @see <A HREF="">https://tools.ietf.org/html/rfc8332#section-3</A> */ public static final String RSA_SHA256_KEY_TYPE_ALIAS = "rsa-sha2-256"; public static final String RSA_SHA512_KEY_TYPE_ALIAS = "rsa-sha2-512"; + public static final String RSA_SHA256_CERT_TYPE_ALIAS = "rsa-sha2-256-cert-...@openssh.com"; + public static final String RSA_SHA512_CERT_TYPE_ALIAS = "rsa-sha2-512-cert-...@openssh.com"; private static final AtomicReference<DigestFactory> DEFAULT_DIGEST_HOLDER = new AtomicReference<>(); @@ -153,9 +156,12 @@ public final class KeyUtils { NavigableMapBuilder.<String, String>builder(String.CASE_INSENSITIVE_ORDER) .put(RSA_SHA256_KEY_TYPE_ALIAS, KeyPairProvider.SSH_RSA) .put(RSA_SHA512_KEY_TYPE_ALIAS, KeyPairProvider.SSH_RSA) + .put(RSA_SHA256_CERT_TYPE_ALIAS, KeyPairProvider.SSH_RSA_CERT) + .put(RSA_SHA512_CERT_TYPE_ALIAS, KeyPairProvider.SSH_RSA_CERT) .build(); static { + registerPublicKeyEntryDecoder(OpenSSHCertificateDecoder.INSTANCE); registerPublicKeyEntryDecoder(RSAPublicKeyDecoder.INSTANCE); registerPublicKeyEntryDecoder(DSSPublicKeyEntryDecoder.INSTANCE); @@ -800,6 +806,8 @@ public final class KeyUtils { } } else if (SecurityUtils.EDDSA.equalsIgnoreCase(key.getAlgorithm())) { return KeyPairProvider.SSH_ED25519; + } else if (key instanceof OpenSshCertificate) { + return ((OpenSshCertificate) key).getKeyType(); } return null; 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 new file mode 100644 index 0000000..9fa7d5a --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/OpenSshCertificate.java @@ -0,0 +1,63 @@ +/* + * 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.common.config.keys; + +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Collection; +import java.util.List; + +public interface OpenSshCertificate extends PublicKey, PrivateKey { + int SSH_CERT_TYPE_USER = 1; + int SSH_CERT_TYPE_HOST = 2; + + String getRawKeyType(); + + byte[] getNonce(); + + String getKeyType(); + + PublicKey getServerHostKey(); + + long getSerial(); + + int getType(); + + String getId(); + + Collection<String> getPrincipals(); + + long getValidAfter(); + + long getValidBefore(); + + List<String> getCriticalOptions(); + + List<String> getExtensions(); + + String getReserved(); + + PublicKey getCaPubKey(); + + byte[] getMessage(); + + byte[] getSignature(); + + String getSignatureAlg(); +} 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 new file mode 100644 index 0000000..3af92ea --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/OpenSshCertificateImpl.java @@ -0,0 +1,210 @@ +/* + * 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.common.config.keys; + +import java.security.PublicKey; +import java.util.Collection; +import java.util.List; + +import org.apache.sshd.common.util.buffer.ByteArrayBuffer; + +public class OpenSshCertificateImpl implements OpenSshCertificate { + + private static final long serialVersionUID = -3592634724148744943L; + + private String keyType; + private byte[] nonce; + private PublicKey serverHostKey; + private long serial; + private int type; + private String id; + private Collection<String> principals; + private long validAfter; + private long validBefore; + private List<String> criticalOptions; + private List<String> extensions; + private String reserved; + private PublicKey caPubKey; + private byte[] message; + private byte[] signature; + + public OpenSshCertificateImpl() { + super(); + } + + @Override + public String getRawKeyType() { + return keyType.split("@")[0].substring(0, keyType.indexOf("-cert")); + } + + @Override + public byte[] getNonce() { + return nonce; + } + + @Override + public String getKeyType() { + return keyType; + } + + @Override + public PublicKey getServerHostKey() { + return serverHostKey; + } + + @Override + public long getSerial() { + return serial; + } + + @Override + public int getType() { + return type; + } + + @Override + public String getId() { + return id; + } + + @Override + public Collection<String> getPrincipals() { + return principals; + } + + @Override + public long getValidAfter() { + return validAfter; + } + + @Override + public long getValidBefore() { + return validBefore; + } + + @Override + public List<String> getCriticalOptions() { + return criticalOptions; + } + + @Override + public List<String> getExtensions() { + return extensions; + } + + @Override + public String getReserved() { + return reserved; + } + + @Override + public PublicKey getCaPubKey() { + return caPubKey; + } + + @Override + public byte[] getMessage() { + return message; + } + + @Override + public byte[] getSignature() { + return signature; + } + + @Override + public String getSignatureAlg() { + return new ByteArrayBuffer(signature).getString(); + } + + @Override + public String getAlgorithm() { + return null; + } + + @Override + public String getFormat() { + return null; + } + + @Override + public byte[] getEncoded() { + return new byte[0]; + } + + public void setKeyType(String keyType) { + this.keyType = keyType; + } + + public void setNonce(byte[] nonce) { + this.nonce = nonce; + } + + public void setServerHostKey(PublicKey serverHostKey) { + this.serverHostKey = serverHostKey; + } + + public void setSerial(long serial) { + this.serial = serial; + } + + public void setType(int type) { + this.type = type; + } + + public void setId(String id) { + this.id = id; + } + + public void setPrincipals(Collection<String> principals) { + this.principals = principals; + } + + public void setValidAfter(long validAfter) { + this.validAfter = validAfter; + } + + public void setValidBefore(long validBefore) { + this.validBefore = validBefore; + } + + public void setCriticalOptions(List<String> criticalOptions) { + this.criticalOptions = criticalOptions; + } + + public void setExtensions(List<String> extensions) { + this.extensions = extensions; + } + + public void setReserved(String reserved) { + this.reserved = reserved; + } + + public void setCaPubKey(PublicKey caPubKey) { + this.caPubKey = caPubKey; + } + + public void setMessage(byte[] message) { + this.message = message; + } + + public void setSignature(byte[] signature) { + this.signature = signature; + } +} 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 new file mode 100644 index 0000000..1e26aa8 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/OpenSSHCertificateDecoder.java @@ -0,0 +1,118 @@ +/* + * 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.common.config.keys.impl; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPairGenerator; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; + +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.config.keys.OpenSshCertificate; +import org.apache.sshd.common.keyprovider.KeyPairProvider; +import org.apache.sshd.common.session.SessionContext; +import org.apache.sshd.common.util.buffer.ByteArrayBuffer; +import org.apache.sshd.common.util.buffer.keys.OpenSSHCertPublicKeyParser; +import org.apache.sshd.common.util.io.IoUtils; + +/** + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +public class OpenSSHCertificateDecoder extends AbstractPublicKeyEntryDecoder<OpenSshCertificate, OpenSshCertificate> { + public static final OpenSSHCertificateDecoder INSTANCE = new OpenSSHCertificateDecoder(); + + public OpenSSHCertificateDecoder() { + super(OpenSshCertificate.class, OpenSshCertificate.class, + Collections.unmodifiableList(Arrays.asList( + KeyUtils.RSA_SHA256_CERT_TYPE_ALIAS, + KeyUtils.RSA_SHA512_CERT_TYPE_ALIAS, + KeyPairProvider.SSH_RSA_CERT, + KeyPairProvider.SSH_DSS_CERT, + KeyPairProvider.SSH_ED25519_CERT, + KeyPairProvider.SSH_ECDSA_SHA2_NISTP256_CERT, + KeyPairProvider.SSH_ECDSA_SHA2_NISTP384_CERT, + KeyPairProvider.SSH_ECDSA_SHA2_NISTP521_CERT + ))); + } + + @Override + public OpenSshCertificate decodePublicKey( + SessionContext session, String keyType, InputStream keyData, Map<String, String> headers) + throws IOException, GeneralSecurityException { + + byte[] bytes = IoUtils.toByteArray(keyData); + + ByteArrayBuffer buffer = new ByteArrayBuffer(bytes); + + OpenSshCertificate cert = (OpenSshCertificate) OpenSSHCertPublicKeyParser.INSTANCE.getRawPublicKey(keyType, buffer); + + if (cert.getType() != OpenSshCertificate.SSH_CERT_TYPE_HOST) { + throw new GeneralSecurityException("The provided certificate is not a Host certificate."); + } + + return cert; + } + + @Override + public String encodePublicKey(OutputStream s, OpenSshCertificate key) throws IOException { + Objects.requireNonNull(key, "No public key provided"); + + ByteArrayBuffer buffer = new ByteArrayBuffer(); + buffer.putRawPublicKeyBytes(key); + s.write(buffer.getCompactData()); + + return key.getKeyType(); + } + + @Override + public OpenSshCertificate clonePublicKey(OpenSshCertificate key) throws GeneralSecurityException { + try (ByteArrayOutputStream outStream = new ByteArrayOutputStream()) { + String keyType = encodePublicKey(outStream, key); + try (InputStream inStream = new ByteArrayInputStream(outStream.toByteArray())) { + return decodePublicKey(null, keyType, inStream, null); + } + } catch (IOException e) { + throw new GeneralSecurityException("Unable to clone key.", e); + } + } + + @Override + public OpenSshCertificate clonePrivateKey(OpenSshCertificate key) throws GeneralSecurityException { + return clonePublicKey(key); + } + + @Override + public KeyPairGenerator getKeyPairGenerator() throws GeneralSecurityException { + return null; + } + + @Override + public KeyFactory getKeyFactoryInstance() throws GeneralSecurityException { + return null; + } +} 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 new file mode 100644 index 0000000..0c9b8b1 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/FileHostKeyCertificateProvider.java @@ -0,0 +1,102 @@ +/* + * 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.common.keyprovider; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.PublicKey; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.StreamSupport; + +import org.apache.sshd.common.config.keys.OpenSshCertificate; +import org.apache.sshd.common.config.keys.PublicKeyEntry; +import org.apache.sshd.common.session.SessionContext; +import org.apache.sshd.common.util.GenericUtils; +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(); + } + + public FileHostKeyCertificateProvider(Path path) { + this(Collections.singletonList(Objects.requireNonNull(path, "No path provided"))); + } + + public FileHostKeyCertificateProvider(Path... files) { + this(Arrays.asList(ValidateUtils.checkNotNullAndNotEmpty(files, "No path provided"))); + } + + public FileHostKeyCertificateProvider(Collection<? extends Path> files) { + this.files = files; + } + + public Collection<? extends Path> getPaths() { + return files; + } + + @Override + public Iterable<OpenSshCertificate> loadCertificates(SessionContext session) throws IOException, GeneralSecurityException { + + List<OpenSshCertificate> certificates = new ArrayList<>(); + for (Path file : files) { + List<String> lines = Files.readAllLines(file, StandardCharsets.UTF_8); + for (String line : lines) { + line = GenericUtils.replaceWhitespaceAndTrim(line); + if (line.isEmpty() || line.startsWith("#")) { + continue; + } + + PublicKeyEntry publicKeyEntry = PublicKeyEntry.parsePublicKeyEntry(line); + if (publicKeyEntry == null) { + continue; + } + PublicKey publicKey = publicKeyEntry.resolvePublicKey(session, null, null); + if (publicKey == null) { + continue; + } + if (!(publicKey instanceof OpenSshCertificate)) { + throw new InvalidKeyException("Got unexpected key type in " + file + ". Expected OpenSSHCertificate."); + } + certificates.add((OpenSshCertificate) publicKey); + } + } + + return certificates; + } + + @Override + public OpenSshCertificate loadCertificate(SessionContext session, String keyType) throws IOException, GeneralSecurityException { + return StreamSupport.stream(loadCertificates(session).spliterator(), false) + .filter(pubKey -> Objects.equals(pubKey.getKeyType(), keyType)) + .findFirst().orElse(null); + } +} diff --git a/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/HostKeyCertificateProvider.java b/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/HostKeyCertificateProvider.java new file mode 100644 index 0000000..ab8eba0 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/HostKeyCertificateProvider.java @@ -0,0 +1,32 @@ +/* + * 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.common.keyprovider; + +import java.io.IOException; +import java.security.GeneralSecurityException; + +import org.apache.sshd.common.config.keys.OpenSshCertificate; +import org.apache.sshd.common.session.SessionContext; + +public interface HostKeyCertificateProvider { + + Iterable<OpenSshCertificate> loadCertificates(SessionContext session) throws IOException, GeneralSecurityException; + + OpenSshCertificate loadCertificate(SessionContext session, String keyType) throws IOException, GeneralSecurityException; +} diff --git a/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/KeyPairProvider.java b/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/KeyPairProvider.java index 237b61f..2bc6114 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/KeyPairProvider.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/KeyPairProvider.java @@ -73,6 +73,16 @@ public interface KeyPairProvider extends KeyIdentityProvider { String ECDSA_SHA2_NISTP521 = ECCurves.nistp521.getKeyType(); /** + * SSH identifier for openssh cert keys + */ + String SSH_RSA_CERT = "ssh-rsa-cert-...@openssh.com"; + String SSH_DSS_CERT = "ssh-dss-cert-...@openssh.com"; + String SSH_ED25519_CERT = "ssh-ed25519-cert-...@openssh.com"; + String SSH_ECDSA_SHA2_NISTP256_CERT = "ecdsa-sha2-nistp256-cert-...@openssh.com"; + String SSH_ECDSA_SHA2_NISTP384_CERT = "ecdsa-sha2-nistp384-cert-...@openssh.com"; + String SSH_ECDSA_SHA2_NISTP521_CERT = "ecdsa-sha2-nistp521-cert-...@openssh.com"; + + /** * A {@link KeyPairProvider} that has no keys */ KeyPairProvider EMPTY_KEYPAIR_PROVIDER = @@ -84,7 +94,7 @@ public interface KeyPairProvider extends KeyIdentityProvider { @Override public Iterable<String> getKeyTypes(SessionContext session) { - return Collections.emptyList(); + return Collections.emptySet(); } @Override @@ -122,7 +132,7 @@ public interface KeyPairProvider extends KeyIdentityProvider { /** * @param session The {@link SessionContext} for invoking this load command - may * be {@code null} if not invoked within a session context (e.g., offline tool). - * @return The available {@link Iterable} key types in preferred order - never {@code null} + * @return The available {@link Iterable} key types - never {@code null} * @throws IOException If failed to read/parse the keys data * @throws GeneralSecurityException If failed to generate the keys */ diff --git a/sshd-common/src/main/java/org/apache/sshd/common/signature/BuiltinSignatures.java b/sshd-common/src/main/java/org/apache/sshd/common/signature/BuiltinSignatures.java index bedf5de..695e26a 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/signature/BuiltinSignatures.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/signature/BuiltinSignatures.java @@ -57,18 +57,36 @@ public enum BuiltinSignatures implements SignatureFactory { return new SignatureDSA(); } }, + dsa_cert(KeyPairProvider.SSH_DSS_CERT) { + @Override + public Signature create() { + return new SignatureDSA(); + } + }, rsa(KeyPairProvider.SSH_RSA) { @Override public Signature create() { return new SignatureRSASHA1(); } }, + rsa_cert(KeyPairProvider.SSH_RSA_CERT) { + @Override + public Signature create() { + return new SignatureRSASHA1(); + } + }, rsaSHA256(KeyUtils.RSA_SHA256_KEY_TYPE_ALIAS) { @Override public Signature create() { return new SignatureRSASHA256(); } }, + rsaSHA256_cert(KeyUtils.RSA_SHA256_CERT_TYPE_ALIAS) { + @Override + public Signature create() { + return new SignatureRSASHA256(); + } + }, rsaSHA512(KeyUtils.RSA_SHA512_KEY_TYPE_ALIAS) { private final AtomicReference<Boolean> supportHolder = new AtomicReference<>(); @@ -95,6 +113,32 @@ public enum BuiltinSignatures implements SignatureFactory { return supported; } }, + rsaSHA512_cert(KeyUtils.RSA_SHA512_CERT_TYPE_ALIAS) { + private final AtomicReference<Boolean> supportHolder = new AtomicReference<>(); + + @Override + public Signature create() { + return new SignatureRSASHA512(); + } + + @Override + public boolean isSupported() { + Boolean supported = supportHolder.get(); + if (supported == null) { + try { + java.security.Signature sig = + SecurityUtils.getSignature(SignatureRSASHA512.ALGORITHM); + supported = sig != null; + } catch (Exception e) { + supported = Boolean.FALSE; + } + + supportHolder.set(supported); + } + + return supported; + } + }, nistp256(KeyPairProvider.ECDSA_SHA2_NISTP256) { @Override public Signature create() { @@ -106,6 +150,17 @@ public enum BuiltinSignatures implements SignatureFactory { return SecurityUtils.isECCSupported(); } }, + nistp256_cert(KeyPairProvider.SSH_ECDSA_SHA2_NISTP256_CERT) { + @Override + public Signature create() { + return new SignatureECDSA.SignatureECDSA256(); + } + + @Override + public boolean isSupported() { + return SecurityUtils.isECCSupported(); + } + }, nistp384(KeyPairProvider.ECDSA_SHA2_NISTP384) { @Override public Signature create() { @@ -117,6 +172,17 @@ public enum BuiltinSignatures implements SignatureFactory { return SecurityUtils.isECCSupported(); } }, + nistp384_cert(KeyPairProvider.SSH_ECDSA_SHA2_NISTP384_CERT) { + @Override + public Signature create() { + return new SignatureECDSA.SignatureECDSA384(); + } + + @Override + public boolean isSupported() { + return SecurityUtils.isECCSupported(); + } + }, nistp521(KeyPairProvider.ECDSA_SHA2_NISTP521) { @Override public Signature create() { @@ -128,6 +194,17 @@ public enum BuiltinSignatures implements SignatureFactory { return SecurityUtils.isECCSupported(); } }, + nistp521_cert(KeyPairProvider.SSH_ECDSA_SHA2_NISTP521_CERT) { + @Override + public Signature create() { + return new SignatureECDSA.SignatureECDSA521(); + } + + @Override + public boolean isSupported() { + return SecurityUtils.isECCSupported(); + } + }, sk_ecdsa_sha2_nistp256(SkECDSAPublicKeyEntryDecoder.KEY_TYPE) { @Override public Signature create() { @@ -150,6 +227,17 @@ public enum BuiltinSignatures implements SignatureFactory { return SecurityUtils.isEDDSACurveSupported(); } }, + ed25519_cert(KeyPairProvider.SSH_ED25519_CERT) { + @Override + public Signature create() { + return SecurityUtils.getEDDSASigner(); + } + + @Override + public boolean isSupported() { + return SecurityUtils.isEDDSACurveSupported(); + } + }, sk_ssh_ed25519(SkED25519PublicKeyEntryDecoder.KEY_TYPE) { @Override public Signature create() { diff --git a/sshd-common/src/main/java/org/apache/sshd/common/signature/SignatureFactory.java b/sshd-common/src/main/java/org/apache/sshd/common/signature/SignatureFactory.java index 25296d0..363859c 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/signature/SignatureFactory.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/signature/SignatureFactory.java @@ -90,7 +90,6 @@ public interface SignatureFactory extends BuiltinFactory<Signature> { return Collections.emptyList(); } - // We want to preserve the original available order as it indicates the preference Set<String> providedKeys = new HashSet<>(); for (String providedType : provided) { Collection<String> equivTypes = @@ -102,6 +101,7 @@ public interface SignatureFactory extends BuiltinFactory<Signature> { return Collections.emptyList(); } + // We want to preserve the original available order as it indicates the preference List<String> supported = new ArrayList<>(available); for (int index = 0; index < supported.size(); index++) { String kt = supported.get(index); 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 b0fb2fe..ff84404 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,9 @@ public abstract class SignatureRSA extends AbstractSignature { * corresponds to a good-faith implementation and is considered safe to accept. */ String canonicalName = KeyUtils.getCanonicalKeyType(keyType); - ValidateUtils.checkTrue(KeyPairProvider.SSH_RSA.equals(canonicalName), "Mismatched key type: %s", keyType); + 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/Buffer.java b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java index 2482fb0..db65ac9 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java @@ -60,6 +60,7 @@ import org.apache.sshd.common.SshConstants; import org.apache.sshd.common.SshException; import org.apache.sshd.common.cipher.ECCurves; import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.config.keys.OpenSshCertificate; import org.apache.sshd.common.keyprovider.KeyPairProvider; import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.common.util.NumberUtils; @@ -114,6 +115,11 @@ public abstract class Buffer implements Readable { public abstract byte[] array(); /** + * @return The bytes consumed so far + */ + public abstract byte[] getBytesConsumed(); + + /** * @param pos A position in the <U>raw</U> underlying data bytes * @return The byte at the specified position without changing the * current {@link #rpos() read position}. <B>Note:</B> no validation @@ -893,18 +899,21 @@ public abstract class Buffer implements Readable { } public void putRawPublicKey(PublicKey key) { + putString(KeyUtils.getKeyType(key)); + putRawPublicKeyBytes(key); + } + + public void putRawPublicKeyBytes(PublicKey key) { Objects.requireNonNull(key, "No key"); if (key instanceof RSAPublicKey) { RSAPublicKey rsaPub = (RSAPublicKey) key; - putString(KeyPairProvider.SSH_RSA); putMPInt(rsaPub.getPublicExponent()); putMPInt(rsaPub.getModulus()); } else if (key instanceof DSAPublicKey) { DSAPublicKey dsaPub = (DSAPublicKey) key; DSAParams dsaParams = dsaPub.getParams(); - putString(KeyPairProvider.SSH_DSS); putMPInt(dsaParams.getP()); putMPInt(dsaParams.getQ()); putMPInt(dsaParams.getG()); @@ -918,11 +927,30 @@ public abstract class Buffer implements Readable { } byte[] ecPoint = ECCurves.encodeECPoint(ecKey.getW(), ecParams); - putString(curve.getKeyType()); putString(curve.getName()); putBytes(ecPoint); } else if (SecurityUtils.EDDSA.equals(key.getAlgorithm())) { SecurityUtils.putRawEDDSAPublicKey(this, key); + } else if (key instanceof OpenSshCertificate) { + OpenSshCertificate cert = (OpenSshCertificate) key; + + putBytes(cert.getNonce()); + putRawPublicKeyBytes(cert.getServerHostKey()); + putLong(cert.getSerial()); + putInt(cert.getType()); + putString(cert.getId()); + ByteArrayBuffer tmpBuffer = new ByteArrayBuffer(); + tmpBuffer.putStringList(cert.getPrincipals(), false); + putBytes(tmpBuffer.getCompactData()); + putLong(cert.getValidAfter()); + putLong(cert.getValidBefore()); + putNameList(cert.getCriticalOptions()); + putNameList(cert.getExtensions()); + putString(cert.getReserved()); + tmpBuffer = new ByteArrayBuffer(); + tmpBuffer.putRawPublicKey(cert.getCaPubKey()); + putBytes(tmpBuffer.getCompactData()); + putBytes(cert.getSignature()); } else { throw new BufferException("Unsupported raw public key algorithm: " + key.getAlgorithm()); } diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/ByteArrayBuffer.java b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/ByteArrayBuffer.java index 65fcb0b..2a98434 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/ByteArrayBuffer.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/ByteArrayBuffer.java @@ -158,6 +158,13 @@ public class ByteArrayBuffer extends Buffer { } @Override + public byte[] getBytesConsumed() { + byte[] consumed = new byte[rpos]; + System.arraycopy(data, 0, consumed, 0, rpos); + return consumed; + } + + @Override public byte rawByte(int pos) { return data[pos]; } diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/keys/BufferPublicKeyParser.java b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/keys/BufferPublicKeyParser.java index 6b9bae2..2b788e5 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/keys/BufferPublicKeyParser.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/keys/BufferPublicKeyParser.java @@ -60,6 +60,7 @@ public interface BufferPublicKeyParser<PUB extends PublicKey> { ECBufferPublicKeyParser.INSTANCE, SkECBufferPublicKeyParser.INSTANCE, ED25519BufferPublicKeyParser.INSTANCE, + OpenSSHCertPublicKeyParser.INSTANCE, SkED25519BufferPublicKeyParser.INSTANCE)); /** 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 new file mode 100644 index 0000000..1c9823a --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/keys/OpenSSHCertPublicKeyParser.java @@ -0,0 +1,89 @@ +/* + * 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.common.util.buffer.keys; + +import java.security.GeneralSecurityException; +import java.security.PublicKey; +import java.util.Arrays; +import java.util.Collection; + +import org.apache.sshd.common.SshException; +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); + } + + @Override + public PublicKey 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)); + + certificate.setSerial(buffer.getLong()); + certificate.setType(buffer.getInt()); + + certificate.setId(buffer.getString()); + + certificate.setPrincipals(new ByteArrayBuffer(buffer.getBytes()).getStringList(false)); + certificate.setValidAfter(buffer.getLong()); + certificate.setValidBefore(buffer.getLong()); + + certificate.setCriticalOptions(buffer.getNameList()); + certificate.setExtensions(buffer.getNameList()); + + certificate.setReserved(buffer.getString()); + + try { + certificate.setCaPubKey(buffer.getPublicKey()); + } catch (SshException ex) { + throw new GeneralSecurityException("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: " + + buffer.rpos() + ", actual: " + buffer.wpos() + ". ID of the ca certificate: " + certificate.getId()); + } + + return certificate; + } +} diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/EdDSASecurityProviderUtils.java b/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/EdDSASecurityProviderUtils.java index 242f550..e002d4b 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/EdDSASecurityProviderUtils.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/EdDSASecurityProviderUtils.java @@ -30,7 +30,6 @@ import java.util.Objects; import org.apache.sshd.common.config.keys.PrivateKeyEntryDecoder; import org.apache.sshd.common.config.keys.PublicKeyEntryDecoder; -import org.apache.sshd.common.keyprovider.KeyPairProvider; import org.apache.sshd.common.util.ValidateUtils; import org.apache.sshd.common.util.buffer.Buffer; import org.apache.sshd.common.util.security.SecurityUtils; @@ -187,7 +186,6 @@ public final class EdDSASecurityProviderUtils { EdDSAPublicKey edKey = ValidateUtils.checkInstanceOf(key, EdDSAPublicKey.class, "Not an EDDSA public key: %s", key); byte[] seed = Ed25519PublicKeyDecoder.getSeedValue(edKey); ValidateUtils.checkNotNull(seed, "No seed extracted from key: %s", edKey.getA()); - buffer.putString(KeyPairProvider.SSH_ED25519); buffer.putBytes(seed); return buffer; } diff --git a/sshd-common/src/test/java/org/apache/sshd/common/util/security/eddsa/EDDSAProviderTest.java b/sshd-common/src/test/java/org/apache/sshd/common/util/security/eddsa/EDDSAProviderTest.java index 3ad00be..51ffd0b 100644 --- a/sshd-common/src/test/java/org/apache/sshd/common/util/security/eddsa/EDDSAProviderTest.java +++ b/sshd-common/src/test/java/org/apache/sshd/common/util/security/eddsa/EDDSAProviderTest.java @@ -30,7 +30,6 @@ import java.security.Signature; import org.apache.sshd.common.config.keys.AuthorizedKeyEntry; import org.apache.sshd.common.config.keys.KeyUtils; import org.apache.sshd.common.keyprovider.KeyPairProvider; -import org.apache.sshd.common.util.buffer.Buffer; import org.apache.sshd.common.util.buffer.ByteArrayBuffer; import org.apache.sshd.common.util.security.SecurityUtils; import org.apache.sshd.util.test.JUnitTestSupport; @@ -118,7 +117,8 @@ public class EDDSAProviderTest extends JUnitTestSupport { assertNotNull("No public key generated", pubKey); assertEquals("Mismatched public key algorithm", SecurityUtils.EDDSA, pubKey.getAlgorithm()); - Buffer buf = SecurityUtils.putRawEDDSAPublicKey(new ByteArrayBuffer(), pubKey); + ByteArrayBuffer buf = new ByteArrayBuffer(); + buf.putRawPublicKey(pubKey); PublicKey actual = buf.getRawPublicKey(); assertEquals("Mismatched key algorithm", pubKey.getAlgorithm(), actual.getAlgorithm()); assertEquals("Mismatched recovered key", pubKey, actual); diff --git a/sshd-core/src/main/java/org/apache/sshd/client/ClientBuilder.java b/sshd-core/src/main/java/org/apache/sshd/client/ClientBuilder.java index 46d7ba9..36d1e4c 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/ClientBuilder.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/ClientBuilder.java @@ -76,6 +76,12 @@ public class ClientBuilder extends BaseBuilder<SshClient, ClientBuilder> { */ Collections.unmodifiableList( Arrays.asList( + BuiltinSignatures.nistp256_cert, + BuiltinSignatures.nistp384_cert, + BuiltinSignatures.nistp521_cert, + BuiltinSignatures.ed25519_cert, + BuiltinSignatures.rsa_cert, + BuiltinSignatures.dsa_cert, BuiltinSignatures.nistp256, BuiltinSignatures.nistp384, BuiltinSignatures.nistp521, 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 5c8694b..017cf7a 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 @@ -114,6 +114,12 @@ public interface ClientFactoryManager boolean DEFAULT_IGNORE_INVALID_IDENTITIES = true; /** + * 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"; + + /** * @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 e2694ee..4f1a4f0 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 @@ -18,22 +18,31 @@ */ package org.apache.sshd.client.kex; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.security.PublicKey; +import java.util.Collection; import java.util.Objects; +import org.apache.sshd.client.ClientFactoryManager; import org.apache.sshd.common.NamedFactory; import org.apache.sshd.common.SshConstants; 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.kex.AbstractDH; import org.apache.sshd.common.kex.DHFactory; +import org.apache.sshd.common.kex.KexProposalOption; import org.apache.sshd.common.kex.KeyExchange; import org.apache.sshd.common.kex.KeyExchangeFactory; +import org.apache.sshd.common.keyprovider.KeyPairProvider; import org.apache.sshd.common.session.Session; import org.apache.sshd.common.signature.Signature; import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.common.util.ValidateUtils; import org.apache.sshd.common.util.buffer.Buffer; import org.apache.sshd.common.util.buffer.ByteArrayBuffer; +import org.apache.sshd.common.util.net.SshdSocketAddress; /** * Base class for DHG key exchange algorithms. @@ -124,10 +133,29 @@ public class DHGClient extends AbstractDHClientKeyExchange { buffer = new ByteArrayBuffer(k_s); serverKey = buffer.getRawPublicKey(); - String keyAlg = KeyUtils.getKeyType(serverKey); + PublicKey serverPublicHostKey = serverKey; + + if (serverKey instanceof OpenSshCertificate) { + OpenSshCertificate openSshKey = (OpenSshCertificate) serverKey; + serverPublicHostKey = openSshKey.getServerHostKey(); + + try { + verifyCertificate(session, openSshKey); + } catch (SshException e) { + if (session.getBooleanProperty(ClientFactoryManager.ABORT_ON_INVALID_CERTIFICATE, false)) { + throw e; + } else { + // ignore certificate + serverKey = openSshKey.getServerHostKey(); + log.info("Ignoring invalid certificate {}", openSshKey.getId(), e); + } + } + } + + String keyAlg = session.getNegotiatedKexParameter(KexProposalOption.SERVERKEYS); if (GenericUtils.isEmpty(keyAlg)) { - throw new SshException("Unsupported server key type: " + serverKey.getAlgorithm() - + "[" + serverKey.getFormat() + "]"); + throw new SshException("Unsupported server key type: " + serverPublicHostKey.getAlgorithm() + + "[" + serverPublicHostKey.getFormat() + "]"); } buffer = new ByteArrayBuffer(); @@ -145,7 +173,7 @@ public class DHGClient extends AbstractDHClientKeyExchange { Signature verif = ValidateUtils.checkNotNull( NamedFactory.create(session.getSignatureFactories(), keyAlg), "No verifier located for algorithm=%s", keyAlg); - verif.initVerifier(session, serverKey); + verif.initVerifier(session, serverPublicHostKey); verif.update(session, h); if (!verif.verify(session, sig)) { throw new SshException(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED, @@ -154,4 +182,73 @@ public class DHGClient extends AbstractDHClientKeyExchange { return true; } + + protected void verifyCertificate(Session session, OpenSshCertificate openSshKey) throws Exception { + PublicKey signatureKey = openSshKey.getCaPubKey(); + String keyAlg = KeyUtils.getKeyType(signatureKey); + + 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); + keyAlg = variant; + } else { + throw new SshException(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED, + "Found invalid signature alg " + variant); + } + } + + Signature verif = ValidateUtils.checkNotNull( + NamedFactory.create(session.getSignatureFactories(), keyAlg), + "No verifier located for algorithm=%s", keyAlg); + 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); + } + + 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()); + } + + long now = System.currentTimeMillis() / 1000; + // valid after <= current time < valid before + 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()); + } + + /* + * We compare only the connect address against the principals and do not do any reverse DNS lookups. + * If one wants to connect with the IP it has to be included in the principals list of the certificate. + */ + SocketAddress connectSocketAddress = getClientSession().getConnectAddress(); + 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)) { + throw new SshException(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED, + "KeyExchange signature verification failed, invalid principal: " + + principals); + } + } else { + throw new SshException(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED, + "KeyExchange signature verification failed, could not determine connect host."); + } + + 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()); + } + } } 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 558266c..b36dfb6 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 @@ -51,6 +51,7 @@ 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.ForwardingFilter; import org.apache.sshd.common.future.DefaultKeyExchangeFuture; import org.apache.sshd.common.future.KeyExchangeFuture; @@ -546,10 +547,28 @@ public abstract class AbstractClientSession extends AbstractSession implements C IoSession networkSession = getIoSession(); SocketAddress remoteAddress = networkSession.getRemoteAddress(); PublicKey serverKey = kex.getServerKey(); - boolean verified = serverKeyVerifier.verifyServerKey(this, remoteAddress, serverKey); - if (log.isDebugEnabled()) { - log.debug("checkKeys({}) key={}-{}, verified={}", - this, KeyUtils.getKeyType(serverKey), KeyUtils.getFingerPrint(serverKey), verified); + + 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).getServerHostKey(); + } + } + + if (!verified) { + 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/main/java/org/apache/sshd/server/ServerBuilder.java b/sshd-core/src/main/java/org/apache/sshd/server/ServerBuilder.java index 0f34f80..cbcebc3 100644 --- a/sshd-core/src/main/java/org/apache/sshd/server/ServerBuilder.java +++ b/sshd-core/src/main/java/org/apache/sshd/server/ServerBuilder.java @@ -98,6 +98,14 @@ public class ServerBuilder extends BaseBuilder<SshServer, ServerBuilder> { public static final List<BuiltinSignatures> DEFAULT_SIGNATURE_PREFERENCE = Collections.unmodifiableList( Arrays.asList( + BuiltinSignatures.nistp256_cert, + BuiltinSignatures.nistp384_cert, + BuiltinSignatures.nistp521_cert, + BuiltinSignatures.ed25519_cert, + BuiltinSignatures.rsaSHA512_cert, + BuiltinSignatures.rsaSHA256_cert, + BuiltinSignatures.rsa_cert, + BuiltinSignatures.dsa_cert, BuiltinSignatures.nistp256, BuiltinSignatures.nistp384, BuiltinSignatures.nistp521, 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 10e23b0..83f7770 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,6 +22,7 @@ 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; @@ -110,4 +111,9 @@ 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 fcc676f..be5d946 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 @@ -39,6 +39,7 @@ import org.apache.sshd.common.helpers.AbstractFactoryManager; import org.apache.sshd.common.io.IoAcceptor; import org.apache.sshd.common.io.IoServiceFactory; import org.apache.sshd.common.io.IoSession; +import org.apache.sshd.common.keyprovider.HostKeyCertificateProvider; import org.apache.sshd.common.keyprovider.KeyPairProvider; import org.apache.sshd.common.session.helpers.AbstractSession; import org.apache.sshd.common.util.GenericUtils; @@ -105,6 +106,7 @@ public class SshServer extends AbstractFactoryManager implements ServerFactoryMa private List<SubsystemFactory> subsystemFactories; private List<UserAuthFactory> userAuthFactories; private KeyPairProvider keyPairProvider; + private HostKeyCertificateProvider hostKeyCertificateProvider; private PasswordAuthenticator passwordAuthenticator; private PublickeyAuthenticator publickeyAuthenticator; private KeyboardInteractiveAuthenticator interactiveAuthenticator; @@ -261,6 +263,15 @@ public class SshServer extends AbstractFactoryManager implements ServerFactoryMa } @Override + public HostKeyCertificateProvider getHostKeyCertificateProvider() { + return hostKeyCertificateProvider; + } + + public void setHostKeyCertificateProvider(HostKeyCertificateProvider hostKeyCertificateProvider) { + this.hostKeyCertificateProvider = hostKeyCertificateProvider; + } + + @Override protected void checkConfig() { super.checkConfig(); diff --git a/sshd-core/src/main/java/org/apache/sshd/server/config/keys/ServerIdentity.java b/sshd-core/src/main/java/org/apache/sshd/server/config/keys/ServerIdentity.java index 7cc5010..5e3ffc5 100644 --- a/sshd-core/src/main/java/org/apache/sshd/server/config/keys/ServerIdentity.java +++ b/sshd-core/src/main/java/org/apache/sshd/server/config/keys/ServerIdentity.java @@ -55,6 +55,7 @@ public final class ServerIdentity { * The server's keys configuration multi-value */ public static final String HOST_KEY_CONFIG_PROP = "HostKey"; + public static final String HOST_CERT_CONFIG_PROP = "HostCertificate"; public static final Function<String, String> ID_GENERATOR = ServerIdentity::getIdentityFileName; @@ -137,11 +138,31 @@ public final class ServerIdentity { * @see org.apache.sshd.common.config.ConfigFileReaderSupport#readConfigFile(Path, java.nio.file.OpenOption...) */ public static Map<String, Path> findIdentities(Properties props, LinkOption... options) throws IOException { + return getLocations(HOST_KEY_CONFIG_PROP, props, options); + } + + /** + * @param props The {@link Properties} holding the server's configuration - ignored + * if {@code null}/empty + * @param options The {@link LinkOption}s to use when checking files existence + * @return A {@link Map} of the found certificates where key=the identity type + * (case <U>insensitive</U>) and value=the {@link Path} of the file holding + * the specific type key + * @throws IOException If failed to access the file system + * @see #getIdentityType(String) + * @see #HOST_CERT_CONFIG_PROP + * @see org.apache.sshd.common.config.ConfigFileReaderSupport#readConfigFile(Path, java.nio.file.OpenOption...) + */ + public static Map<String, Path> findCertificates(Properties props, LinkOption... options) throws IOException { + return getLocations(HOST_CERT_CONFIG_PROP, props, options); + } + + private static Map<String, Path> getLocations(String configPropKey, Properties props, LinkOption... options) throws IOException { if (GenericUtils.isEmpty(props)) { return Collections.emptyMap(); } - String keyList = props.getProperty(HOST_KEY_CONFIG_PROP); + String keyList = props.getProperty(configPropKey); String[] paths = GenericUtils.split(keyList, ','); if (GenericUtils.isEmpty(paths)) { return Collections.emptyMap(); diff --git a/sshd-core/src/main/java/org/apache/sshd/server/kex/DHGServer.java b/sshd-core/src/main/java/org/apache/sshd/server/kex/DHGServer.java index c8ee45e..7e96045 100644 --- a/sshd-core/src/main/java/org/apache/sshd/server/kex/DHGServer.java +++ b/sshd-core/src/main/java/org/apache/sshd/server/kex/DHGServer.java @@ -104,6 +104,7 @@ public class DHGServer extends AbstractDHServerKeyExchange { KeyPair kp = Objects.requireNonNull(session.getHostKey(), "No server key pair available"); String algo = session.getNegotiatedKexParameter(KexProposalOption.SERVERKEYS); + Signature sig = ValidateUtils.checkNotNull( NamedFactory.create(session.getSignatureFactories(), algo), "Unknown negotiated server keys: %s", algo); 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 7232935..5f4ddcf 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,6 +28,8 @@ 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; import org.apache.sshd.common.NamedResource; @@ -37,6 +39,7 @@ import org.apache.sshd.common.SshConstants; import org.apache.sshd.common.SshException; import org.apache.sshd.common.auth.AbstractUserAuthServiceFactory; import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.config.keys.OpenSshCertificate; import org.apache.sshd.common.io.IoService; import org.apache.sshd.common.io.IoSession; import org.apache.sshd.common.io.IoWriteFuture; @@ -46,6 +49,7 @@ import org.apache.sshd.common.kex.KexState; import org.apache.sshd.common.kex.extension.KexExtensionHandler; import org.apache.sshd.common.kex.extension.KexExtensionHandler.AvailabilityPhase; import org.apache.sshd.common.kex.extension.KexExtensionHandler.KexPhase; +import org.apache.sshd.common.keyprovider.HostKeyCertificateProvider; import org.apache.sshd.common.keyprovider.KeyPairProvider; import org.apache.sshd.common.session.ConnectionService; import org.apache.sshd.common.session.SessionContext; @@ -81,6 +85,7 @@ public abstract class AbstractServerSession extends AbstractSession implements S private HostBasedAuthenticator hostBasedAuthenticator; private List<UserAuthFactory> userAuthFactories; private KeyPairProvider keyPairProvider; + private HostKeyCertificateProvider hostKeyCertificateProvider; protected AbstractServerSession(ServerFactoryManager factoryManager, IoSession ioSession) { super(true, factoryManager, ioSession); @@ -189,6 +194,15 @@ public abstract class AbstractServerSession extends AbstractSession implements S (parent == null) ? null : ((ServerAuthenticationManager) parent).getKeyPairProvider()); } + public HostKeyCertificateProvider getHostKeyCertificateProvider() { + ServerFactoryManager manager = getFactoryManager(); + return resolveEffectiveProvider(HostKeyCertificateProvider.class, hostKeyCertificateProvider, manager.getHostKeyCertificateProvider()); + } + + public void setHostKeyCertificateProvider(HostKeyCertificateProvider hostKeyCertificateProvider) { + this.hostKeyCertificateProvider = hostKeyCertificateProvider; + } + @Override public void setKeyPairProvider(KeyPairProvider keyPairProvider) { this.keyPairProvider = keyPairProvider; @@ -367,9 +381,25 @@ public abstract class AbstractServerSession extends AbstractSession implements S KeyPairProvider kpp = getKeyPairProvider(); boolean debugEnabled = log.isDebugEnabled(); - Iterable<String> provided; + Set<String> provided = null; try { - provided = (kpp == null) ? null : kpp.getKeyTypes(this); + if (kpp != null) { + provided = GenericUtils.stream(kpp.getKeyTypes(this)).collect(Collectors.toSet()); + + HostKeyCertificateProvider hostKeyCertificateProvider = getHostKeyCertificateProvider(); + if (hostKeyCertificateProvider != null) { + 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())) { + provided.add(certificate.getKeyType()); + } else { + log.info("No private key for provided certificate available. Missing private key type: {}", + certificate.getRawKeyType()); + } + } + } + } } catch (Error e) { log.warn("resolveAvailableSignaturesProposal({}) failed ({}) to get key types: {}", this, e.getClass().getSimpleName(), e.getMessage()); @@ -498,7 +528,16 @@ public abstract class AbstractServerSession extends AbstractSession implements S KeyPairProvider provider = Objects.requireNonNull(getKeyPairProvider(), "No host keys provider"); try { + HostKeyCertificateProvider hostKeyCertificateProvider = getHostKeyCertificateProvider(); + if (hostKeyCertificateProvider != null) { + OpenSshCertificate publicKey = hostKeyCertificateProvider.loadCertificate(this, keyType); + if (publicKey != null) { + KeyPair keyPair = provider.loadKey(this, publicKey.getRawKeyType()); + return new KeyPair(publicKey, keyPair.getPrivate()); + } + } 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 new file mode 100644 index 0000000..50bd39d --- /dev/null +++ b/sshd-core/src/test/java/org/apache/sshd/common/signature/OpenSSHCertificateTest.java @@ -0,0 +1,161 @@ +/* + * 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.common.signature; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.sshd.client.ClientFactoryManager; +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.NamedFactory; +import org.apache.sshd.common.PropertyResolverUtils; +import org.apache.sshd.common.SshConstants; +import org.apache.sshd.common.SshException; +import org.apache.sshd.common.keyprovider.FileHostKeyCertificateProvider; +import org.apache.sshd.common.keyprovider.FileKeyPairProvider; +import org.apache.sshd.server.SshServer; +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.BeforeClass; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.MethodSorters; +import org.junit.runners.Parameterized; + +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@RunWith(Parameterized.class) // see https://github.com/junit-team/junit/wiki/Parameterized-tests +@Parameterized.UseParametersRunnerFactory(JUnit4ClassRunnerWithParametersFactory.class) +public class OpenSSHCertificateTest extends BaseTestSupport { + private static SshServer sshd; + private static SshClient client; + private static int port; + + 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)); + this.signatureFactory = signatureFactory; + } + + @BeforeClass + public static void setupClientAndServer() throws Exception { + sshd = CoreTestSupportUtils.setupTestServer(OpenSSHCertificateTest.class); + sshd.start(); + port = sshd.getPort(); + + client = CoreTestSupportUtils.setupTestClient(OpenSSHCertificateTest.class); + client.start(); + } + + @AfterClass + public static void tearDownClientAndServer() throws Exception { + if (sshd != null) { + try { + sshd.stop(true); + } finally { + sshd = null; + } + } + + if (client != null) { + try { + client.stop(); + } finally { + client = null; + } + } + } + + @Parameterized.Parameters(name = "type={2}") + public static List<Object[]> parameters() { + List<Object[]> list = new ArrayList<>(); + + String key = "ssh_host_rsa_key"; + String certificate = "ssh_host_rsa_key_sha1-cert.pub"; + String certificateSha512 = "ssh_host_rsa_key-cert.pub"; + + // default client + list.add(new Object[]{key, certificate, null}); + list.add(new Object[]{key, certificate, Arrays.asList(BuiltinSignatures.rsa_cert, BuiltinSignatures.rsa)}); + // client does not support cert + list.add(new Object[]{key, certificate, Collections.singletonList(BuiltinSignatures.rsa)}); + // rsa variant + list.add(new Object[]{key, certificateSha512, Arrays.asList(BuiltinSignatures.rsaSHA512_cert, BuiltinSignatures.rsaSHA512)}); + list.add(new Object[]{key, certificateSha512, Arrays.asList(BuiltinSignatures.rsa_cert, BuiltinSignatures.rsaSHA512)}); + + return Collections.unmodifiableList(list); + } + + @Test + public void testOpenSshCertificates() throws Exception { + sshd.setKeyPairProvider(keyPairProvider); + sshd.setHostKeyCertificateProvider(certificateProvider); + if (signatureFactory != null) { + client.setSignatureFactories(signatureFactory); + } + + // default client + try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port) + .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 + PropertyResolverUtils.updateProperty(client, ClientFactoryManager.ABORT_ON_INVALID_CERTIFICATE, false); + try (ClientSession s = client.connect(getCurrentTestName(), "localhost", port) + .verify(CONNECT_TIMEOUT).getSession()) { + s.addPasswordIdentity(getCurrentTestName()); + s.auth().verify(AUTH_TIMEOUT); + } + + // invalid principal, abort + PropertyResolverUtils.updateProperty(client, ClientFactoryManager.ABORT_ON_INVALID_CERTIFICATE, true); + try (ClientSession s = client.connect(getCurrentTestName(), "localhost", port) + .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)); + } catch (SshException e) { + Assert.assertEquals(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED, e.getDisconnectCode()); + } + } +} diff --git a/sshd-core/src/test/resources/org/apache/sshd/common/signature/example-ca b/sshd-core/src/test/resources/org/apache/sshd/common/signature/example-ca new file mode 100644 index 0000000..5c1f3cd --- /dev/null +++ b/sshd-core/src/test/resources/org/apache/sshd/common/signature/example-ca @@ -0,0 +1,49 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +-----END OPENSSH PRIVATE KEY----- diff --git a/sshd-core/src/test/resources/org/apache/sshd/common/signature/example-ca.pub b/sshd-core/src/test/resources/org/apache/sshd/common/signature/example-ca.pub new file mode 100644 index 0000000..c5776fc --- /dev/null +++ b/sshd-core/src/test/resources/org/apache/sshd/common/signature/example-ca.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDC9ueamUQ2HJDZvS9L6fpb4umROUrPrRjb6TaIuH3K8h9uT8+8oJIM4Pqn7mlSMvX0Vi8VUCpMQ2fZnqukFrB5W8jNdGG7a/IEDF/Yg3dON1D7I43GRo/I7Qk9EGSeDayvIeh0g6dqalx7dyIg83GvsuqC+dZlQpLdFML1PciaHur1NHF9zDyLKjb+Qfxi738Y3tmfSFOd37r0r2dWLqRQ0uDk1uY2fF0YYnQonvpW3Sc6pS8lxJXAOPRYs0a3IG0AQKvzXxjg3U4NYKU0rJ8LLpoeEYbgSYwCPg6RWYg618Yu6NU7OjdQ0i3ZfoCh4O/xuW+oz/RoEEkM8bzDr7GuLeluX7u5jUXUYoeBCbgdp21xTIlCezmzYhZYb6X5Vuk481xXCaEvHAJz++Qz8M3K7YWGilgynFFm6UUHCefgGirei10X4x24FpnTxKmLpxgGWlW1 [...] diff --git a/sshd-core/src/test/resources/org/apache/sshd/common/signature/ssh_host_rsa_key b/sshd-core/src/test/resources/org/apache/sshd/common/signature/ssh_host_rsa_key new file mode 100644 index 0000000..1db1976 --- /dev/null +++ b/sshd-core/src/test/resources/org/apache/sshd/common/signature/ssh_host_rsa_key @@ -0,0 +1,38 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +-----END OPENSSH PRIVATE KEY----- diff --git a/sshd-core/src/test/resources/org/apache/sshd/common/signature/ssh_host_rsa_key-cert.pub b/sshd-core/src/test/resources/org/apache/sshd/common/signature/ssh_host_rsa_key-cert.pub new file mode 100644 index 0000000..c570d82 --- /dev/null +++ b/sshd-core/src/test/resources/org/apache/sshd/common/signature/ssh_host_rsa_key-cert.pub @@ -0,0 +1 @@ +ssh-rsa-cert-...@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgaLndJbPMJA6TJ5Gd1R+vScUdZaKELX/nEdWsfMmmOZ8AAAADAQABAAABgQDq0+az6bCKulks7E/RQBGUNlo8Pxe8Ylwc7KF2PxTrlGtD2qEjNyLMkBGAJMOME5PKiAoUNpmgxA/2oDbNQUEQpHOISnqz58SrkYwtlbRPntiu1bfZKQBxgs8oP0nVZU30ck9CmUVWdOY3R+sy85lL/g1vEwgGQPJHmnARg76TlX1xaDaR2RsHR+dz5MfPgZUn3C1SSIWSEoiuTNoh9uthHulybAhnHccTCt+90HN7nUXGSuoiu6P+Wg0p3oqClMk548MOBtSNyPFsSKFEOoivPHOfIuP1j5Of+j9iYhTp6bIrqKkUeX/y6s9iuw/d/ZlKYp2/h913iCHQ+VlJVJa/C96FXzudZ67pmuJ [...] diff --git a/sshd-core/src/test/resources/org/apache/sshd/common/signature/ssh_host_rsa_key.pub b/sshd-core/src/test/resources/org/apache/sshd/common/signature/ssh_host_rsa_key.pub new file mode 100644 index 0000000..65bea39 --- /dev/null +++ b/sshd-core/src/test/resources/org/apache/sshd/common/signature/ssh_host_rsa_key.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDq0+az6bCKulks7E/RQBGUNlo8Pxe8Ylwc7KF2PxTrlGtD2qEjNyLMkBGAJMOME5PKiAoUNpmgxA/2oDbNQUEQpHOISnqz58SrkYwtlbRPntiu1bfZKQBxgs8oP0nVZU30ck9CmUVWdOY3R+sy85lL/g1vEwgGQPJHmnARg76TlX1xaDaR2RsHR+dz5MfPgZUn3C1SSIWSEoiuTNoh9uthHulybAhnHccTCt+90HN7nUXGSuoiu6P+Wg0p3oqClMk548MOBtSNyPFsSKFEOoivPHOfIuP1j5Of+j9iYhTp6bIrqKkUeX/y6s9iuw/d/ZlKYp2/h913iCHQ+VlJVJa/C96FXzudZ67pmuJKIesGlYfQLz96Q/Tq5F+QuJm0wG0qUuq3Aa+LZcobOSw+Th/WEmQ9j9O6stZ4Pfh/BPUwPa12u19pLgKH8m25DYLjBLYZO+Zu [...] diff --git a/sshd-core/src/test/resources/org/apache/sshd/common/signature/ssh_host_rsa_key_sha1-cert.pub b/sshd-core/src/test/resources/org/apache/sshd/common/signature/ssh_host_rsa_key_sha1-cert.pub new file mode 100644 index 0000000..67116fa --- /dev/null +++ b/sshd-core/src/test/resources/org/apache/sshd/common/signature/ssh_host_rsa_key_sha1-cert.pub @@ -0,0 +1 @@ +ssh-rsa-cert-...@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgH+fKXukuzsdEZJ/q+yMiHyxAjVOMR3ZNBfiwxjfKVc0AAAADAQABAAABgQDq0+az6bCKulks7E/RQBGUNlo8Pxe8Ylwc7KF2PxTrlGtD2qEjNyLMkBGAJMOME5PKiAoUNpmgxA/2oDbNQUEQpHOISnqz58SrkYwtlbRPntiu1bfZKQBxgs8oP0nVZU30ck9CmUVWdOY3R+sy85lL/g1vEwgGQPJHmnARg76TlX1xaDaR2RsHR+dz5MfPgZUn3C1SSIWSEoiuTNoh9uthHulybAhnHccTCt+90HN7nUXGSuoiu6P+Wg0p3oqClMk548MOBtSNyPFsSKFEOoivPHOfIuP1j5Of+j9iYhTp6bIrqKkUeX/y6s9iuw/d/ZlKYp2/h913iCHQ+VlJVJa/C96FXzudZ67pmuJ [...]