diff --git a/src/main/java/com/jcraft/jsch/Buffer.java b/src/main/java/com/jcraft/jsch/Buffer.java
index e57fef608..42462c710 100644
--- a/src/main/java/com/jcraft/jsch/Buffer.java
+++ b/src/main/java/com/jcraft/jsch/Buffer.java
@@ -29,7 +29,15 @@
public class Buffer {
final byte[] tmp = new byte[4];
byte[] buffer;
+
+ // write position
+ // Tracks the current writing position in the buffer and points to where the next
+ // write operation will occur and increments as data is written
int index;
+
+ // read position - Tracks the current reading position in the buffer and points to where the next
+ // read operation will
+ // occur and increments as data is read
int s;
public Buffer(int size) {
@@ -290,6 +298,22 @@ static Buffer fromBytes(byte[][] args) {
return buf;
}
+ /**
+ * Advances the read position by the specified number of bytes.
+ *
+ * If the requested number of bytes would move the position beyond the end of the available data,
+ * the position is advanced to the very end instead.
+ *
+ * @param bytesToSkip the number of bytes to skip.
+ */
+ void readSkip(int bytesToSkip) {
+ if (bytesToSkip > getLength()) {
+ s += getLength();
+ return;
+ }
+ s += bytesToSkip;
+ }
+
/*
* static String[] chars={ "0","1","2","3","4","5","6","7","8","9", "a","b","c","d","e","f" };
* static void dump_buffer(){ int foo; for(int i=0; i
+ * The host field can contain multiple comma-separated patterns. The method returns {@code true} + * if the hostname matches ANY of the patterns. + *
+ *+ * Examples: + *
+ *+ * This method implements wildcard matching similar to OpenSSH, supporting: + *
+ *+ * The matching is case-sensitive to maintain consistency with OpenSSH behavior. + *
+ * + * @param pattern the wildcard pattern to match against (e.g., {@code *.example.com}) + * @param hostname the hostname to test (e.g., {@code host.example.com}) + * @return {@code true} if the hostname matches the pattern; {@code false} otherwise + */ + private boolean matchesWildcardPattern(String pattern, String hostname) { + if (pattern == null || hostname == null) { + return false; + } + + int pLen = pattern.length(); + int hLen = hostname.length(); + int p = 0; // pattern index + int h = 0; // hostname index + int starIdx = -1; // last '*' position in pattern + int matchIdx = 0; // position in hostname after last '*' match + + while (h < hLen) { + if (p < pLen && (pattern.charAt(p) == '?' || pattern.charAt(p) == hostname.charAt(h))) { + // Match single character or '?' + p++; + h++; + } else if (p < pLen && pattern.charAt(p) == '*') { + // Found '*', record position and try to match rest + starIdx = p; + matchIdx = h; + p++; + } else if (starIdx != -1) { + // No match, but we have a previous '*', backtrack + p = starIdx + 1; + matchIdx++; + h = matchIdx; + } else { + // No match and no '*' to backtrack to + return false; + } + } + + // Process remaining pattern characters (should be all '*') + while (p < pLen && pattern.charAt(p) == '*') { + p++; + } + + // Match if we've consumed entire pattern + return p == pLen; + } + private boolean isIncluded(String _host) { int i = 0; String hosts = this.host; diff --git a/src/main/java/com/jcraft/jsch/JSch.java b/src/main/java/com/jcraft/jsch/JSch.java index 729f2ba67..826973495 100644 --- a/src/main/java/com/jcraft/jsch/JSch.java +++ b/src/main/java/com/jcraft/jsch/JSch.java @@ -26,6 +26,8 @@ package com.jcraft.jsch; +import java.io.File; +import java.io.IOException; import java.io.InputStream; import java.util.Collections; import java.util.Enumeration; @@ -38,12 +40,18 @@ public class JSch { /** The version number. */ public static final String VERSION = Version.getVersion(); + private static final String CERTIFICATE_FILENAME_SUFFIX = "-cert.pub"; + static Hashtableusername or host are invalid.
* @return the instance of Session class.
+ * @throws JSchException if username or host are invalid.
* @see #getSession(String username, String host, int port)
- * @see com.jcraft.jsch.Session
- * @see com.jcraft.jsch.ConfigRepository
+ * @see Session
+ * @see ConfigRepository
*/
public Session getSession(String host) throws JSchException {
return getSession(null, host, 22);
@@ -337,10 +354,10 @@ public Session getSession(String host) throws JSchException {
*
* @param username user name
* @param host hostname
- * @throws JSchException if username or host are invalid.
* @return the instance of Session class.
+ * @throws JSchException if username or host are invalid.
* @see #getSession(String username, String host, int port)
- * @see com.jcraft.jsch.Session
+ * @see Session
*/
public Session getSession(String username, String host) throws JSchException {
return getSession(username, host, 22);
@@ -354,10 +371,10 @@ public Session getSession(String username, String host) throws JSchException {
* @param username user name
* @param host hostname
* @param port port number
- * @throws JSchException if username or host are invalid.
* @return the instance of Session class.
+ * @throws JSchException if username or host are invalid.
* @see #getSession(String username, String host, int port)
- * @see com.jcraft.jsch.Session
+ * @see Session
*/
public Session getSession(String username, String host, int port) throws JSchException {
if (host == null) {
@@ -383,8 +400,8 @@ protected boolean removeSession(Session session) {
* Sets the hostkey repository.
*
* @param hkrepo
- * @see com.jcraft.jsch.HostKeyRepository
- * @see com.jcraft.jsch.KnownHosts
+ * @see HostKeyRepository
+ * @see KnownHosts
*/
public void setHostKeyRepository(HostKeyRepository hkrepo) {
known_hosts = hkrepo;
@@ -395,7 +412,7 @@ public void setHostKeyRepository(HostKeyRepository hkrepo) {
*
* @param filename filename of known_hosts file.
* @throws JSchException if the given filename is invalid.
- * @see com.jcraft.jsch.KnownHosts
+ * @see KnownHosts
*/
public void setKnownHosts(String filename) throws JSchException {
if (known_hosts == null)
@@ -412,7 +429,7 @@ public void setKnownHosts(String filename) throws JSchException {
*
* @param stream the instance of InputStream from known_hosts file.
* @throws JSchException if an I/O error occurs.
- * @see com.jcraft.jsch.KnownHosts
+ * @see KnownHosts
*/
public void setKnownHosts(InputStream stream) throws JSchException {
if (known_hosts == null)
@@ -429,8 +446,8 @@ public void setKnownHosts(InputStream stream) throws JSchException {
* KnownHosts.
*
* @return current hostkey repository.
- * @see com.jcraft.jsch.HostKeyRepository
- * @see com.jcraft.jsch.KnownHosts
+ * @see HostKeyRepository
+ * @see KnownHosts
*/
public HostKeyRepository getHostKeyRepository() {
if (known_hosts == null)
@@ -492,7 +509,39 @@ public void addIdentity(String prvkey, byte[] passphrase) throws JSchException {
* @throws JSchException if passphrase is not right.
*/
public void addIdentity(String prvkey, String pubkey, byte[] passphrase) throws JSchException {
- Identity identity = IdentityFile.newInstance(prvkey, pubkey, instLogger);
+ byte[] pubkeyFileContent = null;
+ String pubkeyFile = pubkey;
+ Identity identity;
+
+ // If pubkey is null, try to auto-discover certificate file (prvkey + "-cert.pub")
+ // This mimics KeyPair.load() behavior which tries prvkey + ".pub"
+ if (pubkeyFile == null) {
+ String certFile = prvkey + CERTIFICATE_FILENAME_SUFFIX;
+ if (new File(certFile).exists()) {
+ pubkeyFile = certFile;
+ }
+ }
+
+ if (pubkeyFile != null) {
+ try {
+ pubkeyFileContent = Util.fromFile(pubkeyFile);
+ } catch (IOException e) {
+ // Only throw if pubkey was explicitly provided (not auto-discovered)
+ // This matches KeyPair.load() behavior
+ if (pubkey != null) {
+ throw new JSchException(e.toString(), e);
+ }
+ // Otherwise, silently ignore and fall through to IdentityFile
+ }
+ }
+
+ if (pubkeyFileContent != null
+ && OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(pubkeyFileContent)) {
+ identity = OpenSshCertificateAwareIdentityFile.newInstance(prvkey, pubkeyFile, instLogger);
+ } else {
+ identity = IdentityFile.newInstance(prvkey, pubkey, instLogger);
+ }
+
addIdentity(identity, passphrase);
}
@@ -507,7 +556,12 @@ public void addIdentity(String prvkey, String pubkey, byte[] passphrase) throws
*/
public void addIdentity(String name, byte[] prvkey, byte[] pubkey, byte[] passphrase)
throws JSchException {
- Identity identity = IdentityFile.newInstance(name, prvkey, pubkey, instLogger);
+ Identity identity;
+ if (OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(pubkey)) {
+ identity = OpenSshCertificateAwareIdentityFile.newInstance(name, prvkey, pubkey, instLogger);
+ } else {
+ identity = IdentityFile.newInstance(name, prvkey, pubkey, instLogger);
+ }
addIdentity(identity, passphrase);
}
@@ -665,7 +719,7 @@ public static Mapnull if no logging should take place
- * @see com.jcraft.jsch.Logger
+ * @see Logger
*/
public static void setLogger(Logger logger) {
if (logger == null)
diff --git a/src/main/java/com/jcraft/jsch/JSchInvalidHostCertificateException.java b/src/main/java/com/jcraft/jsch/JSchInvalidHostCertificateException.java
new file mode 100644
index 000000000..bca31ec74
--- /dev/null
+++ b/src/main/java/com/jcraft/jsch/JSchInvalidHostCertificateException.java
@@ -0,0 +1,10 @@
+package com.jcraft.jsch;
+
+public class JSchInvalidHostCertificateException extends JSchHostKeyException {
+
+ private static final long serialVersionUID = -1L;
+
+ public JSchInvalidHostCertificateException(String s) {
+ super(s);
+ }
+}
diff --git a/src/main/java/com/jcraft/jsch/JSchUnknownCAKeyException.java b/src/main/java/com/jcraft/jsch/JSchUnknownCAKeyException.java
new file mode 100644
index 000000000..33979e71c
--- /dev/null
+++ b/src/main/java/com/jcraft/jsch/JSchUnknownCAKeyException.java
@@ -0,0 +1,10 @@
+package com.jcraft.jsch;
+
+public class JSchUnknownCAKeyException extends JSchHostKeyException {
+
+ private static final long serialVersionUID = -1L;
+
+ JSchUnknownCAKeyException(String s) {
+ super(s);
+ }
+}
diff --git a/src/main/java/com/jcraft/jsch/JSchUnknownPublicKeyAlgorithmException.java b/src/main/java/com/jcraft/jsch/JSchUnknownPublicKeyAlgorithmException.java
new file mode 100644
index 000000000..937ec9bc7
--- /dev/null
+++ b/src/main/java/com/jcraft/jsch/JSchUnknownPublicKeyAlgorithmException.java
@@ -0,0 +1,10 @@
+package com.jcraft.jsch;
+
+public class JSchUnknownPublicKeyAlgorithmException extends JSchHostKeyException {
+
+ private static final long serialVersionUID = -1L;
+
+ JSchUnknownPublicKeyAlgorithmException(String s) {
+ super(s);
+ }
+}
diff --git a/src/main/java/com/jcraft/jsch/KeyExchange.java b/src/main/java/com/jcraft/jsch/KeyExchange.java
index 7f7b58f9e..6a441f63a 100644
--- a/src/main/java/com/jcraft/jsch/KeyExchange.java
+++ b/src/main/java/com/jcraft/jsch/KeyExchange.java
@@ -68,6 +68,11 @@ public abstract class KeyExchange {
protected byte[] K = null;
protected byte[] H = null;
protected byte[] K_S = null;
+ protected OpenSshCertificate hostKeyCertificate = null;
+
+ OpenSshCertificate getHostKeyCertificate() {
+ return hostKeyCertificate;
+ }
public abstract void init(Session session, byte[] V_S, byte[] V_C, byte[] I_S, byte[] I_C)
throws Exception;
@@ -290,11 +295,117 @@ protected byte[] normalize(byte[] secret) {
return foo;
}
+ /**
+ * Verifies the cryptographic signature of the SSH key exchange hash.
+ * + * This method performs cryptographic verification that the remote server possesses the private + * key corresponding to the public key presented during the SSH key exchange. It supports both + * traditional SSH public keys and OpenSSH certificates. + *
+ * + *+ * The method handles two distinct input formats: + *
+ *+ * For OpenSSH certificates, this method performs only the first stage of verification: + * proving that the server possesses the private key corresponding to the public key embedded in + * the certificate. The second stage (validating the certificate's CA signature, validity + * period, principals, and other certificate-specific properties) is performed separately by + * {@link OpenSshCertificateHostKeyVerifier#checkHostCertificate(Session, OpenSshCertificate)}. + *
+ * + *+ * After extracting the public key (either from the plain input or from within a certificate), the + * method: + *
+ *+ * OpenSSH certificates are a mechanism for providing cryptographic proof of authorization to access + * SSH resources. They consist of a key along with identity information and usage restrictions that + * have been signed by a certificate authority (CA). + *
+ * + *+ * This class supports both user certificates (for authenticating users to hosts) and host + * certificates (for authenticating hosts to users). + *
+ * + * @see OpenSSH Certificate + * Protocol + */ +class OpenSshCertificate { + + /** + * Certificate type constant for user certificates + */ + static final int SSH2_CERT_TYPE_USER = 1; + + /** + * Certificate type constant for host certificates + */ + static final int SSH2_CERT_TYPE_HOST = 2; + + /** + * Minimum validity period (epoch start) + */ + static final long MIN_VALIDITY = 0L; + + /** + * Maximum validity period (maximum unsigned 64-bit value) + */ + static final long MAX_VALIDITY = 0xffff_ffff_ffff_ffffL; + + /** + * The certificate key type (e.g., "ssh-rsa-cert-v01@openssh.com") + */ + private final String keyType; + + /** + * Random nonce to make certificates unique + */ + private final byte[] nonce; + + /** + * The certificate's key in SSH wire format + */ + private final byte[] certificatePublicKey; + + /** + * Certificate serial number + */ + private final long serial; + + /** + * Certificate type (user or host) + */ + private final int type; + + /** + * Certificate identifier string + */ + private final String id; + + /** + * Collection of principal names this certificate is valid for + */ + private final Collection+ * This class handles SSH identity files that contain OpenSSH certificates, which combine a public + * key with additional metadata and restrictions signed by a certificate authority. It supports all + * standard OpenSSH certificate types including RSA, DSA, ECDSA, and Ed25519. + *
+ * + *+ * The class can load certificates from file pairs (private key file + certificate file) and + * provides all the functionality needed for SSH authentication using certificates. + *
+ */ +class OpenSshCertificateAwareIdentityFile implements Identity { + + /** + * Maximum expected length for a key type string. Used as a sanity check to quickly reject + * obviously invalid data before performing string conversion. The longest current certificate key + * type is 41 characters (ecdsa-sha2-nistp521-cert-v01@openssh.com), so 100 provides sufficient + * headroom for potential future key types while still being small enough for an effective + * early-exit check. + */ + static final int MAX_KEY_TYPE_LENGTH = 100; + + /** + * parsed certificate. + **/ + private final OpenSshCertificate certificate; + + /** + * the key type declared in the first part of the file + **/ + private final String keyType; + + /** + * The entire certificate as raw bytes + */ + private final byte[] publicKeyBlob; + + /** + * The key pair containing the private key + */ + private final KeyPair kpair; + + /** + * The identity name/path + */ + private final String identity; + + /** + * Optional comment associated with the identity + */ + private final String comment; + + /** + * Determines if the given certificate file content as byte array, represents an OpenSSH + * certificate. + * + * @param certificateFileContent the certificate bytes to check + * @return {@code true} if the file content is a supported OpenSSH certificate type, {@code false} + * otherwise + */ + static boolean isOpenSshCertificateFile(byte[] certificateFileContent) { + if (certificateFileContent == null || certificateFileContent.length == 0) { + return false; + } + + byte[] keyTypeBytes = + OpenSshCertificateUtil.extractSpaceDelimitedString(certificateFileContent, 0); + + // avoid converting byte array to string if the keyType is clearly not a supported certificate + if (keyTypeBytes == null || keyTypeBytes.length == 0 + || keyTypeBytes.length > MAX_KEY_TYPE_LENGTH) { + return false; + } + + String keyType = new String(keyTypeBytes, StandardCharsets.UTF_8); + + return OpenSshCertificateKeyTypes.isCertificateKeyType(keyType); + } + + /** + * Creates a new certificate-aware identity from file paths. + * + * @param prvfile path to the private key file + * @param pubfile path to the certificate file + * @param instLogger logger instance for debugging + * @return a new Identity instance + * @throws JSchException if the files cannot be loaded or parsed + */ + static Identity newInstance(String prvfile, String pubfile, JSch.InstanceLogger instLogger) + throws JSchException { + byte[] prvkey; + byte[] certificateFileContent; + + try { + prvkey = Util.fromFile(prvfile); + certificateFileContent = Util.fromFile(pubfile); + } catch (IOException e) { + throw new JSchException(e.toString(), e); + } + return newInstance(prvfile, prvkey, certificateFileContent, instLogger); + } + + /** + * Creates a new certificate-aware identity from byte arrays. + * + * @param name the identity name + * @param prvkey the private key bytes + * @param certificateFileContentBytes the certificate bytes + * @param instLogger logger instance for debugging + * @return a new Identity instance + * @throws JSchException if the certificate cannot be parsed + */ + static Identity newInstance(String name, byte[] prvkey, byte[] certificateFileContentBytes, + JSch.InstanceLogger instLogger) throws JSchException { + OpenSshCertificate cert; + byte[] certPublicKey; + KeyPair kpair; + byte[] declaredKeyTypeBytes; + byte[] commentBytes; + byte[] keyData; + String declaredKeyType; + String comment; + + try { + declaredKeyTypeBytes = OpenSshCertificateUtil.extractKeyType(certificateFileContentBytes); + if (declaredKeyTypeBytes == null || declaredKeyTypeBytes.length == 0) { + throw new JSchException("Invalid certificate file: missing or empty key type"); + } + byte[] base64KeyDataBytes = + OpenSshCertificateUtil.extractKeyData(certificateFileContentBytes); + if (base64KeyDataBytes == null || base64KeyDataBytes.length == 0) { + throw new JSchException("Invalid certificate file: missing or empty key data"); + } + commentBytes = OpenSshCertificateUtil.extractComment(certificateFileContentBytes); + + keyData = Util.fromBase64(base64KeyDataBytes, 0, base64KeyDataBytes.length); + cert = OpenSshCertificateParser.parse(instLogger, keyData); + + declaredKeyType = Util.byte2str(declaredKeyTypeBytes, StandardCharsets.UTF_8); + comment = commentBytes != null ? Util.byte2str(commentBytes, StandardCharsets.UTF_8) : null; + + // keyType + if (OpenSshCertificateUtil.isEmpty(cert.getKeyType()) + || !cert.getKeyType().equals(declaredKeyType)) { + instLogger.getLogger().log(Logger.WARN, + "Key type declared at the beginning of the certificate file, does not correspond to the encoded key type. Declared type: '" + + declaredKeyType + "' - Encoded Key type: '" + cert.getKeyType() + "'"); + } + + if (!cert.isValidNow()) { + instLogger.getLogger().log(Logger.WARN, + "certificate is not valid. Valid after: " + + OpenSshCertificateUtil.toDateString(cert.getValidAfter()) + " - Valid before: " + + OpenSshCertificateUtil.toDateString(cert.getValidBefore())); + } + + certPublicKey = cert.getCertificatePublicKey(); + if (certPublicKey == null) { + throw new JSchException("Invalid certificate: missing public key"); + } + kpair = KeyPair.load(instLogger, prvkey, certPublicKey); + + } catch (JSchException e) { + throw e; + } catch (IllegalArgumentException e) { + throw new JSchException("Invalid certificate format: " + e.getMessage(), e); + } catch (IllegalStateException e) { + throw new JSchException("Invalid certificate data: " + e.getMessage(), e); + } catch (RuntimeException e) { + throw new JSchException("Unexpected error parsing certificate: " + e.getMessage(), e); + } + return new OpenSshCertificateAwareIdentityFile(name, declaredKeyType, keyData, cert, kpair, + comment); + } + + /** + * Private constructor for creating certificate-aware identity instances. + * + * @param name the identity name + * @param keyType the key type declared in the certificate file + * @param publicKeyBlob the decoded public key blob (certificate in binary form) + * @param certificate the parsed certificate + * @param kpair the key pair containing the private key + * @param comment the optional comment from the certificate file + */ + private OpenSshCertificateAwareIdentityFile(String name, String keyType, byte[] publicKeyBlob, + OpenSshCertificate certificate, KeyPair kpair, String comment) { + this.identity = name; + this.certificate = certificate; + this.kpair = kpair; + this.comment = comment; + this.keyType = keyType; + this.publicKeyBlob = publicKeyBlob; + } + + @Override + public boolean setPassphrase(byte[] passphrase) { + return kpair.decrypt(passphrase); + } + + @Override + public byte[] getPublicKeyBlob() { + return publicKeyBlob; + } + + @Override + public byte[] getSignature(byte[] data) { + return kpair.getSignature(data); + } + + @Override + public byte[] getSignature(byte[] data, String alg) { + String rawKeyType = OpenSshCertificateUtil.getRawKeyType(keyType); + // Fall back to keyType if rawKeyType is null (defensive check) + return kpair.getSignature(data, rawKeyType != null ? rawKeyType : keyType); + } + + @Override + public String getAlgName() { + return certificate.getKeyType(); + } + + @Override + public String getName() { + return identity; + } + + @Override + public boolean isEncrypted() { + return kpair.isEncrypted(); + } + + @Override + public void clear() { + kpair.dispose(); + } + + String getKeyType() { + return keyType; + } + + KeyPair getKpair() { + return kpair; + } + + String getIdentity() { + return identity; + } + + String getComment() { + return comment; + } +} diff --git a/src/main/java/com/jcraft/jsch/OpenSshCertificateBuffer.java b/src/main/java/com/jcraft/jsch/OpenSshCertificateBuffer.java new file mode 100644 index 000000000..19c599208 --- /dev/null +++ b/src/main/java/com/jcraft/jsch/OpenSshCertificateBuffer.java @@ -0,0 +1,127 @@ +package com.jcraft.jsch; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * A specialized buffer for parsing OpenSSH certificate data. + * + *+ * This class extends the base {@link Buffer} class to provide additional methods specific to + * parsing OpenSSH certificate format data structures, including string collections, key-value maps, + * and certificate-specific data types. + *
+ * + *+ * The buffer follows the SSH wire format protocol for data serialization, where strings and byte + * arrays are prefixed with their length as a 32-bit integer. + *
+ */ +class OpenSshCertificateBuffer extends Buffer { + + /** + * Creates a new OpenSSH certificate buffer from decoded certificate bytes. + * + * @param certificateByteDecoded the decoded certificate data + */ + OpenSshCertificateBuffer(byte[] certificateByteDecoded) { + super(certificateByteDecoded); + s = 0; + index = certificateByteDecoded.length; + + } + + /** + * Reads a length-prefixed byte array from the buffer. + * + * @return the byte array data + * @throws IllegalArgumentException if the length prefix is negative or exceeds available data + */ + byte[] getBytes() { + int reqLen = getInt(); + if (reqLen < 0) { + throw new IllegalArgumentException( + "Invalid length in certificate data: negative length " + reqLen); + } + if (reqLen > getLength()) { + throw new IllegalArgumentException("Invalid length in certificate data: requested " + reqLen + + " bytes but only " + getLength() + " available"); + } + byte[] b = new byte[reqLen]; + getByte(b); + return b; + } + + /** + * Reads a collection of UTF-8 encoded strings from the buffer. + * + *+ * This method reads all remaining data in the buffer and parses it as a sequence of + * length-prefixed UTF-8 strings. + *
+ * + * @return collection of strings + */ + Collection+ * Critical options are stored as key-value pairs in SSH wire format. + *
+ * + * @return map of critical option names to values + */ + Map+ * Extensions are stored as key-value pairs in SSH wire format. + *
+ * + * @return map of extension names to values + */ + Map+ * This method handles the SSH wire format for storing maps, where the entire map is first stored + * as a length-prefixed blob, followed by alternating keys and values, each also length-prefixed. + *
+ * + * @return map of keys to values + */ + private Map+ * This class provides the logic to authenticate a remote host based on an OpenSSH certificate + * presented during the key exchange process. The verification ensures that the host certificate was + * signed by a trusted Certificate Authority (CA) listed in the user's {@code known_hosts} file. + *
+ * The verification process includes: + *+ * This is the main entry point for host certificate validation. It orchestrates all the necessary + * checks to ensure the certificate is valid and signed by a trusted Certificate Authority (CA) + * for the connected host. + *
+ * + * @param session the current JSch session. + * @param certificate the certificate to check. + * @throws JSchException if the certificate is invalid, expired, not signed by a trusted CA, or + * fails any other validation check. Throws specific subclasses of {@link JSchException} + * for different failure reasons. + */ + static void checkHostCertificate(Session session, OpenSshCertificate certificate) + throws JSchException { + + byte[] caPublicKeyByteArray = certificate.getSignatureKey(); + + String host = session.host; + if (host == null) { + throw new JSchException("Cannot verify host certificate: session host is null"); + } + HostKeyRepository repository = session.getHostKeyRepository(); + + boolean caFound = OpenSshCertificateUtil.isCertificateSignedByTrustedCA(repository, host, + caPublicKeyByteArray); + + if (!caFound) { + throw new JSchUnknownCAKeyException("Rejected certificate '" + certificate.getId() + "': " + + "Certification Authority not in the known hosts or revoked for " + host); + } + + Buffer caPublicKeyBuffer = new Buffer(caPublicKeyByteArray); + String caPublicKeyAlgorithm = Util.byte2str(caPublicKeyBuffer.getString()); + String certificateId = certificate.getId(); + + // check if this is a Host certificate + if (!certificate.isHostCertificate()) { + throw new JSchInvalidHostCertificateException("rejected HostKey: certificate id='" + + certificateId + "' is not a host certificate. Host:" + host); + } + + if (!certificate.isValidNow()) { + throw new JSchInvalidHostCertificateException( + "rejected HostKey: certificate not valid (expired or not yet valid) for id:" + + certificateId); + } + + checkSignature(certificate, caPublicKeyAlgorithm, session); + + Collection+ * This method ensures that the certificate was actually signed by the private key corresponding + * to the public key of the Certificate Authority. + *
+ * + * @param certificate the certificate to verify. + * @param caPublicKeyAlgorithm the algorithm of the CA's public key. + * @throws JSchException if the signature algorithm does not match the CA key algorithm or if the + * signature is cryptographically invalid. + */ + static void checkSignature(OpenSshCertificate certificate, String caPublicKeyAlgorithm, + Session session) throws JSchException { + // Check signature + SignatureWrapper signature = getSignatureWrapper(certificate, caPublicKeyAlgorithm, session); + byte[][] publicKey = + OpenSshCertificateUtil.parsePublicKeyComponents(certificate.getSignatureKey()); + boolean verified; + try { + signature.init(); + signature.setPubKey(publicKey); + signature.update(certificate.getMessage()); + verified = signature.verify(certificate.getSignature()); + } catch (Exception e) { + throw new JSchException("invalid signature key", e); + } + + if (!verified) { + throw new JSchInvalidHostCertificateException( + "rejected HostKey: signature verification failed"); + } + } + + /** + * Creates and validates a {@link SignatureWrapper} for the certificate. + *+ * This helper method extracts the signature algorithm from the certificate and verifies that it + * matches the algorithm of the signing CA's key. It also validates that the signature algorithm + * is allowed and available according to the {@code ca_signature_algorithms} configuration. + *
+ * + * @param certificate the OpenSSH certificate. + * @param caPublicKeyAlgorithm the expected public key algorithm of the CA. + * @param session the current session. + * @return a configured {@link SignatureWrapper} instance. + * @throws JSchException if the signature algorithm does not match the CA's key algorithm, is not + * in the allowed CA signature algorithms list, is not available at runtime, or if the + * wrapper cannot be instantiated. + */ + static SignatureWrapper getSignatureWrapper(OpenSshCertificate certificate, + String caPublicKeyAlgorithm, Session session) throws JSchException { + byte[] certificateSignature = certificate.getSignature(); + Buffer signatureBuffer = new Buffer(certificateSignature); + String signatureAlgorithm = Util.byte2str(signatureBuffer.getString()); + + if (!caPublicKeyAlgorithm.equals(signatureAlgorithm)) { + throw new JSchInvalidHostCertificateException( + "rejected HostKey: signature verification failed, " + "signature algorithm: '" + + signatureAlgorithm + "' - CA public Key algorithm: '" + caPublicKeyAlgorithm + "'"); + } + + // Validate that the CA signature algorithm is allowed and available at runtime + session.checkCASignatureAlgorithm(signatureAlgorithm); + + return new SignatureWrapper(signatureAlgorithm, session); + } + +} diff --git a/src/main/java/com/jcraft/jsch/OpenSshCertificateKeyTypes.java b/src/main/java/com/jcraft/jsch/OpenSshCertificateKeyTypes.java new file mode 100644 index 000000000..7f5b8b3e6 --- /dev/null +++ b/src/main/java/com/jcraft/jsch/OpenSshCertificateKeyTypes.java @@ -0,0 +1,161 @@ +package com.jcraft.jsch; + +/** + * Constants for OpenSSH certificate key type identifiers. + *+ * This class provides a centralized location for all OpenSSH certificate key type strings as + * defined in the OpenSSH certificate protocol. These constants are used throughout JSch for + * identifying and handling certificate-based authentication. + *
+ * + *
+ * Certificate key types follow the naming convention: {@code
+ * For example, {@code ssh-rsa-cert-v01@openssh.com} returns {@code ssh-rsa}. + *
+ * + * @param certificateKeyType the certificate key type + * @return the base key type, or the original string if it's not a certificate type, or + * {@code null} if the input is {@code null}, empty or blank + */ + static String getBaseKeyType(String certificateKeyType) { + if (certificateKeyType == null || certificateKeyType.isEmpty()) { + return null; + } + if (certificateKeyType.endsWith(CERT_SUFFIX)) { + return certificateKeyType.substring(0, certificateKeyType.length() - CERT_SUFFIX.length()); + } + return certificateKeyType; + } + + /** + * Returns the certificate key type for a given base signature algorithm. + *+ * For example, {@code ssh-rsa} returns {@code ssh-rsa-cert-v01@openssh.com}. + *
+ * + * @param baseAlgorithm the base signature algorithm (e.g., "ssh-rsa", "ssh-ed25519") + * @return the corresponding certificate key type, or {@code null} if the algorithm is not + * recognized or is {@code null} + */ + static String getCertificateKeyType(String baseAlgorithm) { + if (baseAlgorithm == null) { + return null; + } + switch (baseAlgorithm) { + case "ssh-rsa": + return SSH_RSA_CERT_V01; + case "rsa-sha2-256": + return RSA_SHA2_256_CERT_V01; + case "rsa-sha2-512": + return RSA_SHA2_512_CERT_V01; + case "ssh-dss": + return SSH_DSS_CERT_V01; + case "ecdsa-sha2-nistp256": + return ECDSA_SHA2_NISTP256_CERT_V01; + case "ecdsa-sha2-nistp384": + return ECDSA_SHA2_NISTP384_CERT_V01; + case "ecdsa-sha2-nistp521": + return ECDSA_SHA2_NISTP521_CERT_V01; + case "ssh-ed25519": + return SSH_ED25519_CERT_V01; + case "ssh-ed448": + return SSH_ED448_CERT_V01; + default: + return null; + } + } +} diff --git a/src/main/java/com/jcraft/jsch/OpenSshCertificateParser.java b/src/main/java/com/jcraft/jsch/OpenSshCertificateParser.java new file mode 100644 index 000000000..5f1464f0f --- /dev/null +++ b/src/main/java/com/jcraft/jsch/OpenSshCertificateParser.java @@ -0,0 +1,206 @@ +package com.jcraft.jsch; + +import com.jcraft.jsch.JSch.InstanceLogger; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collection; + +/** + * Parser for OpenSSH certificate format. + * + *+ * This class is responsible for parsing OpenSSH certificates from their string representation + * (typically found in .pub files) into structured {@link OpenSshCertificate} objects. It handles + * the base64 decoding and binary parsing of all certificate fields according to the OpenSSH + * certificate specification. + *
+ * + *+ * The parser supports all standard OpenSSH certificate types including RSA, DSA, ECDSA, and Ed25519 + * certificates. + *
+ * + * @see OpenSshCertificate + * @see OpenSSH Certificate + * Protocol + */ +class OpenSshCertificateParser { + + /** + * Parses the certificate data and returns a complete {@link OpenSshCertificate} object. + * + *+ * This method reads all fields from the certificate in the order specified by the OpenSSH + * certificate format: + *
+ *+ * The uncompressed point format consists of a 0x04 prefix byte followed by the X and Y + * coordinates. The total length is therefore: coordinate_size * 2 + 1. + *
+ * + * @param keyType the certificate key type + * @return the expected point length in bytes + * @throws JSchException if the key type is not a recognized ECDSA certificate type + */ + private static int getExpectedEcdsaPointLength(String keyType) throws JSchException { + switch (keyType) { + case OpenSshCertificateKeyTypes.ECDSA_SHA2_NISTP256_CERT_V01: + return OpenSshCertificateUtil.ECDSA_P256_POINT_LENGTH; + case OpenSshCertificateKeyTypes.ECDSA_SHA2_NISTP384_CERT_V01: + return OpenSshCertificateUtil.ECDSA_P384_POINT_LENGTH; + case OpenSshCertificateKeyTypes.ECDSA_SHA2_NISTP521_CERT_V01: + return OpenSshCertificateUtil.ECDSA_P521_POINT_LENGTH; + default: + throw new JSchException("Unknown ECDSA certificate key type: " + keyType); + } + } +} diff --git a/src/main/java/com/jcraft/jsch/OpenSshCertificateUtil.java b/src/main/java/com/jcraft/jsch/OpenSshCertificateUtil.java new file mode 100644 index 000000000..3bcc11214 --- /dev/null +++ b/src/main/java/com/jcraft/jsch/OpenSshCertificateUtil.java @@ -0,0 +1,635 @@ +package com.jcraft.jsch; + +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +class OpenSshCertificateUtil { + + /** + * Predicate that identifies Certificate Authority (CA) public key entries in a known_hosts file. + *+ * This predicate tests whether a {@link HostKey} represents a trusted CA entry, identified by the + * {@code @cert-authority} marker in the known_hosts file. CA entries are used to validate OpenSSH + * certificates presented by hosts during authentication. + *
+ */ + static final Predicate+ * This predicate tests whether a {@link HostKey} is marked as revoked (using the {@code @revoked} + * marker) or is {@code null}. It implements fail-closed security semantics by treating + * {@code null} entries as revoked. + *
+ */ + static final Predicate+ * In the uncompressed point format, the first byte is 0x04, followed by the X and Y coordinates. + * This is the standard format used in SSH for ECDSA public keys. + *
+ * + * @see SEC 1: Elliptic Curve Cryptography + */ + static final int EC_POINT_FORMAT_UNCOMPRESSED = 0x04; + + /** + * Expected public key length for Ed25519 keys (32 bytes). + *+ * Ed25519 uses a 256-bit (32-byte) public key as defined in RFC 8032. + *
+ * + * @see RFC 8032: Edwards-Curve Digital Signature + * Algorithm (EdDSA) + */ + static final int ED25519_PUBLIC_KEY_LENGTH = 32; + + /** + * Expected public key length for Ed448 keys (57 bytes). + *+ * Ed448 uses a 456-bit public key, which requires 57 bytes when encoded, as defined in RFC 8032. + *
+ * + * @see RFC 8032: Edwards-Curve Digital Signature + * Algorithm (EdDSA) + */ + static final int ED448_PUBLIC_KEY_LENGTH = 57; + + /** + * Expected uncompressed EC point length for NIST P-256 keys (65 bytes). + *+ * The uncompressed point format is: 0x04 || X coordinate (32 bytes) || Y coordinate (32 bytes). + *
+ * + * @see RFC 5656: Elliptic Curve Algorithm + * Integration in the Secure Shell Transport Layer + */ + static final int ECDSA_P256_POINT_LENGTH = 65; + + /** + * Expected uncompressed EC point length for NIST P-384 keys (97 bytes). + *+ * The uncompressed point format is: 0x04 || X coordinate (48 bytes) || Y coordinate (48 bytes). + *
+ * + * @see RFC 5656: Elliptic Curve Algorithm + * Integration in the Secure Shell Transport Layer + */ + static final int ECDSA_P384_POINT_LENGTH = 97; + + /** + * Expected uncompressed EC point length for NIST P-521 keys (133 bytes). + *+ * The uncompressed point format is: 0x04 || X coordinate (66 bytes) || Y coordinate (66 bytes). + *
+ * + * @see RFC 5656: Elliptic Curve Algorithm + * Integration in the Secure Shell Transport Layer + */ + static final int ECDSA_P521_POINT_LENGTH = 133; + + /** + * Trims leading and trailing whitespace from the input string. Returns an empty string if the + * input is null. + * + * @param s the string to trim, may be null + * @return the trimmed string, or an empty string if the input is null + */ + static String trimToEmptyIfNull(String s) { + if (s == null) { + return ""; + } else { + return s.trim(); + } + } + + /** + * Checks if a String is empty or null. + * + * @param string the String to check, may be null + * @return true if the CharSequence is null or has a length of 0, false otherwise + */ + static boolean isEmpty(String string) { + return string == null || string.isEmpty(); + } + + /** + * Checks if a Collection is empty or null. + * + * @param c the Collection to check, may be null + * @return true if the Collection is null or has 0 elements, false otherwise + */ + static boolean isEmpty(Collection> c) { + return c == null || c.isEmpty(); + } + + /** + * Checks if a Map is empty or null. + * + * @param c the Map to check, may be null + * @return true if the Map is null or has 0 elements, false otherwise + */ + static boolean isEmpty(Map, ?> c) { + return c == null || c.isEmpty(); + } + + /** + * Extracts the key type from a certificate file content byte array. This method assumes the key + * type is the first field (index 0) in the space-delimited string. + * + * @param certificateFileContent The content of the certificate file as a byte array. + * @return The key type as a byte array, or {@code null} if the content is null, empty, or the + * field does not exist. + */ + static byte[] extractKeyType(byte[] certificateFileContent) { + return extractSpaceDelimitedString(certificateFileContent, 0); + } + + /** + * Extracts the comment from a certificate file content byte array. This method assumes the + * comment is the third field (index 2) in the space-delimited string. + * + * @param certificateFileContent The content of the certificate file as a byte array. + * @return The comment as a byte array, or {@code null} if the content is null, empty, or the + * field does not exist. + */ + static byte[] extractComment(byte[] certificateFileContent) { + return extractSpaceDelimitedString(certificateFileContent, 2); + } + + /** + * Extracts the key data from a certificate file content byte array. This method assumes the key + * data is the second field (index 1) in the space-delimited string. + * + * @param certificateFileContent The content of the certificate file as a byte array. + * @return The key data as a byte array, or {@code null} if the content is null, empty, or the + * field does not exist. + */ + static byte[] extractKeyData(byte[] certificateFileContent) { + return extractSpaceDelimitedString(certificateFileContent, 1); + } + + /** + * Checks if a byte represents a whitespace character (space, tab, newline, or carriage return). + * + * @param b the byte to check + * @return true if the byte is a whitespace character, false otherwise + */ + private static boolean isWhitespace(byte b) { + return b == ' ' || b == '\t' || b == '\n' || b == '\r'; + } + + /** + * A utility method to safely extract a space-delimited field from a certificate content byte + * array at a given index. This method avoids String allocation by working directly with bytes and + * returning a byte array. It handles whitespace (space, tab, newline, carriage return) as + * delimiters. + * + * @param certificate The byte array to be parsed, typically representing certificate content. + * @param index The zero-based index of the field to extract. + * @return The byte array field at the specified index, or {@code null} if the input is invalid or + * the index is out of bounds. + */ + static byte[] extractSpaceDelimitedString(byte[] certificate, int index) { + if (certificate == null || certificate.length == 0) { + return null; + } + if (index < 0) { + return null; + } + + int fieldCount = 0; + int fieldStart = -1; + int i = 0; + + // Skip leading whitespace + while (i < certificate.length && isWhitespace(certificate[i])) { + i++; + } + + while (i < certificate.length) { + // Found start of a field + if (!isWhitespace(certificate[i])) { + if (fieldStart == -1) { + fieldStart = i; + } + i++; + } else { + // Found end of a field + if (fieldStart != -1) { + if (fieldCount == index) { + // This is the field we want - copy and return it + int length = i - fieldStart; + byte[] result = new byte[length]; + System.arraycopy(certificate, fieldStart, result, 0, length); + return result; + } + fieldCount++; + fieldStart = -1; + } + + // Skip whitespace + while (i < certificate.length && isWhitespace(certificate[i])) { + i++; + } + } + } + // Handle last field (no trailing whitespace) + if (fieldStart != -1 && fieldCount == index) { + int length = i - fieldStart; + byte[] result = new byte[length]; + System.arraycopy(certificate, fieldStart, result, 0, length); + return result; + } + + return null; + } + + /** + * Determines whether the given {@link OpenSshCertificate} is valid at the current local system + * time. + * + * @param cert to check + * @return {@code true} if the certificate is valid according to its timestamps, {@code false} + * otherwise + */ + static boolean isValidNow(OpenSshCertificate cert) { + return isValidNow(cert, Instant.now().getEpochSecond()); + } + + /** + * Determines whether the given {@link OpenSshCertificate} is valid at the given time. + * + * @param cert to check + * @param now the current time in seconds since epoch + * @return {@code true} if the certificate is valid according to its timestamps, {@code false} + * otherwise + */ + static boolean isValidNow(OpenSshCertificate cert, long now) { + return Long.compareUnsigned(cert.getValidAfter(), now) <= 0 + && Long.compareUnsigned(now, cert.getValidBefore()) < 0; + } + + /** + * Converts a Unix timestamp to a {@link Date} string representation. + *+ * If the timestamp is negative, it indicates an infinite expiration time, and the method returns + * the string "infinity". Otherwise, it converts the timestamp from seconds to milliseconds and + * returns the default string representation of the resulting {@link Date} object. + * + * @param timestamp The Unix timestamp in seconds. + * @return A string representing the date, or "infinity" if the timestamp is negative. + */ + static String toDateString(long timestamp) { + if (timestamp < 0) { + return "infinity"; + } + return SftpATTRS.toDateString(timestamp); + } + + /** + * Extracts the raw key type from a given key type string. + *
+ * For certificate key types (ending with {@code -cert-v01@openssh.com}), this method returns the + * base algorithm name (e.g., {@code ssh-rsa-cert-v01@openssh.com} returns {@code ssh-rsa}). For + * non-certificate key types, the original string is returned unchanged. + *
+ *+ * This method delegates to {@link OpenSshCertificateKeyTypes#getBaseKeyType(String)}. + *
+ * + * @param keyType The full key type string, may be null. + * @return The raw key type (e.g., "ssh-rsa"), or the original string if not a certificate type, + * or null if the input is null or empty. + */ + static String getRawKeyType(String keyType) { + return OpenSshCertificateKeyTypes.getBaseKeyType(keyType); + } + + /** + * Filters out OpenSSH certificate types whose underlying signature algorithms are unavailable. + *+ * This method ensures that JSch does not attempt to negotiate certificate-based host key + * algorithms when the corresponding signature implementation is unavailable on the system. This + * prevents connection failures that would occur if JSch negotiated a certificate type (e.g., + * {@code ssh-ed25519-cert-v01@openssh.com}) but could not verify it because the base signature + * algorithm (e.g., {@code ssh-ed25519}) requires Java 15+ or Bouncy Castle. + *
+ * + *+ * The method maintains an internal mapping between base signature algorithms and their + * certificate counterparts: + *
+ *+ * This method performs the critical CA validation step for OpenSSH certificate authentication. It + * verifies that: + *
+ *+ * The validation follows these steps: + *
+ * + *
+ * 1. Retrieve all {@code @cert-authority} entries from known_hosts
+ * 2. Filter to only non-null entries
+ * 3. Check each CA to ensure it hasn't been revoked
+ * 4. Test if any remaining CA:
+ * - Matches the host pattern (e.g., *.example.com matches host.example.com)
+ * - Has a public key that equals the certificate's signing CA key
+ *
+ *
+ * + * A CA is considered revoked if there exists a {@code @revoked} entry in the known_hosts file + * with the same public key value. The revocation check uses + * {@link #hasBeenRevoked(HostKeyRepository, HostKey)} to ensure that compromised CA keys are + * rejected even if they appear as {@code @cert-authority}. + *
+ * + * + * @param repository the {@link HostKeyRepository} containing known_hosts entries, must not be + * {@code null} + * @param host the hostname or host pattern being connected to (e.g., "host.example.com" or + * "[host.example.com]:2222"), must not be {@code null} + * @param caPublicKey the raw CA public key bytes from the certificate that needs validation, must + * not be {@code null} + * @return {@code true} if a trusted, non-revoked CA matching the host pattern signed the + * certificate; {@code false} if no matching CA exists, all matching CAs are revoked, or + * the CA key doesn't match + */ + static boolean isCertificateSignedByTrustedCA(HostKeyRepository repository, String host, + byte[] caPublicKey) throws JSchException { + return getTrustedCAs(repository).stream().filter(Objects::nonNull) + .filter(hostkey -> !hasBeenRevoked(repository, hostkey)).anyMatch(trustedCA -> { + byte[] trustedCAKeyBytes = trustedCA.key; + if (trustedCAKeyBytes == null) { + return false; + } + return trustedCA.isWildcardMatched(host) + && Util.arraysequals(trustedCAKeyBytes, caPublicKey); + }); + } + + /** + * Retrieves all trusted Certificate Authority (CA) host keys from the repository. + *+ * This method extracts all entries from the known_hosts file that are marked with the + * {@code @cert-authority} marker, which designates them as trusted CAs for certificate-based host + * authentication. + *
+ * + * @param knownHosts the {@link HostKeyRepository} to query (typically populated from a + * known_hosts file), may be empty but must not be {@code null} + * @return a {@link Set} of {@link HostKey} objects representing all CA entries; returns empty set + * if repository is empty or contains no CA entries, never returns {@code null} + */ + static Set+ * This method extracts all entries from the known_hosts file that are marked with the + * {@code @revoked} marker, indicating keys that have been explicitly blacklisted and must not be + * trusted for authentication. + *
+ *+ * Revoked entries take precedence over trusted entries. If a key appears in both: + *
+ *+ * The key must be rejected. Use {@link #hasBeenRevoked(HostKeyRepository, HostKey)} to check if a + * specific key has been revoked. + *
+ * + * @param knownHosts the {@link HostKeyRepository} to query (typically populated from a + * known_hosts file), may be empty but must not be {@code null} + * @return a {@link Set} of {@link HostKey} objects representing all revoked entries (includes + * {@code null} entries due to fail-closed security); returns empty set if repository + * contains no revoked entries, never returns {@code null} + */ + static Set+ * This method determines whether a {@link HostKey} appears in the known_hosts file with the + * {@code @revoked} marker, indicating it should not be trusted for authentication. It compares + * the key's public key value against all revoked entries. + *
+ * + * @param knownHosts the {@link HostKeyRepository} to query for revoked entries, must not be + * {@code null} + * @param key the {@link HostKey} to check for revocation, may be {@code null} + * @return {@code true} if {@code key} is {@code null} (fail-closed) or if the key's public key + * value matches any {@code @revoked} entry in the repository; {@code false} if the key is + * valid and not revoked + */ + static boolean hasBeenRevoked(HostKeyRepository knownHosts, HostKey key) { + if (key == null) { + return true; + } + byte[] keyBytes = key.key; + if (keyBytes == null) { + // Fail-closed: treat keys with null key value as revoked + return true; + } + // Compare raw byte arrays directly using timing-safe comparison + return getRevokedKeys(knownHosts).stream().filter(Objects::nonNull) + .map(revokedKey -> revokedKey.key).filter(Objects::nonNull) + .anyMatch(revokedKeyBytes -> Util.arraysequals(revokedKeyBytes, keyBytes)); + } + + /** + * Parses a raw public key byte array into its constituent mathematical components. + *+ * Different public key algorithms (RSA, DSS, ECDSA, EdDSA) have different structures. This method + * decodes the algorithm-specific format and returns the components needed for cryptographic + * operations. + *
+ * + * @param publicKeyBlob the raw byte array of the public key blob in SSH wire format. + * @return A 2D byte array where each inner array is a component of the public key. + * @throws JSchException if the public key algorithm is unknown or the key format is corrupt. + */ + static byte[][] parsePublicKeyComponents(byte[] publicKeyBlob) throws JSchException { + Buffer buffer = new Buffer(publicKeyBlob); + String algorithm = Util.byte2str(buffer.getString()); + return parsePublicKeyComponentsFromBuffer(algorithm, buffer); + } + + /** + * Parses public key components from a buffer based on the algorithm. + *+ * This method reads key components from the current buffer position. The buffer should be + * positioned after the algorithm string. + *
+ * + * @param algorithm the public key algorithm name. + * @param buffer the buffer containing the key components. + * @return A 2D byte array where each inner array is a component of the public key. + * @throws JSchException if the algorithm is unknown or the key format is invalid. + */ + static byte[][] parsePublicKeyComponentsFromBuffer(String algorithm, Buffer buffer) + throws JSchException { + + if (algorithm.startsWith("ssh-rsa") || algorithm.startsWith("rsa-")) { + byte[] e = buffer.getMPInt(); + byte[] n = buffer.getMPInt(); + return new byte[][] {e, n}; + } + + if (algorithm.startsWith("ssh-dss")) { + byte[] p = buffer.getMPInt(); + byte[] q = buffer.getMPInt(); + byte[] g = buffer.getMPInt(); + byte[] y = buffer.getMPInt(); + // Order matches SignatureDSA.setPubKey(y, p, q, g) + return new byte[][] {y, p, q, g}; + } + + if (algorithm.startsWith("ecdsa-sha2-")) { + // https://www.rfc-editor.org/rfc/rfc5656#section-3.1 + buffer.getString(); // skip curve identifier + int len = buffer.getInt(); + if (len < ECDSA_P256_POINT_LENGTH || len > ECDSA_P521_POINT_LENGTH) { + throw new JSchException("Invalid ECDSA public key length: " + len + " (expected between " + + ECDSA_P256_POINT_LENGTH + " and " + ECDSA_P521_POINT_LENGTH + ")"); + } + int pointFormat = buffer.getByte(); + if (pointFormat != EC_POINT_FORMAT_UNCOMPRESSED) { + throw new JSchException( + "Invalid ECDSA public key format: expected uncompressed point (0x04), got 0x" + + Integer.toHexString(pointFormat & 0xff)); + } + byte[] r = new byte[(len - 1) / 2]; + byte[] s = new byte[(len - 1) / 2]; + buffer.getByte(r); + buffer.getByte(s); + // Order matches SignatureECDSA.setPubKey(r, s) + return new byte[][] {r, s}; + } + + if (algorithm.startsWith("ssh-ed25519")) { + int keyLength = buffer.getInt(); + if (keyLength != ED25519_PUBLIC_KEY_LENGTH) { + throw new JSchException("Invalid Ed25519 public key length: expected " + + ED25519_PUBLIC_KEY_LENGTH + ", got " + keyLength); + } + byte[] pubKey = new byte[keyLength]; + buffer.getByte(pubKey); + return new byte[][] {pubKey}; + } + + if (algorithm.startsWith("ssh-ed448")) { + int keyLength = buffer.getInt(); + if (keyLength != ED448_PUBLIC_KEY_LENGTH) { + throw new JSchException("Invalid Ed448 public key length: expected " + + ED448_PUBLIC_KEY_LENGTH + ", got " + keyLength); + } + byte[] pubKey = new byte[keyLength]; + buffer.getByte(pubKey); + return new byte[][] {pubKey}; + } + + throw new JSchUnknownPublicKeyAlgorithmException( + "Unknown algorithm '" + algorithm.trim() + "'"); + } +} diff --git a/src/main/java/com/jcraft/jsch/Session.java b/src/main/java/com/jcraft/jsch/Session.java index 458033f31..7457b24be 100644 --- a/src/main/java/com/jcraft/jsch/Session.java +++ b/src/main/java/com/jcraft/jsch/Session.java @@ -806,6 +806,19 @@ private void send_kexinit() throws Exception { getLogger().log(Logger.DEBUG, "server_host_key proposal after removing unavailable algos is: " + server_host_key); } + + // Also filter out certificate types for unavailable base algorithms + server_host_key = + OpenSshCertificateUtil.filterUnavailableCertTypes(server_host_key, not_available_shks); + if (server_host_key == null) { + throw new JSchException("There are not any available sig algorithm."); + } + + if (getLogger().isEnabled(Logger.DEBUG)) { + getLogger().log(Logger.DEBUG, + "server_host_key proposal after removing unavailable cert algos is: " + + server_host_key); + } } String prefer_hkr = getConfig("prefer_known_host_key_types"); @@ -929,22 +942,57 @@ private void send_extinfo() throws Exception { } private void checkHost(String chost, int port, KeyExchange kex) throws JSchException { - String shkc = getConfig("StrictHostKeyChecking"); + OpenSshCertificate certificate = kex.getHostKeyCertificate(); + if (certificate != null) { + try { + OpenSshCertificateHostKeyVerifier.checkHostCertificate(this, certificate); + return; + } catch (JSchException e) { + if (getConfig("host_certificate_to_key_fallback").equals("no")) { + throw e; + } + // Fallback to public key verification - log warning as this bypasses CA validation + if (getLogger().isEnabled(Logger.WARN)) { + getLogger().log(Logger.WARN, + "Host certificate validation failed, falling back to public key verification. " + + "This bypasses CA trust validation. Reason: " + e.getMessage()); + } + byte[] K_S = certificate.getCertificatePublicKey(); + if (K_S == null) { + throw new JSchException( + "Invalid certificate '" + certificate.getId() + "': missing public key"); + } + String key_type = kex.getKeyType(); + String key_footprint = kex.getFingerPrint(); + String keyAlgorithmName = kex.getKeyAlgorithName(); + doCheckHostKey(chost, key_type, key_footprint, keyAlgorithmName, K_S); + } + } + checkHostKey(chost, port, kex); + } + private void checkHostKey(String chost, int port, KeyExchange kex) throws JSchException { if (hostKeyAlias != null) { chost = hostKeyAlias; } - - // System.err.println("shkc: "+shkc); - byte[] K_S = kex.getHostKey(); String key_type = kex.getKeyType(); String key_fprint = kex.getFingerPrint(); + String keyAlgorithmName = kex.getKeyAlgorithName(); + + doCheckHostKey(chost, key_type, key_fprint, keyAlgorithmName, K_S); + } + + private void doCheckHostKey(String chost, String key_type, String key_fprint, + String keyAlgorithmName, byte[] K_S) throws JSchException { if (hostKeyAlias == null && port != 22) { chost = ("[" + chost + "]:" + port); } + String shkc = getConfig("StrictHostKeyChecking"); + // System.err.println("shkc: "+shkc); + HostKeyRepository hkr = getHostKeyRepository(); String hkh = getConfig("HashKnownHosts"); @@ -993,7 +1041,7 @@ private void checkHost(String chost, int port, KeyExchange kex) throws JSchExcep } synchronized (hkr) { - hkr.remove(chost, kex.getKeyAlgorithName(), null); + hkr.remove(chost, keyAlgorithmName, null); insert = true; } } @@ -1025,7 +1073,7 @@ private void checkHost(String chost, int port, KeyExchange kex) throws JSchExcep } if (i == HostKeyRepository.OK) { - HostKey[] keys = hkr.getHostKey(chost, kex.getKeyAlgorithName()); + HostKey[] keys = hkr.getHostKey(chost, keyAlgorithmName); String _key = Util.byte2str(Util.toBase64(K_S, 0, K_S.length, true)); for (int j = 0; j < keys.length; j++) { if (keys[j].getKey().equals(_key) && keys[j].getMarker().equals("@revoked")) { @@ -1167,11 +1215,15 @@ private Channel getChannelById(int id) { } Buffer read(Buffer buf) throws Exception { + // determine which encryption and authentication mode is currently active + // for the server-to-client (s2c) connection int j = 0; boolean isChaCha20 = (s2ccipher != null && s2ccipher.isChaCha20()); boolean isAEAD = (s2ccipher != null && s2ccipher.isAEAD()); boolean isEtM = (!isChaCha20 && !isAEAD && s2ccipher != null && s2cmac != null && s2cmac.isEtM()); + + // Decrypting while (true) { buf.reset(); if (isChaCha20) { @@ -1265,17 +1317,23 @@ Buffer read(Buffer buf) throws Exception { s2ccipher.update(buf.buffer, 4, j, buf.buffer, 4); } } else { + // fall back to the older Encrypt-and-MAC mode. io.getByte(buf.buffer, buf.index, s2ccipher_size); buf.index += s2ccipher_size; if (s2ccipher != null) { s2ccipher.update(buf.buffer, 0, s2ccipher_size, buf.buffer, 0); } + + // calculating length of the incoming packet j = ((buf.buffer[0] << 24) & 0xff000000) | ((buf.buffer[1] << 16) & 0x00ff0000) | ((buf.buffer[2] << 8) & 0x0000ff00) | ((buf.buffer[3]) & 0x000000ff); // RFC 4253 6.1. Maximum Packet Length if (j < 5 || j > PACKET_MAX_SIZE) { start_discard(buf, s2ccipher, s2cmac, 0, PACKET_MAX_SIZE); } + + // calculates how many more bytes need to be read from the buffer to get the rest of the + // encrypted SSH packet int need = j + 4 - s2ccipher_size; // if(need<0){ // throw new IOException("invalid data"); @@ -2251,7 +2309,6 @@ public void setThreadFactory(ThreadFactory threadFactory) { this.threadFactory = Objects.requireNonNull(threadFactory); } - /** * Returns the thread factory used by this instance. * @@ -3256,8 +3313,11 @@ private String[] checkSignatures(String sigs) { String[] _sigs = Util.split(sigs, ","); for (int i = 0; i < _sigs.length; i++) { try { + // Map certificate algorithm names to their base signature algorithm. + // Certificate algorithms use the same Signature implementations as their base algorithms. + String sigToCheck = OpenSshCertificateKeyTypes.getBaseKeyType(_sigs[i]); Class extends Signature> c = - Class.forName(JSch.getConfig(_sigs[i])).asSubclass(Signature.class); + Class.forName(JSch.getConfig(sigToCheck)).asSubclass(Signature.class); final Signature sig = c.getDeclaredConstructor().newInstance(); sig.init(); } catch (Exception | LinkageError e) { @@ -3276,6 +3336,57 @@ private String[] checkSignatures(String sigs) { return foo; } + /** + * Checks if a CA signature algorithm is allowed and available at runtime. + *+ * This method validates that the given algorithm is: + *
null.
diff --git a/src/main/java/com/jcraft/jsch/SignatureWrapper.java b/src/main/java/com/jcraft/jsch/SignatureWrapper.java
new file mode 100644
index 000000000..005b2836f
--- /dev/null
+++ b/src/main/java/com/jcraft/jsch/SignatureWrapper.java
@@ -0,0 +1,173 @@
+package com.jcraft.jsch;
+
+/**
+ * A factory and wrapper class for creating and managing digital signature instances.
+ * + * This class abstracts the creation of specific signature algorithm implementations (like RSA, DSA, + * ECDSA, EdDSA) by dynamically loading them based on an algorithm name. It also provides a generic + * interface for setting the public key and performing signature operations (init, update, verify, + * sign) by delegating calls to the underlying signature instance. + *
+ */ +class SignatureWrapper implements Signature { + + private final Signature signature; + + private final PubKeySetter publicKeySetter; + + private final PubKeyParameterValidator pubKeyParameterValidator; + + /** + * Constructs a {@code SignatureWrapper} for the specified signature algorithm. + *+ * This constructor uses reflection to find and instantiate the appropriate {@link Signature} + * implementation based on the algorithm name retrieved from JSch's configuration. It also sets up + * handlers for public key validation and setting based on the algorithm type. + *
+ * + * @param algorithm the name of the signature algorithm (e.g., "ssh-rsa", "ssh-dss"). + * @throws JSchException if the specified algorithm is not recognized, the implementation class + * cannot be found, or an instance cannot be created. + */ + SignatureWrapper(String algorithm, Session session) throws JSchException { + try { + // Session.getConfig(algorithm) + this.signature = Class.forName(session.getConfig(algorithm)).asSubclass(Signature.class) + .getDeclaredConstructor().newInstance(); + } catch (Exception | LinkageError e) { + throw new JSchException("Failed to instantiate signature for algorithm '" + algorithm + "'", + e); + } + + if (signature instanceof SignatureRSA) { + pubKeyParameterValidator = (byte[][] args) -> generateValidator("RSA", 2); + publicKeySetter = + (byte[][] args) -> ((SignatureRSA) this.signature).setPubKey(args[0], args[1]); + } else if (signature instanceof SignatureDSA) { + pubKeyParameterValidator = (byte[][] args) -> generateValidator("DSA", 4); + publicKeySetter = (byte[][] args) -> ((SignatureDSA) this.signature).setPubKey(args[0], + args[1], args[2], args[3]); + } else if (signature instanceof SignatureECDSA) { + pubKeyParameterValidator = (byte[][] args) -> generateValidator("ECDSA", 2); + publicKeySetter = + (byte[][] args) -> ((SignatureECDSA) this.signature).setPubKey(args[0], args[1]); + } else if (signature instanceof SignatureEdDSA) { + pubKeyParameterValidator = (byte[][] args) -> generateValidator("EdDSA", 1); + publicKeySetter = (byte[][] args) -> ((SignatureEdDSA) this.signature).setPubKey(args[0]); + } else { + throw new JSchException("Unrecognized signature algorithm: " + algorithm); + } + } + + /** + * Generates a validator for the public key parameters. + * + * @param algorithm the algorithm name, used for error messages. + * @param expectedNumParams the exact number of byte arrays expected for the public key. + * @return a {@link PubKeyParameterValidator} instance. + * @throws JSchException if the number of provided parameters does not match the expected count. + */ + private static PubKeyParameterValidator generateValidator(String algorithm, int expectedNumParams) + throws JSchException { + return (byte[][] params) -> { + if (params.length != expectedNumParams) { + throw new JSchException("wrong number of arguments:" + algorithm + " signatures expects " + + expectedNumParams + " parameters, found " + params.length); + } + }; + } + + /** + * Initializes the underlying signature instance for signing or verification. This method + * delegates the call to the wrapped signature object. + * + * @throws Exception if an error occurs during initialization. + */ + @Override + public void init() throws Exception { + signature.init(); + } + + /** + * Updates the data to be signed or verified with the given byte array. This method delegates the + * call to the wrapped signature object. + * + * @param H the byte array to update the signature data with. + * @throws Exception if an error occurs during the update. + */ + @Override + public void update(byte[] H) throws Exception { + signature.update(H); + } + + /** + * Verifies the provided signature. This method delegates the call to the wrapped signature + * object. + * + * @param sig the signature bytes to be verified. + * @return {@code true} if the signature is valid, {@code false} otherwise. + * @throws Exception if an error occurs during verification. + */ + @Override + public boolean verify(byte[] sig) throws Exception { + return signature.verify(sig); + } + + /** + * Generates the digital signature of all the data updated so far. This method delegates the call + * to the wrapped signature object. + * + * @return the byte array representing the digital signature. + * @throws Exception if an error occurs during the signing process. + */ + @Override + public byte[] sign() throws Exception { + return signature.sign(); + } + + /** + * Sets the public key required for signature verification. + *+ * This method first validates that the correct number of key parameters are provided for the + * specific algorithm and then passes them to the underlying signature instance. + *
+ * + * @param args a variable number of byte arrays representing the public key components. + * @throws Exception if the key parameters are invalid or if an error occurs while setting the + * public key on the underlying signature instance. + */ + void setPubKey(byte[]... args) throws Exception { + pubKeyParameterValidator.validatePublicKeyParameter(args); + publicKeySetter.setPubKey(args); + } + + /** + * A functional interface for setting a public key on a {@link Signature} instance. + */ + @FunctionalInterface + private interface PubKeySetter { + + /** + * Sets the public key components on the signature instance. + * + * @param keyParams the components of the public key. + * @throws Exception if an error occurs during the operation. + */ + void setPubKey(byte[]... keyParams) throws Exception; + } + + /** + * A functional interface for validating the public key parameters. + */ + @FunctionalInterface + private interface PubKeyParameterValidator { + /** + * Validates the provided public key components. + * + * @param keyParams the components of the public key to validate. + * @throws Exception if the parameters are invalid. + */ + void validatePublicKeyParameter(byte[]... keyParams) throws Exception; + } + +} diff --git a/src/main/java/com/jcraft/jsch/UserAuthPublicKey.java b/src/main/java/com/jcraft/jsch/UserAuthPublicKey.java index 0b5f01b30..bff1835d8 100644 --- a/src/main/java/com/jcraft/jsch/UserAuthPublicKey.java +++ b/src/main/java/com/jcraft/jsch/UserAuthPublicKey.java @@ -74,7 +74,10 @@ public boolean start(Session session) throws Exception { for (String pkmethod : pkmethods) { boolean add = false; for (String server_sig_alg : server_sig_algs) { - if (pkmethod.equals(server_sig_alg)) { + // This cover the case of the public key is in Openssh certificate format. + String pkRawMethod = OpenSshCertificateUtil.getRawKeyType(pkmethod); + if (pkmethod.equals(server_sig_alg) + || (pkRawMethod != null && pkRawMethod.equals(server_sig_alg))) { add = true; break; } @@ -184,11 +187,8 @@ private boolean _start(Session session, List+ * For certificate key types, this checks the availability of the base algorithm. + *
+ * + * @param pkmethod the public key method/algorithm to check + * @param not_available_pks list of unavailable algorithms + * @param identity the identity being used + * @param session the current session (for logging) + * @return true if the algorithm is unavailable and should be skipped, false otherwise + */ + private boolean isAlgorithmUnavailable(String pkmethod, List+ * These tests leverage Testcontainers to spin up a dedicated SSH server in a Docker container, + * configured with specific host keys and certificates. The primary goal is to verify JSch's ability + * to validate server host keys signed by a Certificate Authority (CA) against a {@code known_hosts} + * file. + */ +@Testcontainers +public class HostCertificateIT { + + /** Connection timeout in milliseconds. */ + private static final int TIMEOUT = 5000; + /** Base resource folder for certificates and keys used in the tests. */ + private static final String CERTIFICATES_BASE_FOLDER = "certificates/host"; + /** Test logger for capturing JSch internal logs for debugging purposes. */ + private static final TestLogger jschLogger = TestLoggerFactory.getTestLogger(JSch.class); + /** Test logger for capturing SSH server logs (via a placeholder class). */ + private static final TestLogger sshdLogger = + TestLoggerFactory.getTestLogger(HostCertificateIT.class); + private static org.slf4j.Logger LOGGER = LoggerFactory.getLogger(HostCertificateIT.class); + + /* + * @BeforeAll static void beforeAll() { JSch.setLogger(LOGGER); } + * + * @AfterAll static void afterAll() { JSch.setLogger(null); } + */ + + /** + * Defines and configures the SSH server Docker container using Testcontainers. The container is + * built from a custom Dockerfile that: + *
+ * This test class verifies the correct behavior of the {@code OpenSshCertificateBuffer} class, + * which is responsible for parsing OpenSSH certificate data structures. The tests focus on the + * {@link OpenSshCertificateBuffer#getBytes()} method, ensuring proper handling of: + *
+ *+ * The format follows the SSH wire protocol: 4 bytes (big-endian length) followed by the data + * bytes. + *
+ * + * @param data the data bytes to prefix with length + * @return byte array containing the length prefix followed by the data + */ + private byte[] createLengthPrefixedData(byte[] data) { + byte[] result = new byte[4 + data.length]; + int len = data.length; + result[0] = (byte) ((len >> 24) & 0xff); + result[1] = (byte) ((len >> 16) & 0xff); + result[2] = (byte) ((len >> 8) & 0xff); + result[3] = (byte) (len & 0xff); + System.arraycopy(data, 0, result, 4, data.length); + return result; + } + + /** + * Helper method to create a buffer with a specific length prefix that may not match the actual + * data length. + * + *+ * This is useful for testing error conditions where the length prefix is intentionally incorrect. + *
+ * + * @param length the length value to encode in the prefix (may differ from actual data length) + * @param data the actual data bytes to include after the prefix + * @return byte array containing the specified length prefix followed by the data + */ + private byte[] createLengthPrefixedDataWithLength(int length, byte[] data) { + byte[] result = new byte[4 + data.length]; + result[0] = (byte) ((length >> 24) & 0xff); + result[1] = (byte) ((length >> 16) & 0xff); + result[2] = (byte) ((length >> 8) & 0xff); + result[3] = (byte) (length & 0xff); + System.arraycopy(data, 0, result, 4, data.length); + return result; + } + + /** + * Tests that {@code getBytes()} correctly reads a valid length-prefixed byte array. + * + *+ * Given a buffer containing properly formatted length-prefixed data, the method should return the + * exact data bytes without the length prefix. + *
+ */ + @Test + void getBytes_validData_returnsCorrectBytes() { + byte[] data = {0x01, 0x02, 0x03, 0x04, 0x05}; + byte[] bufferData = createLengthPrefixedData(data); + + OpenSshCertificateBuffer buffer = new OpenSshCertificateBuffer(bufferData); + byte[] result = buffer.getBytes(); + + assertArrayEquals(data, result); + } + + /** + * Tests that {@code getBytes()} correctly handles an empty data array. + * + *+ * When the length prefix is zero, the method should return an empty byte array without throwing + * any exceptions. + *
+ */ + @Test + void getBytes_emptyData_returnsEmptyArray() { + byte[] data = {}; + byte[] bufferData = createLengthPrefixedData(data); + + OpenSshCertificateBuffer buffer = new OpenSshCertificateBuffer(bufferData); + byte[] result = buffer.getBytes(); + + assertArrayEquals(data, result); + assertEquals(0, result.length); + } + + /** + * Tests that {@code getBytes()} throws an exception when the length prefix is negative. + * + *+ * A negative length value (e.g., 0xFFFFFFFF interpreted as -1) indicates malformed certificate + * data. The method should throw an {@link IllegalArgumentException} with a descriptive message + * rather than attempting to allocate a negative-sized array. + *
+ */ + @Test + void getBytes_negativeLength_throwsIllegalArgumentException() { + // Create buffer with negative length (-1 = 0xFFFFFFFF in unsigned, but interpreted as -1) + byte[] bufferData = {(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, 0x01, 0x02}; + + OpenSshCertificateBuffer buffer = new OpenSshCertificateBuffer(bufferData); + + IllegalArgumentException exception = + assertThrows(IllegalArgumentException.class, buffer::getBytes); + assertEquals("Invalid length in certificate data: negative length -1", exception.getMessage()); + } + + /** + * Tests that {@code getBytes()} throws an exception when the length prefix exceeds available + * data. + * + *+ * When the length prefix claims more bytes than are actually available in the buffer, the method + * should throw an {@link IllegalArgumentException} rather than reading beyond the buffer bounds + * or returning incomplete data. + *
+ */ + @Test + void getBytes_lengthExceedsAvailableData_throwsIllegalArgumentException() { + // Create buffer claiming 100 bytes but only having 5 + byte[] actualData = {0x01, 0x02, 0x03, 0x04, 0x05}; + byte[] bufferData = createLengthPrefixedDataWithLength(100, actualData); + + OpenSshCertificateBuffer buffer = new OpenSshCertificateBuffer(bufferData); + + IllegalArgumentException exception = + assertThrows(IllegalArgumentException.class, buffer::getBytes); + assertEquals("Invalid length in certificate data: requested 100 bytes but only 5 available", + exception.getMessage()); + } + + /** + * Tests that {@code getBytes()} succeeds when the length prefix exactly matches available data. + * + *+ * This is a boundary condition test to ensure that the method correctly handles the case where + * all remaining buffer data is consumed by a single read operation. + *
+ */ + @Test + void getBytes_lengthExactlyMatchesAvailableData_succeeds() { + byte[] data = {0x0A, 0x0B, 0x0C}; + byte[] bufferData = createLengthPrefixedData(data); + + OpenSshCertificateBuffer buffer = new OpenSshCertificateBuffer(bufferData); + byte[] result = buffer.getBytes(); + + assertArrayEquals(data, result); + } + + /** + * Tests that multiple sequential {@code getBytes()} calls work correctly. + * + *+ * OpenSSH certificates contain multiple length-prefixed fields. This test verifies that the + * buffer correctly advances its read position after each call, allowing sequential fields to be + * read independently. + *
+ */ + @Test + void getBytes_multipleReads_worksCorrectly() { + // Create buffer with two length-prefixed arrays + byte[] data1 = {0x01, 0x02}; + byte[] data2 = {0x03, 0x04, 0x05}; + byte[] prefixed1 = createLengthPrefixedData(data1); + byte[] prefixed2 = createLengthPrefixedData(data2); + + byte[] combined = new byte[prefixed1.length + prefixed2.length]; + System.arraycopy(prefixed1, 0, combined, 0, prefixed1.length); + System.arraycopy(prefixed2, 0, combined, prefixed1.length, prefixed2.length); + + OpenSshCertificateBuffer buffer = new OpenSshCertificateBuffer(combined); + + byte[] result1 = buffer.getBytes(); + byte[] result2 = buffer.getBytes(); + + assertArrayEquals(data1, result1); + assertArrayEquals(data2, result2); + } +} diff --git a/src/test/java/com/jcraft/jsch/OpenSshCertificateHostKeyVerifierTest.java b/src/test/java/com/jcraft/jsch/OpenSshCertificateHostKeyVerifierTest.java new file mode 100644 index 000000000..0325a9769 --- /dev/null +++ b/src/test/java/com/jcraft/jsch/OpenSshCertificateHostKeyVerifierTest.java @@ -0,0 +1,171 @@ +package com.jcraft.jsch; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for OpenSshCertificateHostKeyVerifier focusing on certificate validation edge cases. + */ +public class OpenSshCertificateHostKeyVerifierTest { + + // ==================== Tests for critical options rejection ==================== + + /** + * Test that a certificate with critical options is rejected. + */ + @Test + public void testCheckHostCertificate_withCriticalOptions_shouldReject() throws Exception { + Map+ * This test suite ensures that **JSch can successfully establish an SFTP connection** when + * configured with various types of certified user keys (e.g., RSA, ECDSA, Ed25519). The container + * is configured to trust the certificate authority (CA) key that signed the user certificates being + * tested. + */ +@Testcontainers +public class UserCertAuthIT { + /** + * Standard SLF4J logger for this test class. + */ + private static final Logger logger = LoggerFactory.getLogger(UserCertAuthIT.class); + /** + * Timeout value (in milliseconds) for session and channel connections. + */ + private static final int timeout = 5000; + /** + * Utility for generating SHA-256 digests. + */ + private static final DigestUtils sha256sum = new DigestUtils(DigestUtils.getSha256Digest()); + /** + * Test logger for capturing JSch internal logging output. + */ + private static final TestLogger jschLogger = TestLoggerFactory.getTestLogger(JSch.class); + /** + * Test logger for capturing the logging output of this test class (the SSHD setup). + */ + private static final TestLogger sshdLogger = + TestLoggerFactory.getTestLogger(UserCertAuthIT.class); + /** + * The Testcontainers instance for the SSHD server. + *
+ * The container is built from a Dockerfile and configured with: + *
+ * The keys represent various supported algorithms for user certificates. + * + * @return An {@code Iterable} of strings, each representing a private key file path prefix (e.g., + * "ecdsa_p256/root_ecdsa_sha2_nistp256_key"). + */ + public static Iterable extends String> privateKeyParams() { + return Arrays.asList( + // disable dss because dsa algorithm is deprecated and removed by openssh server + /* "dss/root_dsa_key", */ + "ecdsa_p256/root_ecdsa_sha2_nistp256_key", "ecdsa_p384/root_ecdsa-sha2-nistp384_key", + "ecdsa_p521/root_ecdsa_sha2_nistp521_key", "ed25519/root_ed25519_key", "rsa/root_rsa_key"); + } + + /** + * Tests JSch's ability to perform public key authentication using OpenSSH user certificates for + * various key types. + *
+ * The test adds the private key and its corresponding certificate (`-cert.pub`) to JSch and + * attempts an SFTP connection to the Testcontainers-managed SSHD. + * + * @param privateKey The path prefix to the private key and certificate files (from the test + * resource directory). + * @throws Exception if any error occurs during key reading, session setup, or connection. + */ + @MethodSource("privateKeyParams") + @ParameterizedTest(name = "key: {0}, cert: {0}-cert.pub") + public void opensshCertificateParserTest(String privateKey) throws Exception { + HostKey hostKey = readHostKey( + ResourceUtil.getResourceFile(this.getClass(), "certificates/docker/ssh_host_rsa_key.pub")); + JSch ssh = new JSch(); + ssh.addIdentity(ResourceUtil.getResourceFile(this.getClass(), "certificates/" + privateKey), + ResourceUtil.getResourceFile(this.getClass(), "certificates/" + privateKey + "-cert.pub"), + null); + ssh.getHostKeyRepository().add(hostKey, null); + + Session session = ssh.getSession("root", sshd.getHost(), sshd.getFirstMappedPort()); + session.setConfig("enable_auth_none", "yes"); + session.setConfig("StrictHostKeyChecking", "no"); + session.setConfig("PreferredAuthentications", "publickey"); + // Include ssh-rsa-cert-v01@openssh.com for RSA certificate test (not in defaults per OpenSSH) + session.setConfig("PubkeyAcceptedAlgorithms", + "ssh-ed25519-cert-v01@openssh.com,ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,ssh-rsa-cert-v01@openssh.com,ssh-ed25519,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,rsa-sha2-512,rsa-sha2-256"); + session.setConfig("server_host_key", + "ssh-ed25519,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,rsa-sha2-512,rsa-sha2-256"); + doSftp(session); + } + + /** + * Reads a public key file and constructs a {@link HostKey} object. + *
+ * This method simulates how a known host entry might be created, but uses hardcoded placeholder
+ * values for hostname and port in the constructed {@code HostKey}.
+ *
+ * @param fileName The absolute path to the public key file (e.g., {@code ssh_host_rsa_key.pub}).
+ * @return A {@link HostKey} instance representing the key.
+ * @throws Exception if the file cannot be read or the key content is malformed.
+ */
+ private HostKey readHostKey(String fileName) throws Exception {
+ List
+ * This method wraps the connection and SFTP channel creation in an {@code assertDoesNotThrow} to
+ * ensure the entire process, including authentication, completes successfully.
+ *
+ * @param session The configured JSch session to connect.
+ * @throws Exception if connection or SFTP channel setup fails.
+ */
+ private void doSftp(Session session) throws Exception {
+ Assertions.assertDoesNotThrow(() -> {
+ try {
+ session.setTimeout(timeout);
+ session.connect();
+ ChannelSftp sftp = (ChannelSftp) session.openChannel("sftp");
+ sftp.connect(timeout);
+ Assertions.assertTrue(sftp.isConnected());
+ sftp.disconnect();
+ session.disconnect();
+ } catch (Exception e) {
+ printInfo();
+ throw e;
+ }
+ });
+ }
+
+ /**
+ * Prints all captured logging events from the JSch and SSHD test loggers to the standard output
+ * for debugging purposes, primarily on test failure.
+ */
+ private void printInfo() {
+ jschLogger.getAllLoggingEvents().stream().map(LoggingEvent::getFormattedMessage)
+ .forEach(System.out::println);
+ sshdLogger.getAllLoggingEvents().stream().map(LoggingEvent::getFormattedMessage)
+ .forEach(System.out::println);
+ System.out.println("");
+ System.out.println("");
+ System.out.println("");
+ }
+}
diff --git a/src/test/resources/certificates/ca/ca_jsch_key b/src/test/resources/certificates/ca/ca_jsch_key
new file mode 100644
index 000000000..ff7b96075
--- /dev/null
+++ b/src/test/resources/certificates/ca/ca_jsch_key
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACCk+kZHaz0pZsY+GYszU0oEitHruzVPS4Eu4SUgXD7BcAAAAJi+25zGvtuc
+xgAAAAtzc2gtZWQyNTUxOQAAACCk+kZHaz0pZsY+GYszU0oEitHruzVPS4Eu4SUgXD7BcA
+AAAECbHrZvBtnSJkxCPQWTjFd7CiXOeZZEGYJKxXaulGqWEaT6RkdrPSlmxj4ZizNTSgSK
+0eu7NU9LgS7hJSBcPsFwAAAAEWxkZW1hc2lAYnVtYmxlYmVlAQIDBA==
+-----END OPENSSH PRIVATE KEY-----
diff --git a/src/test/resources/certificates/ca/ca_jsch_key.pub b/src/test/resources/certificates/ca/ca_jsch_key.pub
new file mode 100644
index 000000000..d19506242
--- /dev/null
+++ b/src/test/resources/certificates/ca/ca_jsch_key.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKT6RkdrPSlmxj4ZizNTSgSK0eu7NU9LgS7hJSBcPsFw ca@jsch
diff --git a/src/test/resources/certificates/docker/Dockerfile b/src/test/resources/certificates/docker/Dockerfile
new file mode 100644
index 000000000..d0b521569
--- /dev/null
+++ b/src/test/resources/certificates/docker/Dockerfile
@@ -0,0 +1,17 @@
+FROM alpine:3.13
+RUN apk update && \
+ apk upgrade && \
+ apk add openssh openssh-server bash && \
+ rm /var/cache/apk/* && \
+ mkdir /root/.ssh && \
+ chmod 700 /root/.ssh
+COPY ssh_host_rsa_key /etc/ssh/
+#COPY ssh_host_rsa_key.pub /etc/ssh/
+COPY ssh_host_rsa_key-cert.pub /etc/ssh/ssh_host_rsa_key-cert.pub
+COPY sshd_config /etc/ssh/
+COPY ca_jsch_key.pub /etc/ssh/ca_jsch_key.pub
+
+RUN chmod 600 /etc/ssh/ssh_*_key /root/.ssh -R
+RUN passwd -u root
+EXPOSE 22
+ENTRYPOINT ["/usr/sbin/sshd", "-D", "-e"]
diff --git a/src/test/resources/certificates/docker/ssh_host_rsa_key b/src/test/resources/certificates/docker/ssh_host_rsa_key
new file mode 100644
index 000000000..e6ccd0659
--- /dev/null
+++ b/src/test/resources/certificates/docker/ssh_host_rsa_key
@@ -0,0 +1,49 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+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
+ToOlCkHaEsfZAAAACGhvc3Rfa2V5AQI=
+-----END OPENSSH PRIVATE KEY-----
diff --git a/src/test/resources/certificates/docker/ssh_host_rsa_key-cert.pub b/src/test/resources/certificates/docker/ssh_host_rsa_key-cert.pub
new file mode 100644
index 000000000..c499cc32f
--- /dev/null
+++ b/src/test/resources/certificates/docker/ssh_host_rsa_key-cert.pub
@@ -0,0 +1 @@
+ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgLCtFMxhu2dw89lLLFt5Z2f5ofwxkhWmyqyQDH/LUYVYAAAADAQABAAACAQDHfkXT32RdF58AXB17NQGqzPaVhEJ1STXduFM6sBkWDSyZ2yhsl8/h7VaqKK6XNwzbg9DBua47YLhk39aTN3hqGGjZd7mIt8HfID1sR483Lyp2ooPor+du8OfCAaeeTAy3r1US1QmTYSx6rCw1kqK63G1BLjmMyCdNk7IihUQhP/6Uw/Vq69ol0zeMUE7LwpqlWnIuF2PkF6fe/sPpcYK6l+qBqIxbfvKLmNQ3OUzJkAHQ5LQmcv1FjgXb1nUAe4JdDObyzNKJriWMZVdBJMP3A4eteTxVJjDBAIZLtPjGcD0zpDtGLxlLJcn+8SlozKyGehI8LotQ8EnXj5KVyeqicNroVf/1/s6A/2V5V5mES0VMc3lkM/UhFqrI7CTi++WFQ/OQWlBkCPd9DQe+0LOcv9RTCjlElL23cdlEfuCNghx1lPmIy6CrMjcT7ByA3yaQIyNOfEF7ovs3VG+lDlcCvZ8HJCt9bTQXb/wuoRhCA/w/sQgrwVvJ33u0m2EPCWnZNTuqOmdUq7/YXZobNu2R3aosGIlcLM5wgDjdes6LlAf5Ufz0KaS0eTs4EUil2VXpgxt0tS8GngRRmwfe0cyZeRR/ijv9GRcUPXkPmP+zyJSE2Pc0GTd/mNUDxvmxeT/wb82iny7QL4UVw0bkUutuHKV2bgvoVH5rS77JE2qxPQAAAAAAAAAAAAAAAgAAAAhob3N0X2tleQAAAAsAAAAHb3BlbnNzaAAAAAAAAAAA//////////8AAAAAAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIKT6RkdrPSlmxj4ZizNTSgSK0eu7NU9LgS7hJSBcPsFwAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEB+dLqA7IVOR8tBVJHXckpuZHObKq56/q9Ug2KVzQTVRPE2mUwBuEuE1fRzt2Dk9GyuCsJqiF/9N6AQPllbXQ4I host_key
diff --git a/src/test/resources/certificates/docker/ssh_host_rsa_key.pub b/src/test/resources/certificates/docker/ssh_host_rsa_key.pub
new file mode 100644
index 000000000..d251c829d
--- /dev/null
+++ b/src/test/resources/certificates/docker/ssh_host_rsa_key.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDHfkXT32RdF58AXB17NQGqzPaVhEJ1STXduFM6sBkWDSyZ2yhsl8/h7VaqKK6XNwzbg9DBua47YLhk39aTN3hqGGjZd7mIt8HfID1sR483Lyp2ooPor+du8OfCAaeeTAy3r1US1QmTYSx6rCw1kqK63G1BLjmMyCdNk7IihUQhP/6Uw/Vq69ol0zeMUE7LwpqlWnIuF2PkF6fe/sPpcYK6l+qBqIxbfvKLmNQ3OUzJkAHQ5LQmcv1FjgXb1nUAe4JdDObyzNKJriWMZVdBJMP3A4eteTxVJjDBAIZLtPjGcD0zpDtGLxlLJcn+8SlozKyGehI8LotQ8EnXj5KVyeqicNroVf/1/s6A/2V5V5mES0VMc3lkM/UhFqrI7CTi++WFQ/OQWlBkCPd9DQe+0LOcv9RTCjlElL23cdlEfuCNghx1lPmIy6CrMjcT7ByA3yaQIyNOfEF7ovs3VG+lDlcCvZ8HJCt9bTQXb/wuoRhCA/w/sQgrwVvJ33u0m2EPCWnZNTuqOmdUq7/YXZobNu2R3aosGIlcLM5wgDjdes6LlAf5Ufz0KaS0eTs4EUil2VXpgxt0tS8GngRRmwfe0cyZeRR/ijv9GRcUPXkPmP+zyJSE2Pc0GTd/mNUDxvmxeT/wb82iny7QL4UVw0bkUutuHKV2bgvoVH5rS77JE2qxPQ== host_key
diff --git a/src/test/resources/certificates/docker/sshd_config b/src/test/resources/certificates/docker/sshd_config
new file mode 100644
index 000000000..54111fbb7
--- /dev/null
+++ b/src/test/resources/certificates/docker/sshd_config
@@ -0,0 +1,12 @@
+PubkeyAuthentication yes
+
+AuthenticationMethods publickey
+
+# PubkeyAcceptedKeyTypes ssh-ed25519, ssh-ed25519-cert-v01@openssh.com, ecdsa-sha2-nistp256,ecdsa-sha2-nistp256-cert-v01@openssh.com, rsa-sha2-512,rsa-sha2-512-cert-v01@openssh.com, rsa-sha2-256, rsa-sha2-256-cert-v01@openssh.com,ssh-rsa, ssh-rsa-cert-v01@openssh.com
+PrintMotd no
+PermitRootLogin yes
+Subsystem sftp internal-sftp
+HostKey /etc/ssh/ssh_host_rsa_key
+HostCertificate /etc/ssh/ssh_host_rsa_key-cert.pub
+TrustedUserCAKeys /etc/ssh/ca_jsch_key.pub
+LogLevel DEBUG3
\ No newline at end of file
diff --git a/src/test/resources/certificates/dss/root_dsa_key b/src/test/resources/certificates/dss/root_dsa_key
new file mode 100644
index 000000000..bf030ea78
--- /dev/null
+++ b/src/test/resources/certificates/dss/root_dsa_key
@@ -0,0 +1,12 @@
+-----BEGIN DSA PRIVATE KEY-----
+MIIBuwIBAAKBgQCruwuluNg4VTvBTYg+zAiqJE03oMmfJcqSCvKIauJBAV0kf8dV
+EPvXIpAuQzIYJ/mQH5FZb4c9yROTTMrnsLymQzPvOInDSrpXauN/ED2W8wssGGGM
+exAG5pliTbPMjLsQoC6/53Qy9CD13cWdGnlSSCHdSWGB6usuDg6ULSVoewIVAOsB
+62PJcOXKehKCrnN0q3OiKZnZAoGAUnX5TigTfvRUn/COlhoRjopjc31hj/Mu3ads
+NL9AdIsWff31L3ELOOXgPW03GHD1HY7sIQsUfG96QCt6WOH2sPnBmi7Zz3ajb9ss
+UClYEpcBz+vBdT1VVFepHPtDA6t3cOCzX3Q80FOGBLrO1JcI9BVRMTz5qX9YYq+C
+LgtbDwkCgYAxZ9mmj2CmkX5f2bHzyfJTkto2G8wpf/pc4cHNMOdSrcaHP9ZTZzeG
+tDKjoPSddqkWajgyAebZ6xGz9Q2PlQLLSC+EYj2oTigsw1jt51BeGDXpr6v9JLDP
+ehqmA6erImoeeT2dmSRuEl+5A22fDhYvY0vzxXK3Lj6liC0q9uMQbQIVAKvGCKw4
+LA85L+loGn40UdUmfrSE
+-----END DSA PRIVATE KEY-----
diff --git a/src/test/resources/certificates/dss/root_dsa_key-cert.pub b/src/test/resources/certificates/dss/root_dsa_key-cert.pub
new file mode 100644
index 000000000..56e03f30d
--- /dev/null
+++ b/src/test/resources/certificates/dss/root_dsa_key-cert.pub
@@ -0,0 +1 @@
+ssh-dss-cert-v01@openssh.com AAAAHHNzaC1kc3MtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgIIdP/OvhQzSPW55o4R2ZFeu+TsYn/aJKkzT/g273BrYAAACBAKu7C6W42DhVO8FNiD7MCKokTTegyZ8lypIK8ohq4kEBXSR/x1UQ+9cikC5DMhgn+ZAfkVlvhz3JE5NMyuewvKZDM+84icNKuldq438QPZbzCywYYYx7EAbmmWJNs8yMuxCgLr/ndDL0IPXdxZ0aeVJIId1JYYHq6y4ODpQtJWh7AAAAFQDrAetjyXDlynoSgq5zdKtzoimZ2QAAAIBSdflOKBN+9FSf8I6WGhGOimNzfWGP8y7dp2w0v0B0ixZ9/fUvcQs45eA9bTcYcPUdjuwhCxR8b3pAK3pY4faw+cGaLtnPdqNv2yxQKVgSlwHP68F1PVVUV6kc+0MDq3dw4LNfdDzQU4YEus7Ulwj0FVExPPmpf1hir4IuC1sPCQAAAIAxZ9mmj2CmkX5f2bHzyfJTkto2G8wpf/pc4cHNMOdSrcaHP9ZTZzeGtDKjoPSddqkWajgyAebZ6xGz9Q2PlQLLSC+EYj2oTigsw1jt51BeGDXpr6v9JLDPehqmA6erImoeeT2dmSRuEl+5A22fDhYvY0vzxXK3Lj6liC0q9uMQbQAAAAAAAAAAAAAAAQAAABVyb290X2RzYV9rZXktY2VydC5wdWIAAAAIAAAABHJvb3QAAAAAAAAAAP//////////AAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAgpPpGR2s9KWbGPhmLM1NKBIrR67s1T0uBLuElIFw+wXAAAABTAAAAC3NzaC1lZDI1NTE5AAAAQIRMKra4a32AEK0tESXcDcaGguIUZiHaKrGcjqnQZKc4VgfrYubAdBN9OSPsxorc8Rwse9SfBDb7Iyz+ZuFFxAg= root@jsch
diff --git a/src/test/resources/certificates/dss/root_dsa_key.pub b/src/test/resources/certificates/dss/root_dsa_key.pub
new file mode 100644
index 000000000..724535c55
--- /dev/null
+++ b/src/test/resources/certificates/dss/root_dsa_key.pub
@@ -0,0 +1 @@
+ssh-dss AAAAB3NzaC1kc3MAAACBAKu7C6W42DhVO8FNiD7MCKokTTegyZ8lypIK8ohq4kEBXSR/x1UQ+9cikC5DMhgn+ZAfkVlvhz3JE5NMyuewvKZDM+84icNKuldq438QPZbzCywYYYx7EAbmmWJNs8yMuxCgLr/ndDL0IPXdxZ0aeVJIId1JYYHq6y4ODpQtJWh7AAAAFQDrAetjyXDlynoSgq5zdKtzoimZ2QAAAIBSdflOKBN+9FSf8I6WGhGOimNzfWGP8y7dp2w0v0B0ixZ9/fUvcQs45eA9bTcYcPUdjuwhCxR8b3pAK3pY4faw+cGaLtnPdqNv2yxQKVgSlwHP68F1PVVUV6kc+0MDq3dw4LNfdDzQU4YEus7Ulwj0FVExPPmpf1hir4IuC1sPCQAAAIAxZ9mmj2CmkX5f2bHzyfJTkto2G8wpf/pc4cHNMOdSrcaHP9ZTZzeGtDKjoPSddqkWajgyAebZ6xGz9Q2PlQLLSC+EYj2oTigsw1jt51BeGDXpr6v9JLDPehqmA6erImoeeT2dmSRuEl+5A22fDhYvY0vzxXK3Lj6liC0q9uMQbQ== root@jsch
diff --git a/src/test/resources/certificates/ecdsa_p256/root_ecdsa_sha2_nistp256_key b/src/test/resources/certificates/ecdsa_p256/root_ecdsa_sha2_nistp256_key
new file mode 100644
index 000000000..a7e5c934b
--- /dev/null
+++ b/src/test/resources/certificates/ecdsa_p256/root_ecdsa_sha2_nistp256_key
@@ -0,0 +1,9 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
+1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQRkaq5BaJvpM5EaSzVMwljb2Iw5zokN
+ReyFKcVgrtInxYmE5eldwgEAi7ufNzCpVFbBn9+cHHwyHlPpD4Pvl8J3AAAAqNFdU7HRXV
+OxAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGRqrkFom+kzkRpL
+NUzCWNvYjDnOiQ1F7IUpxWCu0ifFiYTl6V3CAQCLu583MKlUVsGf35wcfDIeU+kPg++Xwn
+cAAAAgIGS67O/aW+THSnKSM57umRNDHLH0j97t0S2/wRVJiBkAAAAJcm9vdEBqc2NoAQID
+BAUGBw==
+-----END OPENSSH PRIVATE KEY-----
diff --git a/src/test/resources/certificates/ecdsa_p256/root_ecdsa_sha2_nistp256_key-cert.pub b/src/test/resources/certificates/ecdsa_p256/root_ecdsa_sha2_nistp256_key-cert.pub
new file mode 100644
index 000000000..a0c7c5287
--- /dev/null
+++ b/src/test/resources/certificates/ecdsa_p256/root_ecdsa_sha2_nistp256_key-cert.pub
@@ -0,0 +1 @@
+ecdsa-sha2-nistp256-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgRKvdnrNUwLYg6XXTN7ZjjPO+peDU5tisQvPBCH3H8FQAAAAIbmlzdHAyNTYAAABBBGRqrkFom+kzkRpLNUzCWNvYjDnOiQ1F7IUpxWCu0ifFiYTl6V3CAQCLu583MKlUVsGf35wcfDIeU+kPg++XwncAAAAAAAAAAAAAAAEAAAAdcm9vdF9lY2RzYV9zaGEyX25pc3RwMjU2X2NlcnQAAAAIAAAABHJvb3QAAAAAAAAAAP//////////AAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAgpPpGR2s9KWbGPhmLM1NKBIrR67s1T0uBLuElIFw+wXAAAABTAAAAC3NzaC1lZDI1NTE5AAAAQNWKoY98XF3JQsS4imfv/pzFNaU7mxbC4PbsAwl5myz8axaJLBIfqP4fFB0tpuB9u99PGrFqe+JUGw02kWXXlwE= root@jsch
diff --git a/src/test/resources/certificates/ecdsa_p256/root_ecdsa_sha2_nistp256_key.pub b/src/test/resources/certificates/ecdsa_p256/root_ecdsa_sha2_nistp256_key.pub
new file mode 100644
index 000000000..ee7acc472
--- /dev/null
+++ b/src/test/resources/certificates/ecdsa_p256/root_ecdsa_sha2_nistp256_key.pub
@@ -0,0 +1 @@
+ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGRqrkFom+kzkRpLNUzCWNvYjDnOiQ1F7IUpxWCu0ifFiYTl6V3CAQCLu583MKlUVsGf35wcfDIeU+kPg++Xwnc= root@jsch
diff --git a/src/test/resources/certificates/ecdsa_p384/root_ecdsa-sha2-nistp384_key b/src/test/resources/certificates/ecdsa_p384/root_ecdsa-sha2-nistp384_key
new file mode 100644
index 000000000..779f8dfd2
--- /dev/null
+++ b/src/test/resources/certificates/ecdsa_p384/root_ecdsa-sha2-nistp384_key
@@ -0,0 +1,10 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS
+1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQRPJ3mj4Vp+co/OuPhMpXWrG/FcqRxc
+3c5Wc+n+X9jTgVk4S3okgl3DBib43ZRnABAD2ZMylrQQnTYcxiT+wXeRZQCS61z/NP8XHu
+sE+i0GE1zeBFdD5akg3O5XQtvHzQwAAADY0Or4IdDq+CEAAAATZWNkc2Etc2hhMi1uaXN0
+cDM4NAAAAAhuaXN0cDM4NAAAAGEETyd5o+FafnKPzrj4TKV1qxvxXKkcXN3OVnPp/l/Y04
+FZOEt6JIJdwwYm+N2UZwAQA9mTMpa0EJ02HMYk/sF3kWUAkutc/zT/Fx7rBPotBhNc3gRX
+Q+WpINzuV0Lbx80MAAAAMQCwU7ixUtk0z4smYo44MySZrfTJuOC0fnWrO8NZJayjBLUW0x
+ELNDuwbaKRhSP+odAAAAAJcm9vdEBqc2NoAQIDBAUG
+-----END OPENSSH PRIVATE KEY-----
diff --git a/src/test/resources/certificates/ecdsa_p384/root_ecdsa-sha2-nistp384_key-cert.pub b/src/test/resources/certificates/ecdsa_p384/root_ecdsa-sha2-nistp384_key-cert.pub
new file mode 100644
index 000000000..90e8bd313
--- /dev/null
+++ b/src/test/resources/certificates/ecdsa_p384/root_ecdsa-sha2-nistp384_key-cert.pub
@@ -0,0 +1 @@
+ecdsa-sha2-nistp384-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHAzODQtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgOjGNjBgPpGzVUvbu3dcSzPedDvOFIZz5uq58x4IIvA4AAAAIbmlzdHAzODQAAABhBE8neaPhWn5yj864+Eyldasb8VypHFzdzlZz6f5f2NOBWThLeiSCXcMGJvjdlGcAEAPZkzKWtBCdNhzGJP7Bd5FlAJLrXP80/xce6wT6LQYTXN4EV0PlqSDc7ldC28fNDAAAAAAAAAAAAAAAAQAAAB1yb290X2VjZHNhX3NoYTJfbmlzdHAzODRfY2VydAAAAAgAAAAEcm9vdAAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACCk+kZHaz0pZsY+GYszU0oEitHruzVPS4Eu4SUgXD7BcAAAAFMAAAALc3NoLWVkMjU1MTkAAABAGzsuog2FwQnJwi7d0MfCZFkINAjubRDR7D88GQ14uGR8+uQZhz0ijVYXVEbGXQxPulbZNHE88VTHOjqzCp8LCw== root@jsch
diff --git a/src/test/resources/certificates/ecdsa_p384/root_ecdsa-sha2_nistp384_key.pub b/src/test/resources/certificates/ecdsa_p384/root_ecdsa-sha2_nistp384_key.pub
new file mode 100644
index 000000000..598ce2e85
--- /dev/null
+++ b/src/test/resources/certificates/ecdsa_p384/root_ecdsa-sha2_nistp384_key.pub
@@ -0,0 +1 @@
+ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBE8neaPhWn5yj864+Eyldasb8VypHFzdzlZz6f5f2NOBWThLeiSCXcMGJvjdlGcAEAPZkzKWtBCdNhzGJP7Bd5FlAJLrXP80/xce6wT6LQYTXN4EV0PlqSDc7ldC28fNDA== root@jsch
diff --git a/src/test/resources/certificates/ecdsa_p521/root_ecdsa_sha2_nistp521_key b/src/test/resources/certificates/ecdsa_p521/root_ecdsa_sha2_nistp521_key
new file mode 100644
index 000000000..c40d38f20
--- /dev/null
+++ b/src/test/resources/certificates/ecdsa_p521/root_ecdsa_sha2_nistp521_key
@@ -0,0 +1,12 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAArAAAABNlY2RzYS
+1zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQAGyUjNEMH3/FviSGY0ll7Je2St1o+
+GNssqOUdnHIERjGNS8sudFSRsTkBwYwO8cFcfan3HztLydIV8CE/oTuuJD4ASTAKx4hT+N
+kkonjAZ4munfyybshUUhmrzgCNThfG7oun44b6UzbeEqn/0zSrr857vyP8/11EqlsfAFA4
+oSrU0kQAAAEIPa26xT2tusUAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQ
+AAAIUEABslIzRDB9/xb4khmNJZeyXtkrdaPhjbLKjlHZxyBEYxjUvLLnRUkbE5AcGMDvHB
+XH2p9x87S8nSFfAhP6E7riQ+AEkwCseIU/jZJKJ4wGeJrp38sm7IVFIZq84AjU4Xxu6Lp+
+OG+lM23hKp/9M0q6/Oe78j/P9dRKpbHwBQOKEq1NJEAAAAQgHfR86PaqYxkl28+uAukdu6
+tE6dTgap5kMrkkjydRze8Pbb3ARCUdXInIATFTV8vKSOokH72JPdhqOu2idGB3jtwwAAAA
+lyb290QGpzY2gB
+-----END OPENSSH PRIVATE KEY-----
diff --git a/src/test/resources/certificates/ecdsa_p521/root_ecdsa_sha2_nistp521_key-cert.pub b/src/test/resources/certificates/ecdsa_p521/root_ecdsa_sha2_nistp521_key-cert.pub
new file mode 100644
index 000000000..4cbaf7ca5
--- /dev/null
+++ b/src/test/resources/certificates/ecdsa_p521/root_ecdsa_sha2_nistp521_key-cert.pub
@@ -0,0 +1 @@
+ecdsa-sha2-nistp521-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHA1MjEtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgnCOPG+mG47nRTN6XgXwlTFkA274OJzZMK16Bocbj6hUAAAAIbmlzdHA1MjEAAACFBAAbJSM0Qwff8W+JIZjSWXsl7ZK3Wj4Y2yyo5R2ccgRGMY1Lyy50VJGxOQHBjA7xwVx9qfcfO0vJ0hXwIT+hO64kPgBJMArHiFP42SSieMBnia6d/LJuyFRSGavOAI1OF8bui6fjhvpTNt4Sqf/TNKuvznu/I/z/XUSqWx8AUDihKtTSRAAAAAAAAAAAAAAAAQAAAB1yb290X2VjZHNhX3NoYTJfbmlzdHA1MjFfY2VydAAAAAgAAAAEcm9vdAAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACCk+kZHaz0pZsY+GYszU0oEitHruzVPS4Eu4SUgXD7BcAAAAFMAAAALc3NoLWVkMjU1MTkAAABACTmzwTLZIrb/+WAX97LzgkkVtmUGqGBSd5cAfdTSewW0I7F0nnrfiWxHKxAGMMu8gCuei//fxU/j0CvZ5zOhCA== root@jsch
diff --git a/src/test/resources/certificates/ecdsa_p521/root_ecdsa_sha2_nistp521_key.pub b/src/test/resources/certificates/ecdsa_p521/root_ecdsa_sha2_nistp521_key.pub
new file mode 100644
index 000000000..28a7b6bb9
--- /dev/null
+++ b/src/test/resources/certificates/ecdsa_p521/root_ecdsa_sha2_nistp521_key.pub
@@ -0,0 +1 @@
+ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAAbJSM0Qwff8W+JIZjSWXsl7ZK3Wj4Y2yyo5R2ccgRGMY1Lyy50VJGxOQHBjA7xwVx9qfcfO0vJ0hXwIT+hO64kPgBJMArHiFP42SSieMBnia6d/LJuyFRSGavOAI1OF8bui6fjhvpTNt4Sqf/TNKuvznu/I/z/XUSqWx8AUDihKtTSRA== root@jsch
diff --git a/src/test/resources/certificates/ed25519/root_ed25519_key b/src/test/resources/certificates/ed25519/root_ed25519_key
new file mode 100644
index 000000000..db5c1b509
--- /dev/null
+++ b/src/test/resources/certificates/ed25519/root_ed25519_key
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACABYJVVa/zEZPnSZIMhe1V94rAnkMefiP3nprDl2H7t0AAAAJALg5pGC4Oa
+RgAAAAtzc2gtZWQyNTUxOQAAACABYJVVa/zEZPnSZIMhe1V94rAnkMefiP3nprDl2H7t0A
+AAAECV6JukBCf/UE+fYisTqvFTeZjP0W7/bksYPFLzdy6lBwFglVVr/MRk+dJkgyF7VX3i
+sCeQx5+I/eemsOXYfu3QAAAACXJvb3RAanNjaAECAwQ=
+-----END OPENSSH PRIVATE KEY-----
diff --git a/src/test/resources/certificates/ed25519/root_ed25519_key-cert.pub b/src/test/resources/certificates/ed25519/root_ed25519_key-cert.pub
new file mode 100644
index 000000000..45016e2c7
--- /dev/null
+++ b/src/test/resources/certificates/ed25519/root_ed25519_key-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIL9+pDLW84sRnn60gmIDrLh85yfgepZa8ONLfseWaKiIAAAAIAFglVVr/MRk+dJkgyF7VX3isCeQx5+I/eemsOXYfu3QAAAAAAAAAAAAAAABAAAAEXJvb3RfZWQyNTUxOV9jZXJ0AAAACAAAAARyb290AAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIKT6RkdrPSlmxj4ZizNTSgSK0eu7NU9LgS7hJSBcPsFwAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEB5w7ox8RcBxmH49p81T7bHWuQ8sbTa0Gr+XMerAjewDWtIFUuUc0iu4dBXuHfOI3+kxCN3RAKI/Ncli1sfd1gO root@jsch
diff --git a/src/test/resources/certificates/ed25519/root_ed25519_key.pub b/src/test/resources/certificates/ed25519/root_ed25519_key.pub
new file mode 100644
index 000000000..6892aaf71
--- /dev/null
+++ b/src/test/resources/certificates/ed25519/root_ed25519_key.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAFglVVr/MRk+dJkgyF7VX3isCeQx5+I/eemsOXYfu3Q root@jsch
diff --git a/src/test/resources/certificates/host/Dockerfile b/src/test/resources/certificates/host/Dockerfile
new file mode 100644
index 000000000..18be3fd72
--- /dev/null
+++ b/src/test/resources/certificates/host/Dockerfile
@@ -0,0 +1,13 @@
+FROM alpine:3.16
+RUN apk add --update openssh openssh-server bash && rm /var/cache/apk/*
+RUN mkdir -p /root/.ssh
+COPY ["sshd_config","/etc/ssh/sshd_config"]
+COPY ["authorized_keys","/root/.ssh/authorized_keys"]
+COPY ["entrypoint.sh","/entrypoint.sh"]
+COPY ["ssh_host_rsa_key","/etc/ssh/ssh_host_rsa_key"]
+COPY ["ssh_host_rsa_key-cert.pub","/etc/ssh/ssh_host_rsa_key-cert.pub"]
+COPY ["ssh_host_ecdsa_key","/etc/ssh/ssh_host_ecdsa_key"]
+COPY ["ssh_host_ecdsa_key-cert.pub","/etc/ssh/ssh_host_ecdsa_key-cert.pub"]
+COPY ["ssh_host_ed25519_key","/etc/ssh/ssh_host_ed25519_key"]
+COPY ["ssh_host_ed25519_key-cert.pub","/etc/ssh/ssh_host_ed25519_key-cert.pub"]
+ENTRYPOINT /entrypoint.sh
\ No newline at end of file
diff --git a/src/test/resources/certificates/host/entrypoint.sh b/src/test/resources/certificates/host/entrypoint.sh
new file mode 100755
index 000000000..6c9047734
--- /dev/null
+++ b/src/test/resources/certificates/host/entrypoint.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+chown -R luigi /home/luigi
+chmod 0600 /home/luigi/.ssh/*
+chmod 600 /etc/ssh/ssh_*_key -R
+/usr/sbin/sshd -D -ddd -e
diff --git a/src/test/resources/certificates/host/ssh_host_dsa_key b/src/test/resources/certificates/host/ssh_host_dsa_key
new file mode 100644
index 000000000..10200152b
--- /dev/null
+++ b/src/test/resources/certificates/host/ssh_host_dsa_key
@@ -0,0 +1,21 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABsQAAAAdzc2gtZH
+NzAAAAgQDCNtFJIHt4LMoBQTFOzpdEed4A358dCpM1Q35g6lUZnn8UBE1DndopqUOiTr8Y
+iM8rqfG11rHYEWN4b3vZ1ogogtwpTT4WQ+Ac0CSKsAuFgg8R6bfXk1oc/Ja9YMPt4tVL7i
+GkMoTG1WJdBsuiOzcANZD2whd/luTpEwwfpM5HaQAAABUAr2+0r9JzvK2MXhWqbaGwCqjT
+GIcAAACAPXS+XP2AYd6q3TfPz4VVCgE78IK2uZPn6jNN9B70Pr6z/h2Qm0BZ9UEtLXOvhl
+Ek+gxpkvBuWi6Cdnhm5ROrM7EQNYw6Nv1xNBJr1o3oLITyoJp/TaecLc4/kAlqOX4teHmY
+z5g9x8THu0gN6ImNandZIHeC1qfChK7iPlpHG5MAAACAbApTLq4yn89qacgTlfG1OGOkzf
+xiQ5Fu23vZq9+PJ9SyZ/WkU1wWp+vlxrnSTdYLMFs+3E03CT+TwrnvOhRjL7dMJqMf/3Oh
+0p02jVzGQ8Xf0ob7Auz+TY4sUP6epSnCiv/12fGFQpuI6cBBlnBjLt/z6EbH6TM1ELxlJ9
+ZfFtUAAAHox+BxpcfgcaUAAAAHc3NoLWRzcwAAAIEAwjbRSSB7eCzKAUExTs6XRHneAN+f
+HQqTNUN+YOpVGZ5/FARNQ53aKalDok6/GIjPK6nxtdax2BFjeG972daIKILcKU0+FkPgHN
+AkirALhYIPEem315NaHPyWvWDD7eLVS+4hpDKExtViXQbLojs3ADWQ9sIXf5bk6RMMH6TO
+R2kAAAAVAK9vtK/Sc7ytjF4Vqm2hsAqo0xiHAAAAgD10vlz9gGHeqt03z8+FVQoBO/CCtr
+mT5+ozTfQe9D6+s/4dkJtAWfVBLS1zr4ZRJPoMaZLwblougnZ4ZuUTqzOxEDWMOjb9cTQS
+a9aN6CyE8qCaf02nnC3OP5AJajl+LXh5mM+YPcfEx7tIDeiJjWp3WSB3gtanwoSu4j5aRx
+uTAAAAgGwKUy6uMp/PamnIE5XxtThjpM38YkORbtt72avfjyfUsmf1pFNcFqfr5ca50k3W
+CzBbPtxNNwk/k8K57zoUYy+3TCajH/9zodKdNo1cxkPF39KG+wLs/k2OLFD+nqUpwor/9d
+nxhUKbiOnAQZZwYy7f8+hGx+kzNRC8ZSfWXxbVAAAAFQCbvkGgAc//pZvVfQ93SF0FMAvq
+hwAAABFyb290QDgxMmQ1ZmNjMmJlMgE=
+-----END OPENSSH PRIVATE KEY-----
diff --git a/src/test/resources/certificates/host/ssh_host_dsa_key.pub b/src/test/resources/certificates/host/ssh_host_dsa_key.pub
new file mode 100644
index 000000000..8180d52d8
--- /dev/null
+++ b/src/test/resources/certificates/host/ssh_host_dsa_key.pub
@@ -0,0 +1 @@
+ssh-dss AAAAB3NzaC1kc3MAAACBAMI20Ukge3gsygFBMU7Ol0R53gDfnx0KkzVDfmDqVRmefxQETUOd2impQ6JOvxiIzyup8bXWsdgRY3hve9nWiCiC3ClNPhZD4BzQJIqwC4WCDxHpt9eTWhz8lr1gw+3i1UvuIaQyhMbVYl0Gy6I7NwA1kPbCF3+W5OkTDB+kzkdpAAAAFQCvb7Sv0nO8rYxeFaptobAKqNMYhwAAAIA9dL5c/YBh3qrdN8/PhVUKATvwgra5k+fqM030HvQ+vrP+HZCbQFn1QS0tc6+GUST6DGmS8G5aLoJ2eGblE6szsRA1jDo2/XE0EmvWjegshPKgmn9Np5wtzj+QCWo5fi14eZjPmD3HxMe7SA3oiY1qd1kgd4LWp8KEruI+WkcbkwAAAIBsClMurjKfz2ppyBOV8bU4Y6TN/GJDkW7be9mr348n1LJn9aRTXBan6+XGudJN1gswWz7cTTcJP5PCue86FGMvt0wmox//c6HSnTaNXMZDxd/ShvsC7P5NjixQ/p6lKcKK//XZ8YVCm4jpwEGWcGMu3/PoRsfpMzUQvGUn1l8W1Q== root@812d5fcc2be2
diff --git a/src/test/resources/certificates/host/ssh_host_ecdsa_key b/src/test/resources/certificates/host/ssh_host_ecdsa_key
new file mode 100644
index 000000000..af17a4bcf
--- /dev/null
+++ b/src/test/resources/certificates/host/ssh_host_ecdsa_key
@@ -0,0 +1,9 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
+1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQQfpw96c0Uhv6F1R7U6yq8fh18hfpAH
+TstDFdjYOzucFcFgOW8jDSJqmrDuFuDYf40DD2/IC0M6LfIVVDNyAMUfAAAAsBMBRnwTAU
+Z8AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBB+nD3pzRSG/oXVH
+tTrKrx+HXyF+kAdOy0MV2Ng7O5wVwWA5byMNImqasO4W4Nh/jQMPb8gLQzot8hVUM3IAxR
+8AAAAhAOa/ybhNwy5ARRH33b2iiGCvH1nJs2LVFpJ+/hsKZggMAAAAEXJvb3RAODEyZDVm
+Y2MyYmUyAQIDBAUG
+-----END OPENSSH PRIVATE KEY-----
diff --git a/src/test/resources/certificates/host/ssh_host_ecdsa_key-cert.pub b/src/test/resources/certificates/host/ssh_host_ecdsa_key-cert.pub
new file mode 100644
index 000000000..0a69c800e
--- /dev/null
+++ b/src/test/resources/certificates/host/ssh_host_ecdsa_key-cert.pub
@@ -0,0 +1 @@
+ecdsa-sha2-nistp256-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgv6PZKc8OzM3kdo9Mhn1Zj20k/ZHbaCAgs/NExJxLzjgAAAAIbmlzdHAyNTYAAABBBB+nD3pzRSG/oXVHtTrKrx+HXyF+kAdOy0MV2Ng7O5wVwWA5byMNImqasO4W4Nh/jQMPb8gLQzot8hVUM3IAxR8AAAAAAAAAAAAAAAIAAAAKaG9zdF9lY2RzYQAAAA0AAAAJbG9jYWxob3N0AAAAAAAAAAD//////////wAAAAAAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAgpPpGR2s9KWbGPhmLM1NKBIrR67s1T0uBLuElIFw+wXAAAABTAAAAC3NzaC1lZDI1NTE5AAAAQEIxgKXLS+HosaCvPlrUsym3K9Oue51qN0qAnKL7v8PsXVXf0u7nEArqLsvcFoODUgemj2nB/grAc2Q0uGznfAU= root@812d5fcc2be2
diff --git a/src/test/resources/certificates/host/ssh_host_ecdsa_key.pub b/src/test/resources/certificates/host/ssh_host_ecdsa_key.pub
new file mode 100644
index 000000000..35359fad2
--- /dev/null
+++ b/src/test/resources/certificates/host/ssh_host_ecdsa_key.pub
@@ -0,0 +1 @@
+ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBB+nD3pzRSG/oXVHtTrKrx+HXyF+kAdOy0MV2Ng7O5wVwWA5byMNImqasO4W4Nh/jQMPb8gLQzot8hVUM3IAxR8= root@812d5fcc2be2
diff --git a/src/test/resources/certificates/host/ssh_host_ed25519_key b/src/test/resources/certificates/host/ssh_host_ed25519_key
new file mode 100644
index 000000000..307f478ba
--- /dev/null
+++ b/src/test/resources/certificates/host/ssh_host_ed25519_key
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACC/A+Qpq1umbEDxIobpJiFIUpJOaK0Bd44A+UoXpJEvbwAAAJh7bpi6e26Y
+ugAAAAtzc2gtZWQyNTUxOQAAACC/A+Qpq1umbEDxIobpJiFIUpJOaK0Bd44A+UoXpJEvbw
+AAAEAnZ+51LM9x7MNO6vkieJDmDSBDFJgmbpRz/fHQX1HmKb8D5CmrW6ZsQPEihukmIUhS
+kk5orQF3jgD5ShekkS9vAAAAEXJvb3RAODEyZDVmY2MyYmUyAQIDBA==
+-----END OPENSSH PRIVATE KEY-----
diff --git a/src/test/resources/certificates/host/ssh_host_ed25519_key-cert.pub b/src/test/resources/certificates/host/ssh_host_ed25519_key-cert.pub
new file mode 100644
index 000000000..85e77f9ac
--- /dev/null
+++ b/src/test/resources/certificates/host/ssh_host_ed25519_key-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIO0sjgw0Lw5NjQOJHNPQfYr6a567Zj9OPNHsAhFryCWaAAAAIL8D5CmrW6ZsQPEihukmIUhSkk5orQF3jgD5ShekkS9vAAAAAAAAAAAAAAACAAAADGhvc3RfZWQyNTUxOQAAAA0AAAAJbG9jYWxob3N0AAAAAAAAAAD//////////wAAAAAAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAgpPpGR2s9KWbGPhmLM1NKBIrR67s1T0uBLuElIFw+wXAAAABTAAAAC3NzaC1lZDI1NTE5AAAAQNxk8bNpFEJBmaiv+WxI7rlnY7dGy5oi0vpz2V684kkrITR6VisgomHa2k6lFv+oyrXBS+aNXp5B5sRfr4QQNQY= root@812d5fcc2be2
diff --git a/src/test/resources/certificates/host/ssh_host_ed25519_key.pub b/src/test/resources/certificates/host/ssh_host_ed25519_key.pub
new file mode 100644
index 000000000..fbbc89971
--- /dev/null
+++ b/src/test/resources/certificates/host/ssh_host_ed25519_key.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL8D5CmrW6ZsQPEihukmIUhSkk5orQF3jgD5ShekkS9v root@812d5fcc2be2
diff --git a/src/test/resources/certificates/host/ssh_host_rsa_key b/src/test/resources/certificates/host/ssh_host_rsa_key
new file mode 100644
index 000000000..f5c70c7a1
--- /dev/null
+++ b/src/test/resources/certificates/host/ssh_host_rsa_key
@@ -0,0 +1,38 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
+NhAAAAAwEAAQAAAYEAxy6jomQ/3xO0nQz0KNWCHhEEe5S9NFlZIYodaOlxJ2PkqryVeJh8
+PQISfK02wnfoKdYH6LtAR+C9xY3K7UjW1sdtXSLH7uDVutNYf8Y29kWxp6TTm3yEP72n7m
+Mp7QqJEuOvz3e0JCL3hvQO3YuWJdAvmnF/hsBN3zPM02Qi6muJKY7ed79JvYQ5Qya9SFWc
+l94pF7/vAiQCIqEgK8Gi4NGoJiRYGmvHXGXZIZ0g0Boe05Y3Mooi0lsvrS2DiXDA9bnW8W
+WH7/1ny9tUyIeFw9+5Ewyt6+OT5rwxg/yenPO/qpYFyRfvocQ75isFP/5DdbtZwtLnUw53
+t/CCuy421opFLea+TWslZrEefQYUMXazQ+43hfr8/uTKQbD2o0KKFt7gaLRY5CeaH1Tt/t
+XBV+NcqQ+5r1f1B57FJdWb2hKjVsEdrqJd/rZoHipMbIfl8C79d/DOZ6dINfHSpPUEl2jM
+T1iYKhi0SzpKw1LzOHizlqjhNkUOIR7CF5Ow/JJlAAAFiPUfrED1H6xAAAAAB3NzaC1yc2
+EAAAGBAMcuo6JkP98TtJ0M9CjVgh4RBHuUvTRZWSGKHWjpcSdj5Kq8lXiYfD0CEnytNsJ3
+6CnWB+i7QEfgvcWNyu1I1tbHbV0ix+7g1brTWH/GNvZFsaek05t8hD+9p+5jKe0KiRLjr8
+93tCQi94b0Dt2LliXQL5pxf4bATd8zzNNkIupriSmO3ne/Sb2EOUMmvUhVnJfeKRe/7wIk
+AiKhICvBouDRqCYkWBprx1xl2SGdINAaHtOWNzKKItJbL60tg4lwwPW51vFlh+/9Z8vbVM
+iHhcPfuRMMrevjk+a8MYP8npzzv6qWBckX76HEO+YrBT/+Q3W7WcLS51MOd7fwgrsuNtaK
+RS3mvk1rJWaxHn0GFDF2s0PuN4X6/P7kykGw9qNCihbe4Gi0WOQnmh9U7f7VwVfjXKkPua
+9X9QeexSXVm9oSo1bBHa6iXf62aB4qTGyH5fAu/XfwzmenSDXx0qT1BJdozE9YmCoYtEs6
+SsNS8zh4s5ao4TZFDiEewheTsPySZQAAAAMBAAEAAAGBAMN6+2+B0cmeblEACJQWzwexDe
+Q3WuWIltg605hGGx5chGwofs2HYc8CPKCN4sNCqOB+RO7c7z5bzAOZoEH2jZrmyGdyniPM
+FxavGxjzsLdMOQnd0yuzLZvdB3YHbntMLrESMlZ8FZitlJ6m4fv+ZZKg2kdKAq1+CC75iJ
+kimr3UYh4eMCn322ga35QO7g+SrgfCKjQ701cXfdz8ozUuaisYuF0OqETt6A+/iTTbH/v8
+1qozr+Jy/a/TfFwK4iA+PfOWhi411xWmwylHLJJImF9NZJ2cKHZAqbeMEpa/HgMurYUvJR
+N4XkaLH6z32n0VObjQX6Y7U5Viu6xDQb3gfxgQrIgvBMhbqAVZpdP4ig7qjGpCvVpYRnMf
+2ZLxPx3oybJEVv11tFH9UfX+gb0LyOSqgtkORFufLPAfop7uljni/XB6h8k7DXrsiJD9ay
+TnJVKHgifN7WfZazz9OHSJqx99LNjcrp5h/MRJkCrTfLLZd979XhZ+FjuU0OajqXML4QAA
+AMAiWYa5vam1+nQCo1qP4CDVeh56uNeCqQDqyljN0LYVo1kL9YVS6wvPt3rditBpWpOUSZ
+kJ48m0QoXAbqD2oEPukkhFvPHkawj9PsW6bO/foKf9K3gGyJVW0bMANFYI75klRGJ5IOAz
+jXK5YwUJk3Yly498WmIPoIGf39sqGH0TctwyPTaGtxYy6jzBesBCvlZGs8sXQfwo9URuww
+xrTGSKOx1G4eLejnq0y1tFf+bko4mLvdUwwzklI5eEmCue3D4AAADBAP9SCSg27Kz4rld8
+D7QwU9MIcF5zQ6iX0mF6HK926VzdH/fMIPVlLsny8XshC6208wDNK68jNb/5nuNwBi339X
+cWD7JUR06ab/At6I1QKRkrWA74qq68gLOJ/13TrYAXYUpp1tsM0m8t46soBykI99qL2hyG
+ukibnPlZqMqHk0SLcyRloiD2IDy4BByFn+LNV2eiQmRA8VxKYHxV3ZHKSLchXPf9cKo+zQ
+uYR5bCE4j9MsyIwwOncJ99AyL9LLOqGQAAAMEAx7ZabxM5zTXUE+VLXsnyz9Wb0rrhR656
+4DWqrQ5iPCQrZAK5vT+VmE75sS7nrUfuEiybYrl1LT5nU+1S809h2kxuyypKzVkbgqElD9
+9yfX7ZrFwDU201nvsLAeEnd/RRe9Civx79mrJleOHPHoDjva//s/AznAo2rk9aTTfcAyos
+sTqmM6h7K/kx0IMf6eRrUXcIPTIcAuz4gwDoitR6KLV2D0Hf1yAxPyNUe7Z561v8PIW2Fd
+CFyjfDpW6nsowtAAAAEXJvb3RAODEyZDVmY2MyYmUyAQ==
+-----END OPENSSH PRIVATE KEY-----
diff --git a/src/test/resources/certificates/host/ssh_host_rsa_key-cert.pub b/src/test/resources/certificates/host/ssh_host_rsa_key-cert.pub
new file mode 100644
index 000000000..85b596e0d
--- /dev/null
+++ b/src/test/resources/certificates/host/ssh_host_rsa_key-cert.pub
@@ -0,0 +1 @@
+ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgEapArMkpl2zhoJXKdvX+3K0hqM8m22k5NPYP/bryULMAAAADAQABAAABgQDHLqOiZD/fE7SdDPQo1YIeEQR7lL00WVkhih1o6XEnY+SqvJV4mHw9AhJ8rTbCd+gp1gfou0BH4L3FjcrtSNbWx21dIsfu4NW601h/xjb2RbGnpNObfIQ/vafuYyntCokS46/Pd7QkIveG9A7di5Yl0C+acX+GwE3fM8zTZCLqa4kpjt53v0m9hDlDJr1IVZyX3ikXv+8CJAIioSArwaLg0agmJFgaa8dcZdkhnSDQGh7TljcyiiLSWy+tLYOJcMD1udbxZYfv/WfL21TIh4XD37kTDK3r45PmvDGD/J6c87+qlgXJF++hxDvmKwU//kN1u1nC0udTDne38IK7LjbWikUt5r5NayVmsR59BhQxdrND7jeF+vz+5MpBsPajQooW3uBotFjkJ5ofVO3+1cFX41ypD7mvV/UHnsUl1ZvaEqNWwR2uol3+tmgeKkxsh+XwLv138M5np0g18dKk9QSXaMxPWJgqGLRLOkrDUvM4eLOWqOE2RQ4hHsIXk7D8kmUAAAAAAAAAAAAAAAIAAAAIaG9zdF9yc2EAAAANAAAACWxvY2FsaG9zdAAAAAAAAAAA//////////8AAAAAAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIKT6RkdrPSlmxj4ZizNTSgSK0eu7NU9LgS7hJSBcPsFwAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEC6TXPaqMTTl3QZrqLnSQ2CDcAD/Xslaf8br4uylFGivfEDmKcB63k9LyL9PlZdtNviAfaM92r55vU8AgiHDh0F root@812d5fcc2be2
diff --git a/src/test/resources/certificates/host/ssh_host_rsa_key.pub b/src/test/resources/certificates/host/ssh_host_rsa_key.pub
new file mode 100644
index 000000000..6b7d09682
--- /dev/null
+++ b/src/test/resources/certificates/host/ssh_host_rsa_key.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDHLqOiZD/fE7SdDPQo1YIeEQR7lL00WVkhih1o6XEnY+SqvJV4mHw9AhJ8rTbCd+gp1gfou0BH4L3FjcrtSNbWx21dIsfu4NW601h/xjb2RbGnpNObfIQ/vafuYyntCokS46/Pd7QkIveG9A7di5Yl0C+acX+GwE3fM8zTZCLqa4kpjt53v0m9hDlDJr1IVZyX3ikXv+8CJAIioSArwaLg0agmJFgaa8dcZdkhnSDQGh7TljcyiiLSWy+tLYOJcMD1udbxZYfv/WfL21TIh4XD37kTDK3r45PmvDGD/J6c87+qlgXJF++hxDvmKwU//kN1u1nC0udTDne38IK7LjbWikUt5r5NayVmsR59BhQxdrND7jeF+vz+5MpBsPajQooW3uBotFjkJ5ofVO3+1cFX41ypD7mvV/UHnsUl1ZvaEqNWwR2uol3+tmgeKkxsh+XwLv138M5np0g18dKk9QSXaMxPWJgqGLRLOkrDUvM4eLOWqOE2RQ4hHsIXk7D8kmU= root@812d5fcc2be2
diff --git a/src/test/resources/certificates/host/sshd_config b/src/test/resources/certificates/host/sshd_config
new file mode 100644
index 000000000..82558ff20
--- /dev/null
+++ b/src/test/resources/certificates/host/sshd_config
@@ -0,0 +1,19 @@
+PubkeyAuthentication yes
+
+AuthenticationMethods publickey
+
+PrintMotd no
+PermitRootLogin yes
+Subsystem sftp internal-sftp
+
+HostKey /etc/ssh/ssh_host_rsa_key
+HostCertificate /etc/ssh/ssh_host_rsa_key-cert.pub
+
+HostKey /etc/ssh/ssh_host_ecdsa_key
+HostCertificate /etc/ssh/ssh_host_ecdsa_key-cert.pub
+
+HostKey /etc/ssh/ssh_host_ed25519_key
+HostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub
+
+TrustedUserCAKeys /etc/ssh/ca_jsch_key.pub
+LogLevel DEBUG3
\ No newline at end of file
diff --git a/src/test/resources/certificates/host/user_keys/id_ecdsa_nistp521 b/src/test/resources/certificates/host/user_keys/id_ecdsa_nistp521
new file mode 100644
index 000000000..88adabfbc
--- /dev/null
+++ b/src/test/resources/certificates/host/user_keys/id_ecdsa_nistp521
@@ -0,0 +1,12 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAArAAAABNlY2RzYS
+1zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQB9z+oAz0HZplsszpeas0VX48xNLkz
+MtGz2IKBy/pi/DwXJ8eVDWfbq3r1phFeuZwv/KtCkyrKI627heWsHhcCA9cB42DMZZT5yM
+N0y/e4p0EPpnVXzcUs51cnsR6mSriLln3w9UBgTnALp5HVdT9IsJ2DJpAB5AY7p0U8ylT/
+LmBSwQIAAAEQU0MaU1NDGlMAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQ
+AAAIUEAfc/qAM9B2aZbLM6XmrNFV+PMTS5MzLRs9iCgcv6Yvw8FyfHlQ1n26t69aYRXrmc
+L/yrQpMqyiOtu4XlrB4XAgPXAeNgzGWU+cjDdMv3uKdBD6Z1V83FLOdXJ7Eepkq4i5Z98P
+VAYE5wC6eR1XU/SLCdgyaQAeQGO6dFPMpU/y5gUsECAAAAQgFgeY58EHXf6AicAmc3YTeD
+tJuLYRzSaXCfHtOGShGWkLtVdnhWP3wy1+1zImTpMLEETwoQatVvg+QTL0BFld1lrwAAAB
+FsdWlnaUBleGVtcGxlLmNvbQE=
+-----END OPENSSH PRIVATE KEY-----
diff --git a/src/test/resources/certificates/host/user_keys/id_ecdsa_nistp521.pub b/src/test/resources/certificates/host/user_keys/id_ecdsa_nistp521.pub
new file mode 100644
index 000000000..5dd07a272
--- /dev/null
+++ b/src/test/resources/certificates/host/user_keys/id_ecdsa_nistp521.pub
@@ -0,0 +1 @@
+ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAH3P6gDPQdmmWyzOl5qzRVfjzE0uTMy0bPYgoHL+mL8PBcnx5UNZ9urevWmEV65nC/8q0KTKsojrbuF5aweFwID1wHjYMxllPnIw3TL97inQQ+mdVfNxSznVyexHqZKuIuWffD1QGBOcAunkdV1P0iwnYMmkAHkBjunRTzKVP8uYFLBAg== luigi@exemple.com
diff --git a/src/test/resources/certificates/known_hosts b/src/test/resources/certificates/known_hosts
new file mode 100644
index 000000000..13b90b620
--- /dev/null
+++ b/src/test/resources/certificates/known_hosts
@@ -0,0 +1 @@
+@cert-authority localhost ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKT6RkdrPSlmxj4ZizNTSgSK0eu7NU9LgS7hJSBcPsFw ca@jsch
\ No newline at end of file
diff --git a/src/test/resources/certificates/rsa/root_rsa_key b/src/test/resources/certificates/rsa/root_rsa_key
new file mode 100644
index 000000000..162895b36
--- /dev/null
+++ b/src/test/resources/certificates/rsa/root_rsa_key
@@ -0,0 +1,49 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+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
+gOkwe4WdyW0AAAAJcm9vdEBqc2NoAQI=
+-----END OPENSSH PRIVATE KEY-----
diff --git a/src/test/resources/certificates/rsa/root_rsa_key-cert.pub b/src/test/resources/certificates/rsa/root_rsa_key-cert.pub
new file mode 100644
index 000000000..b2d1983e5
--- /dev/null
+++ b/src/test/resources/certificates/rsa/root_rsa_key-cert.pub
@@ -0,0 +1 @@
+ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAg8RWHHkK0L3imXXV3i9Bgon7gqasjXsVM24QsOgzzWUMAAAADAQABAAACAQC2EwoWAHefd/of7Zbkz1lMWUvbJLmLj9gvi4NjcPmX7hO758Et2n3CYKq0ZRrf7ZZ9DdoJmWyXG/dlE4NyK0vTnNjgHQHn3bBbYdbz+HKBkSuzvYRJHJSYJrhfJqsURwKjbyJayKx4LFzK/tAvewucVCm5U7r1GL+ScrsllVfu74IwNCTtOGeSYQ/epklphTa8AILLdJzDjyrYH0ZA9biKXBe9OO7mLMtB9uAZf6prPVRK7/QPN83I3e27F9qZtAQ2FOheR8Bu0FtxymKlcskMuNAxxcDB7ug35Rz893457Z8fFGsE58IpSNtuHTX0Xf8Rh3fnmk8lW5SzpP6c/MfT/nldw8aVZqCTt6weOsESxp6HPDkNoK4LcWwad61J9/2jUFeOT1UZXufs8260+P2c118xMwOWZjKCmdAhS9P6DEDI/W0TFzz1um5R06YI3g0Ox0+jX2hvMpDcufQ8OWK5a1cIvDIz1xwGffG3j3qSDQ23WSaI5ZtM8caqP3yQx+94Frd0+ZPOb3usJ8OINT3fAToaofM9FadTbFAlCV7jYOx9/6ov1qhzBxcecpl0I9b2EuS8TLQrNwQXGWeMRyPFE2yrsChRBPPULLncSWp2oyJ7XRxmP7otCRo9kSvf+a7O++9K4CFBOhXkBxOTagPLW7Ygxu/Jlzj/MWQ/ujJnmQAAAAAAAAAAAAAAAQAAABVyb290X3JzYV9rZXktY2VydC5wdWIAAAAIAAAABHJvb3QAAAAAAAAAAP//////////AAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAgpPpGR2s9KWbGPhmLM1NKBIrR67s1T0uBLuElIFw+wXAAAABTAAAAC3NzaC1lZDI1NTE5AAAAQIrR9u2miSHH+BCG8qtGPdHUizjsCctrrqsaTCiFXiZKfs7pM9if1IfArYVnRPpKstycd+L8KAwHUiMayN8JlgA= root@jsch
diff --git a/src/test/resources/certificates/rsa/root_rsa_key.pub b/src/test/resources/certificates/rsa/root_rsa_key.pub
new file mode 100644
index 000000000..1a25619b9
--- /dev/null
+++ b/src/test/resources/certificates/rsa/root_rsa_key.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC2EwoWAHefd/of7Zbkz1lMWUvbJLmLj9gvi4NjcPmX7hO758Et2n3CYKq0ZRrf7ZZ9DdoJmWyXG/dlE4NyK0vTnNjgHQHn3bBbYdbz+HKBkSuzvYRJHJSYJrhfJqsURwKjbyJayKx4LFzK/tAvewucVCm5U7r1GL+ScrsllVfu74IwNCTtOGeSYQ/epklphTa8AILLdJzDjyrYH0ZA9biKXBe9OO7mLMtB9uAZf6prPVRK7/QPN83I3e27F9qZtAQ2FOheR8Bu0FtxymKlcskMuNAxxcDB7ug35Rz893457Z8fFGsE58IpSNtuHTX0Xf8Rh3fnmk8lW5SzpP6c/MfT/nldw8aVZqCTt6weOsESxp6HPDkNoK4LcWwad61J9/2jUFeOT1UZXufs8260+P2c118xMwOWZjKCmdAhS9P6DEDI/W0TFzz1um5R06YI3g0Ox0+jX2hvMpDcufQ8OWK5a1cIvDIz1xwGffG3j3qSDQ23WSaI5ZtM8caqP3yQx+94Frd0+ZPOb3usJ8OINT3fAToaofM9FadTbFAlCV7jYOx9/6ov1qhzBxcecpl0I9b2EuS8TLQrNwQXGWeMRyPFE2yrsChRBPPULLncSWp2oyJ7XRxmP7otCRo9kSvf+a7O++9K4CFBOhXkBxOTagPLW7Ygxu/Jlzj/MWQ/ujJnmQ== root@jsch