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 + * This method supports wildcard patterns similar to OpenSSH's known_hosts file: + *

+ * + *

+ * The host field can contain multiple comma-separated patterns. The method returns {@code true} + * if the hostname matches ANY of the patterns. + *

+ *

+ * Examples: + *

+ * + * + * @param _host the hostname to test against the patterns, must not be {@code null} + * @return {@code true} if the hostname matches any of the patterns (with wildcard support); + * {@code false} otherwise + * @see #isMatched(String) + */ + boolean isWildcardMatched(String _host) { + if (_host == null) { + return false; + } + + String hosts = this.host; + if (hosts == null || hosts.isEmpty()) { + return false; + } + + // Split by comma and check each pattern + int i = 0; + int hostslen = hosts.length(); + while (i < hostslen) { + int j = hosts.indexOf(',', i); + String pattern; + if (j == -1) { + pattern = hosts.substring(i).trim(); + if (matchesWildcardPattern(pattern, _host)) { + return true; + } + break; + } else { + pattern = hosts.substring(i, j).trim(); + if (matchesWildcardPattern(pattern, _host)) { + return true; + } + i = j + 1; + } + } + return false; + } + + /** + * Tests if a hostname matches a single wildcard pattern. + *

+ * 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 Hashtable config = new Hashtable<>(); static { config.put("kex", Util.getSystemProperty("jsch.kex", "mlkem768x25519-sha256,curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256")); config.put("server_host_key", Util.getSystemProperty("jsch.server_host_key", + "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-ed25519,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,rsa-sha2-512,rsa-sha2-256")); + // CASignatureAlgorithms: specifies which algorithms are allowed for signing of certificates + // by certificate authorities (CAs). Default matches OpenSSH 8.2+ (excludes ssh-rsa/SHA-1). + config.put("ca_signature_algorithms", Util.getSystemProperty("jsch.ca_signature_algorithms", "ssh-ed25519,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,rsa-sha2-512,rsa-sha2-256")); config.put("prefer_known_host_key_types", Util.getSystemProperty("jsch.prefer_known_host_key_types", "yes")); @@ -239,7 +247,7 @@ public class JSch { config.put("PreferredAuthentications", Util.getSystemProperty("jsch.preferred_authentications", "gssapi-with-mic,publickey,keyboard-interactive,password")); config.put("PubkeyAcceptedAlgorithms", Util.getSystemProperty("jsch.client_pubkey", - "ssh-ed25519,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,rsa-sha2-512,rsa-sha2-256")); + "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-ed25519,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,rsa-sha2-512,rsa-sha2-256")); config.put("enable_pubkey_auth_query", Util.getSystemProperty("jsch.enable_pubkey_auth_query", "yes")); config.put("try_additional_pubkey_algorithms", @@ -259,6 +267,15 @@ public class JSch { config.put("MaxAuthTries", Util.getSystemProperty("jsch.max_auth_tries", "6")); config.put("ClearAllForwardings", "no"); + /* + * host_certificate_to_key_fallback: Controls behavior when host certificate validation fails. - + * "yes" (default): Fall back to standard public key verification using the certificate's + * embedded public key. This matches OpenSSH behavior, which always performs this fallback. - + * "no": Reject connection if certificate validation fails (more secure, but may break existing + * setups when upgrading to a JSch version with certificate support). + */ + config.put("host_certificate_to_key_fallback", + Util.getSystemProperty("jsch.host_certificate_to_key_fallback", "yes")); } final InstanceLogger instLogger = new InstanceLogger(); @@ -320,11 +337,11 @@ public JSch() {} * "user.name" will be referred. * * @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 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 Map getConfig() { * Sets the logger * * @param logger logger or null 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. + *

+ * + *

Public Key vs. Certificate Handling

+ *

+ * The method handles two distinct input formats: + *

+ *
    + *
  • Plain Public Keys: When {@code alg} is a standard key algorithm (e.g., + * {@code "ssh-rsa"}, {@code "ssh-ed25519"}), the {@code K_S} parameter contains the server's + * public key in SSH wire format. The method parses the key components and verifies the signature + * directly.
  • + * + *
  • OpenSSH Certificates: When {@code alg} is a certificate type (e.g., + * {@code "ssh-rsa-cert-v01@openssh.com"}), the {@code K_S} parameter contains an OpenSSH + * certificate structure. The method: + *
      + *
    1. Parses the certificate to extract the embedded public key
    2. + *
    3. Validates that the certificate is a host certificate (not a user certificate)
    4. + *
    5. Replaces {@code K_S} with the extracted public key
    6. + *
    7. Extracts the underlying algorithm name from the public key
    8. + *
    9. Stores the certificate for subsequent CA validation
    10. + *
    11. Proceeds with signature verification using the extracted public key
    12. + *
    + *
  • + *
+ * + *

Two-Stage Verification for Certificates

+ *

+ * 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)}. + *

+ * + *

Signature Verification Process

+ *

+ * After extracting the public key (either from the plain input or from within a certificate), the + * method: + *

+ *
    + *
  1. Determines the key algorithm (RSA, DSS, ECDSA, or EdDSA)
  2. + *
  3. Parses the algorithm-specific public key components from the SSH wire format
  4. + *
  5. Instantiates the appropriate signature verification class
  6. + *
  7. Verifies that {@code sig_of_H} is a valid signature of the exchange hash {@code H} using + * the public key
  8. + *
+ * + * @param alg the server host key algorithm name. This can be either a plain key algorithm (e.g., + * {@code "ssh-rsa"}, {@code "ssh-dss"}, {@code "ecdsa-sha2-nistp256"}, + * {@code "ssh-ed25519"}) or a certificate type (e.g., + * {@code "ssh-rsa-cert-v01@openssh.com"}, {@code "ssh-ed25519-cert-v01@openssh.com"}). For + * certificates, this parameter is internally replaced with the underlying key algorithm + * extracted from the certificate. + * @param K_S the server's public key blob in SSH wire format. For plain keys, this contains the + * public key directly. For certificates, this contains the complete OpenSSH certificate + * structure, which includes the public key along with additional metadata (CA signature, + * principals, validity period, etc.). When a certificate is detected, this reference is + * replaced internally with the extracted public key for verification purposes. + * @param index the starting byte offset within {@code K_S} from which to begin parsing. For plain + * keys, this is typically the position after the algorithm string. For certificates, this + * is typically {@code 0} (start of the certificate blob), and the offset is recalculated + * after extracting the embedded public key. + * @param sig_of_H the signature bytes to verify. This is the server's signature of the exchange + * hash {@code H}, which proves the server possesses the private key. The signature format + * is algorithm-specific and includes both the algorithm identifier and the actual + * signature data in SSH wire format. + * @return {@code true} if the signature is cryptographically valid and proves the server + * possesses the private key; {@code false} otherwise. + * @throws JSchException if the algorithm is unsupported, if a certificate is detected but is not + * a host certificate (e.g., it's a user certificate), if the signature verification class + * cannot be instantiated, or if any other error occurs during verification. + * @throws Exception if an unexpected error occurs during parsing or cryptographic operations. + */ protected boolean verify(String alg, byte[] K_S, int index, byte[] sig_of_H) throws Exception { int i, j; - - i = index; boolean result = false; + i = index; + + if (OpenSshCertificateKeyTypes.isCertificateKeyType(alg)) { + OpenSshCertificate certificate = OpenSshCertificateParser.parse(session.jsch.instLogger, K_S); + + // Certificates used for host authentication MUST have "certificate role" of + // SSH2_CERT_TYPE_HOST. + // Other certificate types MUST not be accepted. + if (!certificate.isHostCertificate()) { + throw new JSchInvalidHostCertificateException("Rejected certificate '" + certificate.getId() + + "': user certificate presented for host authentication. " + "Host: " + session.host); + } + K_S = certificate.getCertificatePublicKey(); + if (K_S == null) { + throw new JSchException( + "Invalid certificate '" + certificate.getId() + "': missing public key"); + } + + // Extract algorithm from certificate public key + i = 0; + j = 0; + j = ((K_S[i++] << 24) & 0xff000000) | ((K_S[i++] << 16) & 0x00ff0000) + | ((K_S[i++] << 8) & 0x0000ff00) | ((K_S[i++]) & 0x000000ff); + alg = Util.byte2str(K_S, i, j); + i += j; + + this.hostKeyCertificate = certificate; + } if (alg.equals("ssh-rsa")) { byte[] tmp; diff --git a/src/main/java/com/jcraft/jsch/OpenSSHConfig.java b/src/main/java/com/jcraft/jsch/OpenSSHConfig.java index a66a04c7c..1a5dea34a 100644 --- a/src/main/java/com/jcraft/jsch/OpenSSHConfig.java +++ b/src/main/java/com/jcraft/jsch/OpenSSHConfig.java @@ -71,6 +71,7 @@ *
  • LocalForward
  • *
  • RemoteForward
  • *
  • ClearAllForwardings
  • + *
  • CASignatureAlgorithms
  • * * * @see ConfigRepository @@ -79,7 +80,7 @@ public class OpenSSHConfig implements ConfigRepository { private static final Set keysWithListAdoption = Stream .of("KexAlgorithms", "Ciphers", "HostKeyAlgorithms", "MACs", "PubkeyAcceptedAlgorithms", - "PubkeyAcceptedKeyTypes") + "PubkeyAcceptedKeyTypes", "CASignatureAlgorithms") .map(string -> string.toUpperCase(Locale.ROOT)).collect(Collectors.toSet()); /** @@ -173,6 +174,7 @@ static Hashtable getKeymap() { keymap.put("compression.c2s", "Compression"); keymap.put("compression_level", "CompressionLevel"); keymap.put("MaxAuthTries", "NumberOfPasswordPrompts"); + keymap.put("ca_signature_algorithms", "CASignatureAlgorithms"); } class MyConfig implements Config { diff --git a/src/main/java/com/jcraft/jsch/OpenSshCertificate.java b/src/main/java/com/jcraft/jsch/OpenSshCertificate.java new file mode 100644 index 000000000..7a9571db4 --- /dev/null +++ b/src/main/java/com/jcraft/jsch/OpenSshCertificate.java @@ -0,0 +1,350 @@ +package com.jcraft.jsch; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +/** + * Represents an OpenSSH certificate containing all the fields defined in the OpenSSH certificate + * format. + * + *

    + * 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 principals; + + // match ssh-keygen behavior where the default is the epoch + private final long validAfter; + + // match ssh-keygen behavior where the default would be forever + private final long validBefore; + + /** + * Critical options that must be recognized by the SSH implementation + */ + private final Map criticalOptions; + + /** + * Extensions that provide additional functionality + */ + private final Map extensions; + + /** + * Reserved field for future use + */ + private final String reserved; + + /** + * The CA's key that signed this certificate + */ + private final byte[] signatureKey; + + /** + * The cryptographic signature of the certificate + */ + private final byte[] signature; + + /** + * The certificate data without the certificate type and the signature + */ + private final byte[] message; + + /** + * Private constructor to be used exclusively by the Builder. + */ + private OpenSshCertificate(Builder builder) { + this.keyType = builder.keyType; + this.nonce = builder.nonce; + this.certificatePublicKey = builder.certificatePublicKey; + this.serial = builder.serial; + this.type = builder.type; + this.id = builder.id; + this.principals = builder.principals; + this.validAfter = builder.validAfter; + this.validBefore = builder.validBefore; + this.criticalOptions = builder.criticalOptions; + this.extensions = builder.extensions; + this.reserved = builder.reserved; + this.signatureKey = builder.signatureKey; + this.signature = builder.signature; + this.message = builder.message; + } + + String getKeyType() { + return keyType; + } + + byte[] getNonce() { + return nonce == null ? null : nonce.clone(); + } + + byte[] getCertificatePublicKey() { + return certificatePublicKey == null ? null : certificatePublicKey.clone(); + } + + long getSerial() { + return serial; + } + + int getType() { + return type; + } + + String getId() { + return id; + } + + Collection getPrincipals() { + return principals == null ? null : Collections.unmodifiableCollection(principals); + } + + long getValidAfter() { + return validAfter; + } + + long getValidBefore() { + return validBefore; + } + + Map getCriticalOptions() { + return criticalOptions == null ? null : Collections.unmodifiableMap(criticalOptions); + } + + Map getExtensions() { + return extensions == null ? null : Collections.unmodifiableMap(extensions); + } + + String getReserved() { + return reserved; + } + + byte[] getSignatureKey() { + return signatureKey == null ? null : signatureKey.clone(); + } + + byte[] getSignature() { + return signature == null ? null : signature.clone(); + } + + boolean isUserCertificate() { + return SSH2_CERT_TYPE_USER == type; + } + + boolean isHostCertificate() { + return SSH2_CERT_TYPE_HOST == type; + } + + boolean isValidNow() { + return OpenSshCertificateUtil.isValidNow(this); + } + + byte[] getMessage() { + return message == null ? null : message.clone(); + } + + /** + * A static inner builder class for creating immutable OpenSshCertificate instances. + */ + static class Builder { + private String keyType; + private byte[] nonce; + private byte[] certificatePublicKey; + private long serial; + private int type; + private String id; + private Collection principals; + private long validAfter = MIN_VALIDITY; + private long validBefore = MAX_VALIDITY; + private Map criticalOptions; + private Map extensions; + private String reserved; + private byte[] signatureKey; + private byte[] signature; + private byte[] message; + + Builder() {} + + Builder keyType(String keyType) { + this.keyType = keyType; + return this; + } + + Builder nonce(byte[] nonce) { + this.nonce = nonce; + return this; + } + + Builder certificatePublicKey(byte[] pk) { + this.certificatePublicKey = pk; + return this; + } + + Builder serial(long serial) { + this.serial = serial; + return this; + } + + Builder type(int type) { + this.type = type; + return this; + } + + Builder id(String id) { + this.id = id; + return this; + } + + Builder principals(Collection principals) { + this.principals = principals; + return this; + } + + Builder validAfter(long validAfter) { + this.validAfter = validAfter; + return this; + } + + Builder validBefore(long validBefore) { + this.validBefore = validBefore; + return this; + } + + Builder criticalOptions(Map opts) { + this.criticalOptions = opts; + return this; + } + + Builder extensions(Map exts) { + this.extensions = exts; + return this; + } + + Builder reserved(String reserved) { + this.reserved = reserved; + return this; + } + + Builder signatureKey(byte[] sigKey) { + this.signatureKey = sigKey; + return this; + } + + Builder signature(byte[] signature) { + this.signature = signature; + return this; + } + + Builder message(byte[] message) { + this.message = message; + return this; + } + + /** + * Constructs and returns an immutable OpenSshCertificate instance. + * + * @return A new, immutable OpenSshCertificate object. + * @throws IllegalStateException if any required field is missing or invalid. + */ + OpenSshCertificate build() { + validate(); + return new OpenSshCertificate(this); + } + + /** + * Validates that all required fields are present and valid. + * + * @throws IllegalStateException if any required field is missing or invalid. + */ + private void validate() { + if (keyType == null || keyType.trim().isEmpty()) { + throw new IllegalStateException("keyType is required and cannot be null or empty"); + } + if (nonce == null || nonce.length == 0) { + throw new IllegalStateException("nonce is required and cannot be null or empty"); + } + if (certificatePublicKey == null || certificatePublicKey.length == 0) { + throw new IllegalStateException( + "certificatePublicKey is required and cannot be null or empty"); + } + if (type != SSH2_CERT_TYPE_USER && type != SSH2_CERT_TYPE_HOST) { + throw new IllegalStateException( + "type must be SSH2_CERT_TYPE_USER (1) or SSH2_CERT_TYPE_HOST (2), got: " + type); + } + if (signatureKey == null || signatureKey.length == 0) { + throw new IllegalStateException("signatureKey is required and cannot be null or empty"); + } + if (signature == null || signature.length == 0) { + throw new IllegalStateException("signature is required and cannot be null or empty"); + } + if (message == null || message.length == 0) { + throw new IllegalStateException("message is required and cannot be null or empty"); + } + } + } +} diff --git a/src/main/java/com/jcraft/jsch/OpenSshCertificateAwareIdentityFile.java b/src/main/java/com/jcraft/jsch/OpenSshCertificateAwareIdentityFile.java new file mode 100644 index 000000000..755f7f357 --- /dev/null +++ b/src/main/java/com/jcraft/jsch/OpenSshCertificateAwareIdentityFile.java @@ -0,0 +1,261 @@ +package com.jcraft.jsch; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/** + * An {@link Identity} implementation that supports OpenSSH certificates. + * + *

    + * 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 getStrings() { + List list = new ArrayList<>(); + while (getLength() > 0) { + String s = Util.byte2str(getString(), StandardCharsets.UTF_8); + list.add(s); + } + return list; + } + + /** + * Reads critical options from the buffer. + * + *

    + * Critical options are stored as key-value pairs in SSH wire format. + *

    + * + * @return map of critical option names to values + */ + Map getCriticalOptions() { + return getKeyValueData(); + } + + /** + * Reads extensions from the buffer. + * + *

    + * Extensions are stored as key-value pairs in SSH wire format. + *

    + * + * @return map of extension names to values + */ + Map getExtensions() { + return getKeyValueData(); + } + + /** + * Reads key-value pair data from the buffer. + * + *

    + * 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 getKeyValueData() { + Map map = new LinkedHashMap<>(); + + if (getLength() > 0) { + OpenSshCertificateBuffer keyValueDataBuffer = new OpenSshCertificateBuffer(getString()); + while (keyValueDataBuffer.getLength() > 0) { + String key = Util.byte2str(keyValueDataBuffer.getString(), StandardCharsets.UTF_8); + String value = Util.byte2str(keyValueDataBuffer.getString(), StandardCharsets.UTF_8); + map.put(key, value); + } + } + return map; + } +} diff --git a/src/main/java/com/jcraft/jsch/OpenSshCertificateHostKeyVerifier.java b/src/main/java/com/jcraft/jsch/OpenSshCertificateHostKeyVerifier.java new file mode 100644 index 000000000..14951dd60 --- /dev/null +++ b/src/main/java/com/jcraft/jsch/OpenSshCertificateHostKeyVerifier.java @@ -0,0 +1,163 @@ +package com.jcraft.jsch; + +import java.util.Collection; +import java.util.Locale; + +/** + * A verifier for OpenSSH host certificates. + *

    + * 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: + *
      + *
    • Checking that the signing CA is trusted for the given host.
    • + *
    • Validating the certificate's type, validity period, and principals (hostnames).
    • + *
    • Cryptographically verifying the certificate's signature.
    • + *
    • Ensuring no unrecognized critical options are present.
    • + *
    + */ +class OpenSshCertificateHostKeyVerifier { + + /** + * Performs a complete verification of a host's OpenSSH certificate. + *

    + * 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 principals = certificate.getPrincipals(); + if (principals == null || principals.isEmpty()) { + throw new JSchException("rejected HostKey: invalid principal '" + host + + "', allowed principals list is null or empty."); + } + + // Convert host to lowercase for principal matching (same as OpenSSH ssh_login()) + String principalHost = host.toLowerCase(Locale.ROOT); + + if (!principals.contains(principalHost)) { + throw new JSchException("rejected HostKey: invalid principal '" + principalHost + + "', allowed principals: " + principals); + } + + if (!OpenSshCertificateUtil.isEmpty(certificate.getCriticalOptions())) { + // no critical option defined for host keys yet + throw new JSchInvalidHostCertificateException( + "rejected HostKey: unrecognized critical options " + certificate.getCriticalOptions()); + } + } + + /** + * Verifies the cryptographic signature of the certificate. + *

    + * 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 -cert-v01@openssh.com} + *

    + * + * @see OpenSSH Certificate + * Protocol + */ +final class OpenSshCertificateKeyTypes { + + /** + * RSA certificate key type using SHA-1 for the host key signature. + */ + static final String SSH_RSA_CERT_V01 = "ssh-rsa-cert-v01@openssh.com"; + + /** + * RSA certificate key type using SHA-256 for the host key signature. + */ + static final String RSA_SHA2_256_CERT_V01 = "rsa-sha2-256-cert-v01@openssh.com"; + + /** + * RSA certificate key type using SHA-512 for the host key signature. + */ + static final String RSA_SHA2_512_CERT_V01 = "rsa-sha2-512-cert-v01@openssh.com"; + + /** + * DSA (DSS) certificate key type. + */ + static final String SSH_DSS_CERT_V01 = "ssh-dss-cert-v01@openssh.com"; + + /** + * ECDSA certificate key type using NIST P-256 curve. + */ + static final String ECDSA_SHA2_NISTP256_CERT_V01 = "ecdsa-sha2-nistp256-cert-v01@openssh.com"; + + /** + * ECDSA certificate key type using NIST P-384 curve. + */ + static final String ECDSA_SHA2_NISTP384_CERT_V01 = "ecdsa-sha2-nistp384-cert-v01@openssh.com"; + + /** + * ECDSA certificate key type using NIST P-521 curve. + */ + static final String ECDSA_SHA2_NISTP521_CERT_V01 = "ecdsa-sha2-nistp521-cert-v01@openssh.com"; + + /** + * Ed25519 certificate key type. + */ + static final String SSH_ED25519_CERT_V01 = "ssh-ed25519-cert-v01@openssh.com"; + + /** + * Ed448 certificate key type. + */ + static final String SSH_ED448_CERT_V01 = "ssh-ed448-cert-v01@openssh.com"; + + /** + * Suffix used for all OpenSSH certificate key types. + */ + static final String CERT_SUFFIX = "-cert-v01@openssh.com"; + + /** + * Private constructor to prevent instantiation. + */ + private OpenSshCertificateKeyTypes() { + // Utility class - do not instantiate + } + + /** + * Checks if the given key type string represents an OpenSSH certificate. + * + * @param keyType the key type string to check + * @return {@code true} if the key type is a supported OpenSSH certificate type, {@code false} + * otherwise + */ + static boolean isCertificateKeyType(String keyType) { + if (keyType == null) { + return false; + } + switch (keyType) { + case SSH_RSA_CERT_V01: + case RSA_SHA2_256_CERT_V01: + case RSA_SHA2_512_CERT_V01: + case SSH_DSS_CERT_V01: + case ECDSA_SHA2_NISTP256_CERT_V01: + case ECDSA_SHA2_NISTP384_CERT_V01: + case ECDSA_SHA2_NISTP521_CERT_V01: + case SSH_ED25519_CERT_V01: + case SSH_ED448_CERT_V01: + return true; + default: + return false; + } + } + + /** + * Extracts the base key type from a certificate key type. + *

    + * 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: + *

    + *
      + *
    1. Key type
    2. + *
    3. Nonce
    4. + *
    5. Certificate public key
    6. + *
    7. Serial number
    8. + *
    9. Certificate type
    10. + *
    11. Key ID
    12. + *
    13. Valid principals
    14. + *
    15. Valid after timestamp
    16. + *
    17. Valid before timestamp
    18. + *
    19. Critical options
    20. + *
    21. Extensions
    22. + *
    23. Reserved field
    24. + *
    25. Signature key
    26. + *
    27. Signature
    28. + *
    + * + * @param instLogger logger instance for debugging + * @param certificateData the certificate data + * @return the parsed certificate object + * @throws JSchException if the certificate format is invalid or unsupported + */ + static OpenSshCertificate parse(InstanceLogger instLogger, byte[] certificateData) + throws JSchException { + + OpenSshCertificateBuffer buffer = new OpenSshCertificateBuffer(certificateData); + + OpenSshCertificate.Builder openSshCertificateBuilder = new OpenSshCertificate.Builder(); + + String kTypeFromData = OpenSshCertificateUtil + .trimToEmptyIfNull(Util.byte2str(buffer.getString(), StandardCharsets.UTF_8)); + + openSshCertificateBuilder.keyType(kTypeFromData).nonce(buffer.getString()); + + // KeyPair.parsePubkeyBlob expect keytype in public key blob + KeyPair publicKey = parsePublicKey(instLogger, kTypeFromData, buffer); + openSshCertificateBuilder.certificatePublicKey(publicKey.getPublicKeyBlob()) + .serial(buffer.getLong()).type(buffer.getInt()) + .id(Util.byte2str(buffer.getString(), StandardCharsets.UTF_8)); + + // Principals + byte[] principalsBlob = buffer.getBytes(); + OpenSshCertificateBuffer principalsBuffer = new OpenSshCertificateBuffer(principalsBlob); + Collection principals = principalsBuffer.getStrings(); + openSshCertificateBuilder.principals(principals).validAfter(buffer.getLong()) + .validBefore(buffer.getLong()).criticalOptions(buffer.getCriticalOptions()) + .extensions(buffer.getExtensions()) + .reserved(Util.byte2str(buffer.getString(), StandardCharsets.UTF_8)) + .signatureKey(buffer.getString()); + + int messageEndIndex = buffer.s; + + byte[] message = Arrays.copyOfRange(buffer.buffer, 0, messageEndIndex); + + openSshCertificateBuilder.message(message); + + openSshCertificateBuilder.signature(buffer.getString()); + + OpenSshCertificate certificate = openSshCertificateBuilder.build(); + + if (buffer.s != buffer.index) { + throw new JSchException( + "Cannot read OpenSSH certificate, got more data than expected: " + buffer.s + ", actual: " + + buffer.index + ". ID of the ca certificate: " + certificate.getId()); + } + + return certificate; + } + + /** + * Parses a public key from a buffer based on the specified key type. + * + * This method is used to deserialize public key components from a binary buffer, typically from + * an SSH certificate or public key file. It uses a {@code switch} statement to handle different + * key types, including RSA, DSA, ECDSA, Ed25519, and Ed448, and their corresponding certificate + * variations. The method reads the necessary key components (e.g., modulus, exponent, curve + * parameters) from the buffer and uses them to construct the appropriate {@link KeyPair} object. + * + * @param instLogger An instance of {@link JSch.InstanceLogger} for logging. + * @param keyType The string identifier for the public key algorithm (e.g., + * "ssh-rsa-cert-v01@openssh.com"). + * @param buffer The {@link Buffer} containing the binary representation of the public key. + * @return A {@link KeyPair} object representing the parsed public key. + * @throws JSchException if the key type is unsupported or if there is an error parsing the key + * components from the buffer. + */ + static KeyPair parsePublicKey(InstanceLogger instLogger, String keyType, Buffer buffer) + throws JSchException { + switch (keyType) { + case OpenSshCertificateKeyTypes.SSH_RSA_CERT_V01: + case OpenSshCertificateKeyTypes.RSA_SHA2_256_CERT_V01: + case OpenSshCertificateKeyTypes.RSA_SHA2_512_CERT_V01: + byte[] pub_array = buffer.getMPInt(); // e + byte[] n_array = buffer.getMPInt(); // n + return new KeyPairRSA(instLogger, n_array, pub_array, null); + case OpenSshCertificateKeyTypes.SSH_DSS_CERT_V01: + byte[] p_array = buffer.getMPInt(); + byte[] q_array = buffer.getMPInt(); + byte[] g_array = buffer.getMPInt(); + byte[] y_array = buffer.getMPInt(); + return new KeyPairDSA(instLogger, p_array, q_array, g_array, y_array, null); + case OpenSshCertificateKeyTypes.ECDSA_SHA2_NISTP256_CERT_V01: + case OpenSshCertificateKeyTypes.ECDSA_SHA2_NISTP384_CERT_V01: + case OpenSshCertificateKeyTypes.ECDSA_SHA2_NISTP521_CERT_V01: + byte[] name = buffer.getString(); + int len = buffer.getInt(); + int expectedLen = getExpectedEcdsaPointLength(keyType); + if (len != expectedLen) { + throw new JSchException("Invalid ECDSA public key length for " + keyType + ": expected " + + expectedLen + ", got " + len); + } + int pointFormat = buffer.getByte(); + if (pointFormat != OpenSshCertificateUtil.EC_POINT_FORMAT_UNCOMPRESSED) { + throw new JSchException( + "Invalid ECDSA public key format: expected uncompressed point (0x04), got 0x" + + Integer.toHexString(pointFormat & 0xff)); + } + byte[] r_array = new byte[(len - 1) / 2]; + byte[] s_array = new byte[(len - 1) / 2]; + buffer.getByte(r_array); + buffer.getByte(s_array); + return new KeyPairECDSA(instLogger, name, r_array, s_array, null); + case OpenSshCertificateKeyTypes.SSH_ED25519_CERT_V01: + int ed25519Len = buffer.getInt(); + if (ed25519Len != OpenSshCertificateUtil.ED25519_PUBLIC_KEY_LENGTH) { + throw new JSchException("Invalid Ed25519 public key length: expected " + + OpenSshCertificateUtil.ED25519_PUBLIC_KEY_LENGTH + ", got " + ed25519Len); + } + byte[] ed25519_pub_array = new byte[ed25519Len]; + buffer.getByte(ed25519_pub_array); + return new KeyPairEd25519(instLogger, ed25519_pub_array, null); + case OpenSshCertificateKeyTypes.SSH_ED448_CERT_V01: + int ed448Len = buffer.getInt(); + if (ed448Len != OpenSshCertificateUtil.ED448_PUBLIC_KEY_LENGTH) { + throw new JSchException("Invalid Ed448 public key length: expected " + + OpenSshCertificateUtil.ED448_PUBLIC_KEY_LENGTH + ", got " + ed448Len); + } + byte[] ed448_pub_array = new byte[ed448Len]; + buffer.getByte(ed448_pub_array); + return new KeyPairEd448(instLogger, ed448_pub_array, null); + default: + throw new JSchException("Unsupported Algorithm for Certificate public key: " + keyType); + } + } + + /** + * Returns the expected uncompressed EC point length for a given ECDSA certificate key type. + * + *

    + * 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 isKnownHostCaPublicKeyEntry = + hostKey -> Objects.nonNull(hostKey) && "@cert-authority".equals(hostKey.getMarker()); + + /** + * Predicate that identifies revoked key entries in a known_hosts file. + *

    + * 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 isMarkedRevoked = + hostKey -> hostKey == null || "@revoked".equals(hostKey.getMarker()); + + /** + * EC point format indicator for uncompressed points (SEC 1, section 2.3.3). + *

    + * 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. + *

    + * + *

    Algorithm Mapping

    + *

    + * The method maintains an internal mapping between base signature algorithms and their + * certificate counterparts: + *

    + *
      + *
    • {@code ssh-ed25519} → {@code ssh-ed25519-cert-v01@openssh.com}
    • + *
    • {@code ssh-ed448} → {@code ssh-ed448-cert-v01@openssh.com}
    • + *
    • {@code ssh-rsa} → {@code ssh-rsa-cert-v01@openssh.com}
    • + *
    • {@code rsa-sha2-256} → {@code rsa-sha2-256-cert-v01@openssh.com}
    • + *
    • {@code rsa-sha2-512} → {@code rsa-sha2-512-cert-v01@openssh.com}
    • + *
    • {@code ssh-dss} → {@code ssh-dss-cert-v01@openssh.com}
    • + *
    • {@code ecdsa-sha2-nistp256} → {@code ecdsa-sha2-nistp256-cert-v01@openssh.com}
    • + *
    • {@code ecdsa-sha2-nistp384} → {@code ecdsa-sha2-nistp384-cert-v01@openssh.com}
    • + *
    • {@code ecdsa-sha2-nistp521} → {@code ecdsa-sha2-nistp521-cert-v01@openssh.com}
    • + *
    + * + * @param serverHostKey comma-separated list of server host key algorithms to filter. This + * typically contains a mix of plain key algorithms (e.g., {@code ssh-ed25519}) and + * certificate types (e.g., {@code ssh-ed25519-cert-v01@openssh.com}). May be {@code null}. + * @param unavailableSignatures array of base signature algorithms that are unavailable on this + * system, as determined by {@link Session#checkSignatures(String)}. Each entry is a plain + * algorithm name like {@code ssh-ed25519} or {@code rsa-sha2-512}. May be {@code null} or + * empty if all signature algorithms are available. + * @return the filtered comma-separated list of server host key algorithms with unavailable + * certificate types removed, or {@code null} if all algorithms were filtered out. If + * {@code unavailableSignatures} is {@code null} or empty, returns {@code serverHostKey} + * unchanged. + */ + static String filterUnavailableCertTypes(String serverHostKey, String[] unavailableSignatures) { + if (unavailableSignatures == null || unavailableSignatures.length == 0) { + return serverHostKey; + } + + if (JSch.getLogger().isEnabled(Logger.DEBUG)) { + JSch.getLogger().log(Logger.DEBUG, + "server_host_key proposal before removing unavailable cert types is: " + serverHostKey); + } + + // Build list of certificate types to remove based on unavailable base signatures + List certsToRemove = new ArrayList(); + + for (String unavailableSig : unavailableSignatures) { + // Map base algorithm to corresponding certificate type using centralized mapping + String certType = OpenSshCertificateKeyTypes.getCertificateKeyType(unavailableSig); + if (certType != null) { + certsToRemove.add(certType); + } + } + + if (!certsToRemove.isEmpty()) { + String[] certsArray = new String[certsToRemove.size()]; + certsToRemove.toArray(certsArray); + serverHostKey = Util.diffString(serverHostKey, certsArray); + + if (JSch.getLogger().isEnabled(Logger.DEBUG)) { + for (String cert : certsArray) { + JSch.getLogger().log(Logger.DEBUG, "Removing " + cert + " (base algorithm unavailable)"); + } + JSch.getLogger().log(Logger.DEBUG, + "server_host_key proposal after removing unavailable cert types is: " + serverHostKey); + } + } + return serverHostKey; + } + + /** + * Validates that a certificate is signed by a trusted, non-revoked Certificate Authority. + *

    + * This method performs the critical CA validation step for OpenSSH certificate authentication. It + * verifies that: + *

    + *
      + *
    1. The CA public key exists in the known_hosts file with {@code @cert-authority} marker
    2. + *
    3. The CA entry matches the connecting host's pattern
    4. + *
    5. The CA key has not been revoked (no {@code @revoked} entry for same key)
    6. + *
    7. The certificate was signed by this CA (CA public key matches)
    8. + *
    + * + *

    Validation Flow

    + *

    + * 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
    +   * 
    + * + *

    Revocation Checking

    + *

    + * 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 getTrustedCAs(HostKeyRepository knownHosts) { + HostKey[] hostKeys = knownHosts.getHostKey(); + return hostKeys == null ? new HashSet<>() + : Arrays.stream(hostKeys).filter(isKnownHostCaPublicKeyEntry).collect(Collectors.toSet()); + } + + /** + * Retrieves all revoked key entries from the repository. + *

    + * 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: + *

    + *
      + *
    • A {@code @cert-authority} or regular trusted entry, AND
    • + *
    • A {@code @revoked} entry
    • + *
    + *

    + * 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 getRevokedKeys(HostKeyRepository knownHosts) { + HostKey[] hostKeys = knownHosts.getHostKey(); + return hostKeys == null ? new HashSet<>() + : Arrays.stream(hostKeys).filter(isMarkedRevoked).collect(Collectors.toSet()); + } + + /** + * Checks if a given host key has been revoked. + *

    + * 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 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: + *

      + *
    1. Listed in the configured {@code ca_signature_algorithms}
    2. + *
    3. Not in the list of unavailable signature algorithms (as determined by + * {@code CheckSignatures})
    4. + *
    + *

    + * + * @param algorithm the CA signature algorithm to check + * @throws JSchException if the algorithm is not allowed or not available at runtime + */ + void checkCASignatureAlgorithm(String algorithm) throws JSchException { + String caSignatureAlgorithms = getConfig("ca_signature_algorithms"); + + if (caSignatureAlgorithms != null && !caSignatureAlgorithms.isEmpty()) { + // Check if algorithm is in the allowed list + String[] allowedAlgorithms = Util.split(caSignatureAlgorithms, ","); + boolean isAllowed = false; + for (String allowed : allowedAlgorithms) { + if (allowed.equals(algorithm)) { + isAllowed = true; + break; + } + } + + if (!isAllowed) { + throw new JSchException("CA signature algorithm '" + algorithm + + "' is not in the allowed ca_signature_algorithms list: " + caSignatureAlgorithms); + } + } + + // Check if the algorithm is in the cached unavailable signatures list. + // Copy volatile field to local variable to avoid race condition. + String[] unavailableSigs = not_available_shks; + if (unavailableSigs != null) { + for (String unavailable : unavailableSigs) { + if (unavailable.equals(algorithm)) { + throw new JSchException( + "CA signature algorithm '" + algorithm + "' is not available at runtime. " + + "This may be due to missing cryptographic provider support " + + "(e.g., Ed25519 on Java 8 without Bouncy Castle)."); + } + } + } + // If unavailableSigs is null, either all probed algorithms are available, + // or the user chose not to probe via CheckSignatures - respect that choice. + } + /** * Sets the identityRepository, which will be referred in the public key authentication. The * default value 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 identities, List loop3: while (it.hasNext()) { String ipkmethod = it.next(); it.remove(); - if (not_available_pks.contains(ipkmethod) && !(identity instanceof AgentIdentity)) { - if (session.getLogger().isEnabled(Logger.DEBUG)) { - session.getLogger().log(Logger.DEBUG, - ipkmethod + " not available for identity " + identity.getName()); - } + // Map certificate type to base algorithm for availability check + if (isAlgorithmUnavailable(ipkmethod, not_available_pks, identity, session)) { continue loop3; } @@ -272,11 +272,8 @@ private boolean _start(Session session, List identities, List loop4: while (it.hasNext() && session.auth_failures < session.max_auth_tries) { String pkmethodsuccess = it.next(); it.remove(); - if (not_available_pks.contains(pkmethodsuccess) && !(identity instanceof AgentIdentity)) { - if (session.getLogger().isEnabled(Logger.DEBUG)) { - session.getLogger().log(Logger.DEBUG, - pkmethodsuccess + " not available for identity " + identity.getName()); - } + // Map certificate type to base algorithm for availability check + if (isAlgorithmUnavailable(pkmethodsuccess, not_available_pks, identity, session)) { continue loop4; } @@ -379,6 +376,31 @@ private boolean _start(Session session, List identities, List return false; } + /** + * Checks if a public key algorithm is unavailable for a non-agent identity. + *

    + * 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 not_available_pks, + Identity identity, Session session) { + String baseAlgorithm = OpenSshCertificateKeyTypes.getBaseKeyType(pkmethod); + if (not_available_pks.contains(baseAlgorithm) && !(identity instanceof AgentIdentity)) { + if (session.getLogger().isEnabled(Logger.DEBUG)) { + session.getLogger().log(Logger.DEBUG, + pkmethod + " not available for identity " + identity.getName()); + } + return true; + } + return false; + } + private void decryptKey(Session session, Identity identity) throws JSchException { byte[] passphrase = null; int count = 5; diff --git a/src/main/java/com/jcraft/jsch/Util.java b/src/main/java/com/jcraft/jsch/Util.java index b7ef747d2..dd892a9cf 100644 --- a/src/main/java/com/jcraft/jsch/Util.java +++ b/src/main/java/com/jcraft/jsch/Util.java @@ -52,6 +52,10 @@ private static byte val(byte foo) { return 0; } + static byte[] fromBase64(byte[] buf) throws JSchException { + return fromBase64(buf, 0, buf.length); + } + static byte[] fromBase64(byte[] buf, int start, int length) throws JSchException { try { byte[] foo = new byte[length]; diff --git a/src/test/java/com/jcraft/jsch/HostCertificateIT.java b/src/test/java/com/jcraft/jsch/HostCertificateIT.java new file mode 100644 index 000000000..b2a88517f --- /dev/null +++ b/src/test/java/com/jcraft/jsch/HostCertificateIT.java @@ -0,0 +1,202 @@ +package com.jcraft.jsch; + +import com.github.valfirst.slf4jtest.LoggingEvent; +import com.github.valfirst.slf4jtest.TestLogger; +import com.github.valfirst.slf4jtest.TestLoggerFactory; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.images.builder.ImageFromDockerfile; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.util.Arrays; +import java.util.List; + +import static com.jcraft.jsch.ResourceUtil.getResourceFile; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Integration tests for SSH host certificate validation. + *

    + * 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: + *

      + *
    • Starts from a lightweight Alpine Linux image.
    • + *
    • Installs an OpenSSH server.
    • + *
    • Creates a test user ('luigi').
    • + *
    • Copies all necessary configuration, keys, and certificates from the test resources.
    • + *
    • Starts the SSH server via a custom entrypoint script.
    • + *
    + */ + @Container + private GenericContainer sshdContainer = new GenericContainer<>(new ImageFromDockerfile() + .withFileFromClasspath("sshd_config", CERTIFICATES_BASE_FOLDER + "/sshd_config") + .withFileFromClasspath("authorized_keys", + CERTIFICATES_BASE_FOLDER + "/user_keys/id_ecdsa_nistp521.pub") + .withFileFromClasspath("entrypoint.sh", CERTIFICATES_BASE_FOLDER + "/entrypoint.sh") + .withFileFromClasspath("ssh_host_rsa_key", CERTIFICATES_BASE_FOLDER + "/ssh_host_rsa_key") + .withFileFromClasspath("ssh_host_rsa_key-cert.pub", + CERTIFICATES_BASE_FOLDER + "/ssh_host_rsa_key-cert.pub") + .withFileFromClasspath("ssh_host_ecdsa_key", CERTIFICATES_BASE_FOLDER + "/ssh_host_ecdsa_key") + .withFileFromClasspath("ssh_host_ecdsa_key-cert.pub", + CERTIFICATES_BASE_FOLDER + "/ssh_host_ecdsa_key-cert.pub") + .withFileFromClasspath("ssh_host_ed25519_key", + CERTIFICATES_BASE_FOLDER + "/ssh_host_ed25519_key") + .withFileFromClasspath("ssh_host_ed25519_key-cert.pub", + CERTIFICATES_BASE_FOLDER + "/ssh_host_ed25519_key-cert.pub") + .withFileFromClasspath("Dockerfile", CERTIFICATES_BASE_FOLDER + "/Dockerfile")) + .withExposedPorts(22).waitingFor(Wait.forLogMessage(".*Server listening on :: port 22.*", 1)); + + /** + * Provides a stream of server host key algorithms to be used in parameterized tests. Each string + * corresponds to the {@code server_host_key} configuration option in JSch. + * + * @return An iterable of host key algorithm strings for test parameterization. + */ + public static List privateKeyParams() { + return Arrays.asList("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,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com"); + } + + /** + * Tests the successful connection scenario where the server's host certificate is signed by a CA + * that is trusted in the client's {@code known_hosts} file. This test is parameterized to run + * against different host key algorithms. + * + * @param algorithm The server host key algorithm to test, provided by + * {@link #privateKeyParams()}. + * @throws Exception if any error occurs during the test. + */ + @MethodSource("privateKeyParams") + @ParameterizedTest(name = "hostkey algorithm: {0}") + public void hostKeyTestHappyPath(String algorithm) throws Exception { + JSch ssh = new JSch(); + ssh.addIdentity( + getResourceFile(this.getClass(), CERTIFICATES_BASE_FOLDER + "/user_keys/id_ecdsa_nistp521"), + getResourceFile(this.getClass(), + CERTIFICATES_BASE_FOLDER + "/user_keys/id_ecdsa_nistp521.pub"), + null); + + ssh.setKnownHosts(getResourceFile(this.getClass(), "certificates/known_hosts")); + Session session = + ssh.getSession("root", sshdContainer.getHost(), sshdContainer.getFirstMappedPort()); + session.setConfig("enable_auth_none", "no"); + session.setConfig("StrictHostKeyChecking", "yes"); + session.setConfig("PreferredAuthentications", "publickey"); + session.setConfig("server_host_key", algorithm); + assertDoesNotThrow(() -> { + connectSftp(session); + }); + } + + /** + * Tests the failure scenario where the server's host certificate cannot be trusted. This test + * verifies that a {@link JSchHostKeyException} is thrown, as expected when + * {@code StrictHostKeyChecking} is enabled and the host key/certificate does not match any entry + * in the {@code known_hosts} file. + * + * @param algorithm The server host key algorithm to test, provided by + * {@link #privateKeyParams()}. + * @throws Exception if any error occurs during the test setup. + */ + @MethodSource("privateKeyParams") + @ParameterizedTest(name = "hostkey algorithm: {0}") + public void hostKeyTestNotTrustedCA(String algorithm) throws Exception { + JSch ssh = new JSch(); + ssh.addIdentity( + getResourceFile(this.getClass(), CERTIFICATES_BASE_FOLDER + "/user_keys/id_ecdsa_nistp521"), + getResourceFile(this.getClass(), + CERTIFICATES_BASE_FOLDER + "/user_keys/id_ecdsa_nistp521.pub"), + null); + + Session session = setup(ssh, algorithm); + assertThrows(JSchHostKeyException.class, () -> { + connectSftp(session); + }); + } + + /** + * Helper method to create and configure a JSch {@link Session} with common settings for the + * tests. + * + * @param ssh The JSch instance. + * @param algorithm The server host key algorithm to prefer. + * @return A configured {@link Session} object. + * @throws JSchException if there is an error creating the session. + */ + private Session setup(JSch ssh, String algorithm) throws JSchException { + Session session = + ssh.getSession("root", sshdContainer.getHost(), sshdContainer.getFirstMappedPort()); + session.setConfig("enable_auth_none", "no"); + session.setConfig("StrictHostKeyChecking", "yes"); + session.setConfig("PreferredAuthentications", "publickey"); + session.setConfig("server_host_key", algorithm); + return session; + } + + /** + * Establishes a session connection, opens an SFTP channel to verify connectivity, and then + * cleanly disconnects. If any exception occurs, it prints diagnostic information before + * re-throwing the exception. + * + * @param session The session to connect with. + * @throws JSchException if a JSch-specific error occurs. + */ + private void connectSftp(Session session) throws JSchException { + try { + session.setTimeout(TIMEOUT); + session.connect(); + ChannelSftp sftp = (ChannelSftp) session.openChannel("sftp"); + sftp.connect(TIMEOUT); + assertTrue(sftp.isConnected()); + sftp.disconnect(); + session.disconnect(); + } catch (Exception e) { + printInfo(); + throw e; + } + } + + /** + * A utility method for debugging. Prints all captured log events from both the JSch client and + * the mock SSH server to the console. + */ + private void printInfo() { + jschLogger.getAllLoggingEvents().stream().map(LoggingEvent::getFormattedMessage) + .forEach(System.out::println); + sshdLogger.getAllLoggingEvents().stream().map(LoggingEvent::getFormattedMessage) + .forEach(System.out::println); + } +} diff --git a/src/test/java/com/jcraft/jsch/HostKeyTest.java b/src/test/java/com/jcraft/jsch/HostKeyTest.java new file mode 100644 index 000000000..5862496ec --- /dev/null +++ b/src/test/java/com/jcraft/jsch/HostKeyTest.java @@ -0,0 +1,269 @@ +package com.jcraft.jsch; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +public class HostKeyTest { + + // Helper method to create a simple HostKey for testing + private HostKey createHostKey(String hostPattern) throws Exception { + // Create a dummy RSA key (just needs valid Base64 for the test) + String dummyKey = "AAAAB3NzaC1yc2EAAAADAQABAAABAQ=="; + byte[] keyBytes = Util.fromBase64(Util.str2byte(dummyKey), 0, dummyKey.length()); + return new HostKey(hostPattern, HostKey.SSHRSA, keyBytes); + } + + // ==================== Basic wildcard tests ==================== + + @Test + public void testIsWildcardMatched_exactMatch() throws Exception { + HostKey hostKey = createHostKey("example.com"); + assertTrue(hostKey.isWildcardMatched("example.com"), "Should match exact hostname"); + } + + @Test + public void testIsWildcardMatched_noMatch() throws Exception { + HostKey hostKey = createHostKey("example.com"); + assertFalse(hostKey.isWildcardMatched("different.com"), "Should not match different hostname"); + } + + @Test + public void testIsWildcardMatched_nullHostname() throws Exception { + HostKey hostKey = createHostKey("example.com"); + assertFalse(hostKey.isWildcardMatched(null), "Should return false for null hostname"); + } + + // ==================== Single asterisk (*) wildcard tests ==================== + + @Test + public void testIsWildcardMatched_asteriskPrefix() throws Exception { + HostKey hostKey = createHostKey("*.example.com"); + assertTrue(hostKey.isWildcardMatched("host.example.com"), + "Should match *.example.com with host.example.com"); + assertTrue(hostKey.isWildcardMatched("sub.example.com"), + "Should match *.example.com with sub.example.com"); + assertTrue(hostKey.isWildcardMatched("a.example.com"), + "Should match *.example.com with a.example.com"); + } + + @Test + public void testIsWildcardMatched_asteriskPrefixNoMatch() throws Exception { + HostKey hostKey = createHostKey("*.example.com"); + assertFalse(hostKey.isWildcardMatched("example.com"), + "Should not match *.example.com with example.com (no subdomain)"); + assertFalse(hostKey.isWildcardMatched("host.different.com"), + "Should not match *.example.com with host.different.com"); + } + + @Test + public void testIsWildcardMatched_asteriskSuffix() throws Exception { + HostKey hostKey = createHostKey("192.168.1.*"); + assertTrue(hostKey.isWildcardMatched("192.168.1.1"), + "Should match 192.168.1.* with 192.168.1.1"); + assertTrue(hostKey.isWildcardMatched("192.168.1.100"), + "Should match 192.168.1.* with 192.168.1.100"); + assertTrue(hostKey.isWildcardMatched("192.168.1.254"), + "Should match 192.168.1.* with 192.168.1.254"); + } + + @Test + public void testIsWildcardMatched_asteriskSuffixNoMatch() throws Exception { + HostKey hostKey = createHostKey("192.168.1.*"); + assertFalse(hostKey.isWildcardMatched("192.168.2.1"), + "Should not match 192.168.1.* with 192.168.2.1"); + assertFalse(hostKey.isWildcardMatched("192.168.1"), + "Should not match 192.168.1.* with 192.168.1 (incomplete)"); + } + + @Test + public void testIsWildcardMatched_asteriskMiddle() throws Exception { + HostKey hostKey = createHostKey("host-*.example.com"); + assertTrue(hostKey.isWildcardMatched("host-1.example.com"), + "Should match host-*.example.com with host-1.example.com"); + assertTrue(hostKey.isWildcardMatched("host-prod.example.com"), + "Should match host-*.example.com with host-prod.example.com"); + } + + @Test + public void testIsWildcardMatched_multipleAsterisks() throws Exception { + HostKey hostKey = createHostKey("*.*.example.com"); + assertTrue(hostKey.isWildcardMatched("sub.host.example.com"), + "Should match *.*.example.com with sub.host.example.com"); + assertTrue(hostKey.isWildcardMatched("a.b.example.com"), + "Should match *.*.example.com with a.b.example.com"); + } + + @Test + public void testIsWildcardMatched_asteriskMatchesEmpty() throws Exception { + HostKey hostKey = createHostKey("host*.example.com"); + assertTrue(hostKey.isWildcardMatched("host.example.com"), + "Should match host*.example.com with host.example.com (* matches empty string)"); + assertTrue(hostKey.isWildcardMatched("host123.example.com"), + "Should match host*.example.com with host123.example.com"); + } + + @Test + public void testIsWildcardMatched_asteriskOnly() throws Exception { + HostKey hostKey = createHostKey("*"); + assertTrue(hostKey.isWildcardMatched("anything.com"), "Should match * with anything.com"); + assertTrue(hostKey.isWildcardMatched("192.168.1.1"), "Should match * with 192.168.1.1"); + assertTrue(hostKey.isWildcardMatched("host"), "Should match * with host"); + } + + // ==================== Question mark (?) wildcard tests ==================== + + @Test + public void testIsWildcardMatched_questionMarkSingle() throws Exception { + HostKey hostKey = createHostKey("host?.example.com"); + assertTrue(hostKey.isWildcardMatched("host1.example.com"), + "Should match host?.example.com with host1.example.com"); + assertTrue(hostKey.isWildcardMatched("hosta.example.com"), + "Should match host?.example.com with hosta.example.com"); + assertTrue(hostKey.isWildcardMatched("host-.example.com"), + "Should match host?.example.com with host-.example.com"); + } + + @Test + public void testIsWildcardMatched_questionMarkNoMatch() throws Exception { + HostKey hostKey = createHostKey("host?.example.com"); + assertFalse(hostKey.isWildcardMatched("host.example.com"), + "Should not match host?.example.com with host.example.com (missing character)"); + assertFalse(hostKey.isWildcardMatched("host12.example.com"), + "Should not match host?.example.com with host12.example.com (too many characters)"); + } + + @Test + public void testIsWildcardMatched_multipleQuestionMarks() throws Exception { + HostKey hostKey = createHostKey("host-???.example.com"); + assertTrue(hostKey.isWildcardMatched("host-001.example.com"), + "Should match host-???.example.com with host-001.example.com"); + assertTrue(hostKey.isWildcardMatched("host-abc.example.com"), + "Should match host-???.example.com with host-abc.example.com"); + assertFalse(hostKey.isWildcardMatched("host-12.example.com"), + "Should not match host-???.example.com with host-12.example.com (too few characters)"); + } + + // ==================== Mixed wildcard tests ==================== + + @Test + public void testIsWildcardMatched_mixedWildcards() throws Exception { + HostKey hostKey = createHostKey("host-?-*.example.com"); + assertTrue(hostKey.isWildcardMatched("host-1-prod.example.com"), + "Should match host-?-*.example.com with host-1-prod.example.com"); + assertTrue(hostKey.isWildcardMatched("host-a-test.example.com"), + "Should match host-?-*.example.com with host-a-test.example.com"); + } + + // ==================== Comma-separated patterns ==================== + + @Test + public void testIsWildcardMatched_commaSeparatedFirstMatches() throws Exception { + HostKey hostKey = createHostKey("host1.com,host2.com,host3.com"); + assertTrue(hostKey.isWildcardMatched("host1.com"), + "Should match first pattern in comma-separated list"); + } + + @Test + public void testIsWildcardMatched_commaSeparatedMiddleMatches() throws Exception { + HostKey hostKey = createHostKey("host1.com,host2.com,host3.com"); + assertTrue(hostKey.isWildcardMatched("host2.com"), + "Should match middle pattern in comma-separated list"); + } + + @Test + public void testIsWildcardMatched_commaSeparatedLastMatches() throws Exception { + HostKey hostKey = createHostKey("host1.com,host2.com,host3.com"); + assertTrue(hostKey.isWildcardMatched("host3.com"), + "Should match last pattern in comma-separated list"); + } + + @Test + public void testIsWildcardMatched_commaSeparatedNoMatch() throws Exception { + HostKey hostKey = createHostKey("host1.com,host2.com,host3.com"); + assertFalse(hostKey.isWildcardMatched("host4.com"), + "Should not match when hostname doesn't match any pattern in list"); + } + + @Test + public void testIsWildcardMatched_commaSeparatedWithWildcards() throws Exception { + HostKey hostKey = createHostKey("*.prod.com,*.test.com,192.168.*"); + assertTrue(hostKey.isWildcardMatched("host.prod.com"), + "Should match *.prod.com in comma-separated wildcard list"); + assertTrue(hostKey.isWildcardMatched("server.test.com"), + "Should match *.test.com in comma-separated wildcard list"); + assertTrue(hostKey.isWildcardMatched("192.168.1.1"), + "Should match 192.168.* in comma-separated wildcard list"); + assertFalse(hostKey.isWildcardMatched("host.dev.com"), + "Should not match when hostname doesn't match any wildcard pattern"); + } + + @Test + public void testIsWildcardMatched_commaSeparatedWithSpaces() throws Exception { + HostKey hostKey = createHostKey("host1.com, host2.com , host3.com"); + assertTrue(hostKey.isWildcardMatched("host1.com"), "Should handle spaces after comma"); + assertTrue(hostKey.isWildcardMatched("host2.com"), "Should handle spaces around comma"); + assertTrue(hostKey.isWildcardMatched("host3.com"), "Should handle leading space in pattern"); + } + + // ==================== Edge cases ==================== + + @Test + public void testIsWildcardMatched_emptyPattern() throws Exception { + HostKey hostKey = createHostKey(""); + assertFalse(hostKey.isWildcardMatched("host.com"), "Should not match empty pattern"); + } + + @Test + public void testIsWildcardMatched_emptyHostname() throws Exception { + HostKey hostKey = createHostKey("host.com"); + assertFalse(hostKey.isWildcardMatched(""), "Should not match empty hostname"); + } + + @Test + public void testIsWildcardMatched_caseSensitive() throws Exception { + HostKey hostKey = createHostKey("Host.Example.COM"); + // OpenSSH wildcard matching is case-sensitive + assertFalse(hostKey.isWildcardMatched("host.example.com"), + "Wildcard matching should be case-sensitive"); + assertTrue(hostKey.isWildcardMatched("Host.Example.COM"), "Should match exact case"); + } + + @Test + public void testIsWildcardMatched_specialCharacters() throws Exception { + HostKey hostKey = createHostKey("host-1_2.example.com"); + assertTrue(hostKey.isWildcardMatched("host-1_2.example.com"), + "Should match special characters in hostname"); + } + + // ==================== Real-world scenarios ==================== + + @Test + public void testIsWildcardMatched_wildcardSubdomain() throws Exception { + HostKey hostKey = createHostKey("*.corp.example.com"); + assertTrue(hostKey.isWildcardMatched("server.corp.example.com"), + "Should match subdomain with wildcard"); + assertTrue(hostKey.isWildcardMatched("db.corp.example.com"), + "Should match different subdomain with wildcard"); + assertFalse(hostKey.isWildcardMatched("corp.example.com"), + "Should not match base domain without subdomain"); + } + + @Test + public void testIsWildcardMatched_ipv4Range() throws Exception { + HostKey hostKey = createHostKey("10.0.0.*"); + assertTrue(hostKey.isWildcardMatched("10.0.0.1"), "Should match IP in range"); + assertTrue(hostKey.isWildcardMatched("10.0.0.255"), "Should match last IP in range"); + assertFalse(hostKey.isWildcardMatched("10.0.1.1"), "Should not match IP outside range"); + } + + @Test + public void testIsWildcardMatched_hostnameWithPort() throws Exception { + HostKey hostKey = createHostKey("[*.example.com]:2222"); + assertTrue(hostKey.isWildcardMatched("[host.example.com]:2222"), + "Should match hostname with port and wildcard"); + assertFalse(hostKey.isWildcardMatched("[host.example.com]:22"), + "Should not match different port"); + } +} diff --git a/src/test/java/com/jcraft/jsch/JSchAddIdentityTest.java b/src/test/java/com/jcraft/jsch/JSchAddIdentityTest.java new file mode 100644 index 000000000..6661856c1 --- /dev/null +++ b/src/test/java/com/jcraft/jsch/JSchAddIdentityTest.java @@ -0,0 +1,136 @@ +package com.jcraft.jsch; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Vector; +import org.junit.jupiter.api.Test; + +public class JSchAddIdentityTest { + + private static final String CERTIFICATES_BASE = "src/test/resources/certificates"; + + /** + * Tests that addIdentity(prvkey, null, passphrase) auto-discovers the certificate file when a + * file named prvkey + "-cert.pub" exists. + */ + @Test + void addIdentity_withNullPubkey_shouldAutoDiscoverCertificate() throws Exception { + JSch jsch = new JSch(); + + // Private key file: root_ed25519_key + // Certificate file: root_ed25519_key-cert.pub (should be auto-discovered) + String prvkey = CERTIFICATES_BASE + "/ed25519/root_ed25519_key"; + + // Call with pubkey = null, should auto-discover the -cert.pub file + jsch.addIdentity(prvkey, null, null); + + // Verify that an identity was added + IdentityRepository repo = jsch.getIdentityRepository(); + Vector identities = repo.getIdentities(); + + assertNotNull(identities); + assertEquals(1, identities.size()); + + Identity identity = identities.get(0); + // Verify it's a certificate-aware identity by checking the algorithm name + String algName = identity.getAlgName(); + assertTrue(algName.contains("-cert-v01@openssh.com"), + "Expected certificate algorithm, got: " + algName); + } + + /** + * Tests that addIdentity(prvkey, null, passphrase) falls back to regular behavior when no + * certificate file exists (only .pub file). + */ + @Test + void addIdentity_withNullPubkey_shouldFallbackWhenNoCertificate() throws Exception { + JSch jsch = new JSch(); + + // Use a key that has only .pub file, not -cert.pub + // For this test, we need a key without a certificate + // We'll use a temporary approach - create the scenario or use existing non-cert key + String prvkey = CERTIFICATES_BASE + "/host/user_keys/id_ecdsa_nistp521"; + + // This key has .pub but not -cert.pub, should fall back to IdentityFile + jsch.addIdentity(prvkey, null, null); + + // Verify that an identity was added + IdentityRepository repo = jsch.getIdentityRepository(); + Vector identities = repo.getIdentities(); + + assertNotNull(identities); + assertEquals(1, identities.size()); + + Identity identity = identities.get(0); + // Should NOT be a certificate algorithm + String algName = identity.getAlgName(); + assertFalse(algName.contains("-cert-v01@openssh.com"), + "Expected non-certificate algorithm, got: " + algName); + } + + /** + * Tests that addIdentity with explicit pubkey path still works correctly for certificate files. + */ + @Test + void addIdentity_withExplicitCertPubkey_shouldLoadCertificate() throws Exception { + JSch jsch = new JSch(); + + String prvkey = CERTIFICATES_BASE + "/ed25519/root_ed25519_key"; + String pubkey = CERTIFICATES_BASE + "/ed25519/root_ed25519_key-cert.pub"; + + jsch.addIdentity(prvkey, pubkey, null); + + IdentityRepository repo = jsch.getIdentityRepository(); + Vector identities = repo.getIdentities(); + + assertNotNull(identities); + assertEquals(1, identities.size()); + + Identity identity = identities.get(0); + String algName = identity.getAlgName(); + assertEquals("ssh-ed25519-cert-v01@openssh.com", algName); + } + + /** + * Tests that addIdentity throws an exception when an explicitly provided pubkey file does not + * exist. This matches KeyPair.load() behavior. + */ + @Test + void addIdentity_withExplicitNonExistentPubkey_shouldThrowException() { + JSch jsch = new JSch(); + + String prvkey = CERTIFICATES_BASE + "/ed25519/root_ed25519_key"; + String pubkey = CERTIFICATES_BASE + "/ed25519/non_existent_file.pub"; + + assertThrows(JSchException.class, () -> jsch.addIdentity(prvkey, pubkey, null), + "Should throw JSchException when explicitly provided pubkey file does not exist"); + } + + /** + * Tests that addIdentity does NOT throw an exception when auto-discovered certificate file does + * not exist. This matches KeyPair.load() behavior where auto-discovery failures are silently + * ignored. + */ + @Test + void addIdentity_withNullPubkeyAndNoCertFile_shouldNotThrowException() throws Exception { + JSch jsch = new JSch(); + + // This key has only .pub file, no -cert.pub file + // Auto-discovery of -cert.pub should fail silently + String prvkey = CERTIFICATES_BASE + "/host/user_keys/id_ecdsa_nistp521"; + + // Should not throw - auto-discovery failure is silent + jsch.addIdentity(prvkey, null, null); + + // Verify that an identity was still added (via IdentityFile fallback) + IdentityRepository repo = jsch.getIdentityRepository(); + Vector identities = repo.getIdentities(); + + assertNotNull(identities); + assertEquals(1, identities.size()); + } +} diff --git a/src/test/java/com/jcraft/jsch/OpenSshCertificateAwareIdentityFileTest.java b/src/test/java/com/jcraft/jsch/OpenSshCertificateAwareIdentityFileTest.java new file mode 100644 index 000000000..d30d5bdda --- /dev/null +++ b/src/test/java/com/jcraft/jsch/OpenSshCertificateAwareIdentityFileTest.java @@ -0,0 +1,321 @@ +package com.jcraft.jsch; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +public class OpenSshCertificateAwareIdentityFileTest { + + @Test + public void testIsOpenSshCertificate_File_nullInput() { + assertFalse(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(null)); + } + + @Test + public void testIsOpenSshCertificate_File_emptyInput() { + byte[] empty = new byte[0]; + assertFalse(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(empty)); + } + + @Test + public void testIsOpenSshCertificate_sshRsaCertFile() { + String certType = "ssh-rsa-cert-v01@openssh.com"; + byte[] input = certType.getBytes(StandardCharsets.UTF_8); + assertTrue(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_sshRsaCertWithDataFile() { + String certLine = "ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20="; + byte[] input = certLine.getBytes(StandardCharsets.UTF_8); + assertTrue(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_sshRsaCertWithDataAndCommentFile() { + String certLine = + "ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20= user@host"; + byte[] input = certLine.getBytes(StandardCharsets.UTF_8); + assertTrue(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_sshDssCertFile() { + String certType = "ssh-dss-cert-v01@openssh.com"; + byte[] input = certType.getBytes(StandardCharsets.UTF_8); + assertTrue(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_sshDssCertWithDataFile() { + String certLine = + "ssh-dss-cert-v01@openssh.com AAAAHHNzaC1kc3MtY2VydC12MDFAb3BlbnNzaC5jb20= user@host"; + byte[] input = certLine.getBytes(StandardCharsets.UTF_8); + assertTrue(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_File_ecdsaSha2Nistp256Cert() { + String certType = "ecdsa-sha2-nistp256-cert-v01@openssh.com"; + byte[] input = certType.getBytes(StandardCharsets.UTF_8); + assertTrue(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_File_ecdsaSha2Nistp256CertWithData() { + String certLine = + "ecdsa-sha2-nistp256-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20= user@host"; + byte[] input = certLine.getBytes(StandardCharsets.UTF_8); + assertTrue(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_File_ecdsaSha2Nistp384Cert() { + String certType = "ecdsa-sha2-nistp384-cert-v01@openssh.com"; + byte[] input = certType.getBytes(StandardCharsets.UTF_8); + assertTrue(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_File_ecdsaSha2Nistp384CertWithData() { + String certLine = + "ecdsa-sha2-nistp384-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHAzODQtY2VydC12MDFAb3BlbnNzaC5jb20= user@host"; + byte[] input = certLine.getBytes(StandardCharsets.UTF_8); + assertTrue(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_File_ecdsaSha2Nistp521Cert() { + String certType = "ecdsa-sha2-nistp521-cert-v01@openssh.com"; + byte[] input = certType.getBytes(StandardCharsets.UTF_8); + assertTrue(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_File_ecdsaSha2Nistp521CertWithData() { + String certLine = + "ecdsa-sha2-nistp521-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHA1MjEtY2VydC12MDFAb3BlbnNzaC5jb20= user@host"; + byte[] input = certLine.getBytes(StandardCharsets.UTF_8); + assertTrue(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_sshEd25519CertFile() { + String certType = "ssh-ed25519-cert-v01@openssh.com"; + byte[] input = certType.getBytes(StandardCharsets.UTF_8); + assertTrue(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_sshEd25519CertWithDataFile() { + String certLine = + "ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29t user@host"; + byte[] input = certLine.getBytes(StandardCharsets.UTF_8); + assertTrue(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_sshEd448CertFile() { + String certType = "ssh-ed448-cert-v01@openssh.com"; + byte[] input = certType.getBytes(StandardCharsets.UTF_8); + assertTrue(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_sshEd448CertWithDataFile() { + String certLine = + "ssh-ed448-cert-v01@openssh.com AAAAHnNzaC1lZDQ0OC1jZXJ0LXYwMUBvcGVuc3NoLmNvbQ== user@host"; + byte[] input = certLine.getBytes(StandardCharsets.UTF_8); + assertTrue(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_regularSshRsaKeyFile() { + String keyType = "ssh-rsa"; + byte[] input = keyType.getBytes(StandardCharsets.UTF_8); + assertFalse(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_regularSshRsaKeyWithDataFile() { + String keyLine = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC= user@host"; + byte[] input = keyLine.getBytes(StandardCharsets.UTF_8); + assertFalse(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_regularSshDssKeyFile() { + String keyType = "ssh-dss"; + byte[] input = keyType.getBytes(StandardCharsets.UTF_8); + assertFalse(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_File_regularEcdsaKey() { + String keyType = "ecdsa-sha2-nistp256"; + byte[] input = keyType.getBytes(StandardCharsets.UTF_8); + assertFalse(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_File_regularEd25519Key() { + String keyType = "ssh-ed25519"; + byte[] input = keyType.getBytes(StandardCharsets.UTF_8); + assertFalse(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_File_tooShort() { + String tooShort = "short"; + byte[] input = tooShort.getBytes(StandardCharsets.UTF_8); + assertFalse(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_File_wrongSuffix() { + String wrongSuffix = "ssh-rsa-cert-v02@openssh.com"; + byte[] input = wrongSuffix.getBytes(StandardCharsets.UTF_8); + assertFalse(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_File_wrongPrefix() { + String wrongPrefix = "ssh-abc-cert-v01@openssh.com"; + byte[] input = wrongPrefix.getBytes(StandardCharsets.UTF_8); + assertFalse(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_File_invalidAlgorithm() { + String invalid = "invalid-cert-v01@openssh.com"; + byte[] input = invalid.getBytes(StandardCharsets.UTF_8); + assertFalse(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_File_onlyWhitespace() { + String whitespace = " \t\n"; + byte[] input = whitespace.getBytes(StandardCharsets.UTF_8); + assertFalse(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_File_leadingWhitespace() { + String certLine = + " ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20="; + byte[] input = certLine.getBytes(StandardCharsets.UTF_8); + assertTrue(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_File_tabDelimiter() { + String certLine = "ssh-rsa-cert-v01@openssh.com\tAAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20="; + byte[] input = certLine.getBytes(StandardCharsets.UTF_8); + assertTrue(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_File_newlineDelimiter() { + String certLine = "ssh-rsa-cert-v01@openssh.com\nAAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20="; + byte[] input = certLine.getBytes(StandardCharsets.UTF_8); + assertTrue(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_File_carriageReturnDelimiter() { + String certLine = "ssh-rsa-cert-v01@openssh.com\rAAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20="; + byte[] input = certLine.getBytes(StandardCharsets.UTF_8); + assertTrue(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_File_partialMatch() { + String partial = "ssh-rsa-cert-v01@openssh.co"; + byte[] input = partial.getBytes(StandardCharsets.UTF_8); + assertFalse(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_File_extraCharactersInType() { + String extra = "ssh-rsax-cert-v01@openssh.com"; + byte[] input = extra.getBytes(StandardCharsets.UTF_8); + assertFalse(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_File_casesensitive() { + String uppercase = "SSH-RSA-CERT-V01@OPENSSH.COM"; + byte[] input = uppercase.getBytes(StandardCharsets.UTF_8); + assertFalse(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input), + "Certificate type matching should be case-sensitive"); + } + + @Test + public void testIsOpenSshCertificate_File_minimumLengthBoundary() { + // Exactly 27 bytes - one less than minimum (28) + char[] chars = new char[27]; + Arrays.fill(chars, 'a'); + String justUnderMinimum = new String(chars); + byte[] input = justUnderMinimum.getBytes(StandardCharsets.UTF_8); + assertFalse(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_sshRsaWrongLengthFile() { + // ssh-rsa-cert-v01@openssh.com should be exactly 28 chars + String tooLong = "ssh-rsax-cert-v01@openssh.com"; + byte[] input = tooLong.getBytes(StandardCharsets.UTF_8); + assertFalse(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_File_ecdsaWrongLength() { + // ecdsa cert types should be exactly 40 chars + String wrongLength = "ecdsa-sha2-nistp25-cert-v01@openssh.com"; // 39 chars + byte[] input = wrongLength.getBytes(StandardCharsets.UTF_8); + assertFalse(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_File_binaryData() { + // Binary data that might accidentally contain cert-like patterns + byte[] binary = new byte[] {0x00, 0x01, 0x02, 's', 's', 'h', '-', 'r', 's', 'a'}; + assertFalse(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(binary)); + } + + @Test + public void testIsOpenSshCertificate_File_utf8Characters() { + // Certificate type with UTF-8 characters (should fail) + String utf8 = "ssh-rsá-cert-v01@openssh.com"; + byte[] input = utf8.getBytes(StandardCharsets.UTF_8); + assertFalse(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_File_multilineWithCertOnSecondLine() { + // Simulate a file where the cert type is on the second line + String multiline = + "\nssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20="; + byte[] input = multiline.getBytes(StandardCharsets.UTF_8); + assertTrue(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input)); + } + + @Test + public void testIsOpenSshCertificate_File_onlyKeyTypeNoWhitespace() { + // All valid cert types without any trailing data or whitespace + String[] certTypes = {"ssh-rsa-cert-v01@openssh.com", "ssh-dss-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-ed25519-cert-v01@openssh.com", + "ssh-ed448-cert-v01@openssh.com"}; + + for (String certType : certTypes) { + byte[] input = certType.getBytes(StandardCharsets.UTF_8); + assertTrue(OpenSshCertificateAwareIdentityFile.isOpenSshCertificateFile(input), + "Should recognize valid certificate type: " + certType); + } + } +} diff --git a/src/test/java/com/jcraft/jsch/OpenSshCertificateBufferTest.java b/src/test/java/com/jcraft/jsch/OpenSshCertificateBufferTest.java new file mode 100644 index 000000000..62102abca --- /dev/null +++ b/src/test/java/com/jcraft/jsch/OpenSshCertificateBufferTest.java @@ -0,0 +1,205 @@ +package com.jcraft.jsch; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link OpenSshCertificateBuffer}. + * + *

    + * 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: + *

    + *
      + *
    • Valid length-prefixed byte arrays
    • + *
    • Empty data arrays
    • + *
    • Invalid data with negative length prefixes
    • + *
    • Invalid data where the length prefix exceeds available data
    • + *
    • Sequential reads from the buffer
    • + *
    + * + * @see OpenSshCertificateBuffer + */ +class OpenSshCertificateBufferTest { + + /** + * Helper method to create a buffer with a length-prefixed byte array. + * + *

    + * 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 criticalOptions = new HashMap<>(); + criticalOptions.put("force-command", "/bin/false"); + + OpenSshCertificate cert = + createValidHostCertificateBuilder().criticalOptions(criticalOptions).build(); + + // Verify that isEmpty correctly identifies non-empty critical options + assertTrue(!OpenSshCertificateUtil.isEmpty(cert.getCriticalOptions()), + "Critical options should not be empty"); + } + + /** + * Test that a certificate with multiple critical options is detected. + */ + @Test + public void testCheckHostCertificate_withMultipleCriticalOptions() { + Map criticalOptions = new HashMap<>(); + criticalOptions.put("force-command", "/bin/false"); + criticalOptions.put("source-address", "192.168.1.0/24"); + + OpenSshCertificate cert = + createValidHostCertificateBuilder().criticalOptions(criticalOptions).build(); + + assertEquals(2, cert.getCriticalOptions().size(), "Should have 2 critical options"); + } + + /** + * Test that a certificate with empty critical options is accepted. + */ + @Test + public void testCheckHostCertificate_withEmptyCriticalOptions() { + OpenSshCertificate cert = + createValidHostCertificateBuilder().criticalOptions(Collections.emptyMap()).build(); + + assertTrue(OpenSshCertificateUtil.isEmpty(cert.getCriticalOptions()), + "Critical options should be empty"); + } + + /** + * Test that a certificate with null critical options is accepted. + */ + @Test + public void testCheckHostCertificate_withNullCriticalOptions() { + OpenSshCertificate cert = createValidHostCertificateBuilder().criticalOptions(null).build(); + + assertTrue(OpenSshCertificateUtil.isEmpty(cert.getCriticalOptions()), + "Null critical options should be treated as empty"); + } + + // ==================== Tests for certificate type validation ==================== + + /** + * Test that isHostCertificate returns true for host certificate type. + */ + @Test + public void testIsHostCertificate_hostType() { + OpenSshCertificate cert = createValidHostCertificateBuilder().build(); + + assertTrue(cert.isHostCertificate(), "Should be identified as host certificate"); + } + + /** + * Test that isHostCertificate returns false for user certificate type. + */ + @Test + public void testIsHostCertificate_userType() { + OpenSshCertificate cert = createValidUserCertificateBuilder().build(); + + assertTrue(!cert.isHostCertificate(), "Should not be identified as host certificate"); + } + + // ==================== Tests for principal validation ==================== + + /** + * Test that empty principals list is detected. + */ + @Test + public void testPrincipals_emptyList() { + OpenSshCertificate cert = + createValidHostCertificateBuilder().principals(Collections.emptyList()).build(); + + assertTrue(cert.getPrincipals().isEmpty(), "Principals should be empty"); + } + + /** + * Test that null principals list is handled. + */ + @Test + public void testPrincipals_nullList() { + OpenSshCertificate cert = createValidHostCertificateBuilder().principals(null).build(); + + assertTrue(cert.getPrincipals() == null, "Principals should be null"); + } + + /** + * Test that multiple principals are correctly stored. + */ + @Test + public void testPrincipals_multipleValues() { + OpenSshCertificate cert = createValidHostCertificateBuilder() + .principals(Arrays.asList("host1.example.com", "host2.example.com", "10.0.0.1")).build(); + + assertEquals(3, cert.getPrincipals().size(), "Should have 3 principals"); + assertTrue(cert.getPrincipals().contains("host1.example.com"), "Should contain host1"); + assertTrue(cert.getPrincipals().contains("host2.example.com"), "Should contain host2"); + assertTrue(cert.getPrincipals().contains("10.0.0.1"), "Should contain IP"); + } + + // ==================== Helper methods ==================== + + // Dummy byte arrays for required fields in tests + private static final byte[] DUMMY_NONCE = new byte[] {1, 2, 3, 4, 5, 6, 7, 8}; + private static final byte[] DUMMY_PUBLIC_KEY = + new byte[] {0, 0, 0, 7, 's', 's', 'h', '-', 'r', 's', 'a', 0, 0, 0, 1, 35, 0, 0, 0, 1, 0}; + private static final byte[] DUMMY_SIGNATURE_KEY = + new byte[] {0, 0, 0, 7, 's', 's', 'h', '-', 'r', 's', 'a', 0, 0, 0, 1, 35, 0, 0, 0, 1, 0}; + private static final byte[] DUMMY_SIGNATURE = + new byte[] {0, 0, 0, 7, 's', 's', 'h', '-', 'r', 's', 'a', 0, 0, 0, 4, 1, 2, 3, 4}; + private static final byte[] DUMMY_MESSAGE = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + + /** + * Creates a builder pre-configured with valid host certificate defaults. + */ + private OpenSshCertificate.Builder createValidHostCertificateBuilder() { + long now = java.time.Instant.now().getEpochSecond(); + return new OpenSshCertificate.Builder().keyType("ssh-rsa-cert-v01@openssh.com") + .nonce(DUMMY_NONCE).certificatePublicKey(DUMMY_PUBLIC_KEY) + .type(OpenSshCertificate.SSH2_CERT_TYPE_HOST).id("test-certificate") + .principals(Arrays.asList("localhost")).validAfter(now - 3600).validBefore(now + 3600) + .criticalOptions(Collections.emptyMap()).extensions(Collections.emptyMap()) + .signatureKey(DUMMY_SIGNATURE_KEY).signature(DUMMY_SIGNATURE).message(DUMMY_MESSAGE); + } + + /** + * Creates a builder pre-configured with valid user certificate defaults. + */ + private OpenSshCertificate.Builder createValidUserCertificateBuilder() { + long now = java.time.Instant.now().getEpochSecond(); + return new OpenSshCertificate.Builder().keyType("ssh-rsa-cert-v01@openssh.com") + .nonce(DUMMY_NONCE).certificatePublicKey(DUMMY_PUBLIC_KEY) + .type(OpenSshCertificate.SSH2_CERT_TYPE_USER).id("test-certificate") + .principals(Arrays.asList("testuser")).validAfter(now - 3600).validBefore(now + 3600) + .criticalOptions(Collections.emptyMap()).extensions(Collections.emptyMap()) + .signatureKey(DUMMY_SIGNATURE_KEY).signature(DUMMY_SIGNATURE).message(DUMMY_MESSAGE); + } +} diff --git a/src/test/java/com/jcraft/jsch/OpenSshCertificateKeyTypesTest.java b/src/test/java/com/jcraft/jsch/OpenSshCertificateKeyTypesTest.java new file mode 100644 index 000000000..82eebf971 --- /dev/null +++ b/src/test/java/com/jcraft/jsch/OpenSshCertificateKeyTypesTest.java @@ -0,0 +1,146 @@ +package com.jcraft.jsch; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * Unit tests for {@link OpenSshCertificateKeyTypes}. + */ +public class OpenSshCertificateKeyTypesTest { + + // ==================== Tests for isCertificateKeyType ==================== + + @ParameterizedTest + @ValueSource(strings = {"ssh-rsa-cert-v01@openssh.com", "ssh-dss-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-ed25519-cert-v01@openssh.com", + "ssh-ed448-cert-v01@openssh.com", "rsa-sha2-256-cert-v01@openssh.com", + "rsa-sha2-512-cert-v01@openssh.com"}) + public void testIsCertificateKeyType_validCertTypes(String keyType) { + assertTrue(OpenSshCertificateKeyTypes.isCertificateKeyType(keyType), + "Should recognize " + keyType + " as certificate type"); + } + + @ParameterizedTest + @ValueSource(strings = {"ssh-rsa", "ssh-dss", "ecdsa-sha2-nistp256", "ssh-ed25519", "ssh-ed448", + "rsa-sha2-256", "rsa-sha2-512", "unknown-cert-v01@openssh.com", + "ssh-rsa-cert-v02@openssh.com"}) + public void testIsCertificateKeyType_nonCertTypes(String keyType) { + assertFalse(OpenSshCertificateKeyTypes.isCertificateKeyType(keyType), + "Should not recognize " + keyType + " as certificate type"); + } + + @Test + public void testIsCertificateKeyType_null() { + assertFalse(OpenSshCertificateKeyTypes.isCertificateKeyType(null), + "Should return false for null"); + } + + // ==================== Tests for getBaseKeyType ==================== + + @ParameterizedTest + @CsvSource({"ssh-rsa-cert-v01@openssh.com,ssh-rsa", "ssh-dss-cert-v01@openssh.com,ssh-dss", + "ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp256", + "ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp384", + "ecdsa-sha2-nistp521-cert-v01@openssh.com,ecdsa-sha2-nistp521", + "ssh-ed25519-cert-v01@openssh.com,ssh-ed25519", "ssh-ed448-cert-v01@openssh.com,ssh-ed448", + "rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-256", + "rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-512"}) + public void testGetBaseKeyType_certTypes(String certType, String expectedBaseType) { + assertEquals(expectedBaseType, OpenSshCertificateKeyTypes.getBaseKeyType(certType)); + } + + @ParameterizedTest + @ValueSource(strings = {"ssh-rsa", "ssh-dss", "ecdsa-sha2-nistp256", "ssh-ed25519"}) + public void testGetBaseKeyType_nonCertTypes(String keyType) { + assertEquals(keyType, OpenSshCertificateKeyTypes.getBaseKeyType(keyType), + "Should return original key type for non-certificate types"); + } + + @Test + public void testGetBaseKeyType_null() { + assertNull(OpenSshCertificateKeyTypes.getBaseKeyType(null), + "Should return null for null input"); + } + + @ParameterizedTest + @NullAndEmptySource + public void testGetBaseKeyType_nullOrEmpty(String keyType) { + assertNull(OpenSshCertificateKeyTypes.getBaseKeyType(keyType), + "Should return null for null or empty input"); + } + + @ParameterizedTest + @ValueSource(strings = {" ", "\t", "\n"}) + public void testGetBaseKeyType_blankReturnsOriginal(String keyType) { + assertEquals(keyType, OpenSshCertificateKeyTypes.getBaseKeyType(keyType), + "Should return original string for blank (non-empty) input"); + } + + // ==================== Tests for constants ==================== + + @Test + public void testConstantsHaveCorrectValues() { + assertEquals("ssh-rsa-cert-v01@openssh.com", OpenSshCertificateKeyTypes.SSH_RSA_CERT_V01); + assertEquals("ssh-dss-cert-v01@openssh.com", OpenSshCertificateKeyTypes.SSH_DSS_CERT_V01); + assertEquals("ecdsa-sha2-nistp256-cert-v01@openssh.com", + OpenSshCertificateKeyTypes.ECDSA_SHA2_NISTP256_CERT_V01); + assertEquals("ecdsa-sha2-nistp384-cert-v01@openssh.com", + OpenSshCertificateKeyTypes.ECDSA_SHA2_NISTP384_CERT_V01); + assertEquals("ecdsa-sha2-nistp521-cert-v01@openssh.com", + OpenSshCertificateKeyTypes.ECDSA_SHA2_NISTP521_CERT_V01); + assertEquals("ssh-ed25519-cert-v01@openssh.com", + OpenSshCertificateKeyTypes.SSH_ED25519_CERT_V01); + assertEquals("ssh-ed448-cert-v01@openssh.com", OpenSshCertificateKeyTypes.SSH_ED448_CERT_V01); + assertEquals("-cert-v01@openssh.com", OpenSshCertificateKeyTypes.CERT_SUFFIX); + } + + // ==================== Tests for getCertificateKeyType ==================== + + @ParameterizedTest + @CsvSource({"ssh-rsa,ssh-rsa-cert-v01@openssh.com", "ssh-dss,ssh-dss-cert-v01@openssh.com", + "ecdsa-sha2-nistp256,ecdsa-sha2-nistp256-cert-v01@openssh.com", + "ecdsa-sha2-nistp384,ecdsa-sha2-nistp384-cert-v01@openssh.com", + "ecdsa-sha2-nistp521,ecdsa-sha2-nistp521-cert-v01@openssh.com", + "ssh-ed25519,ssh-ed25519-cert-v01@openssh.com", "ssh-ed448,ssh-ed448-cert-v01@openssh.com", + "rsa-sha2-256,rsa-sha2-256-cert-v01@openssh.com", + "rsa-sha2-512,rsa-sha2-512-cert-v01@openssh.com"}) + public void testGetCertificateKeyType_knownAlgorithms(String baseAlg, String expectedCertType) { + assertEquals(expectedCertType, OpenSshCertificateKeyTypes.getCertificateKeyType(baseAlg)); + } + + @ParameterizedTest + @ValueSource(strings = {"unknown-algorithm", "ssh-rsa-cert-v01@openssh.com", "aes256-ctr"}) + public void testGetCertificateKeyType_unknownAlgorithms(String algorithm) { + assertNull(OpenSshCertificateKeyTypes.getCertificateKeyType(algorithm), + "Should return null for unknown or already-certificate algorithms"); + } + + @Test + public void testGetCertificateKeyType_null() { + assertNull(OpenSshCertificateKeyTypes.getCertificateKeyType(null), + "Should return null for null input"); + } + + @Test + public void testGetCertificateKeyType_roundTrip() { + // Verify that getCertificateKeyType and getBaseKeyType are inverse operations + String[] baseAlgorithms = + {"ssh-rsa", "ssh-dss", "ecdsa-sha2-nistp256", "ssh-ed25519", "ssh-ed448"}; + for (String base : baseAlgorithms) { + String certType = OpenSshCertificateKeyTypes.getCertificateKeyType(base); + assertNotNull(certType, "Certificate type should not be null for " + base); + String recoveredBase = OpenSshCertificateKeyTypes.getBaseKeyType(certType); + assertEquals(base, recoveredBase, "Round-trip should recover original base algorithm"); + } + } +} diff --git a/src/test/java/com/jcraft/jsch/OpenSshCertificateUtilTest.java b/src/test/java/com/jcraft/jsch/OpenSshCertificateUtilTest.java new file mode 100644 index 000000000..d3dc8c4ed --- /dev/null +++ b/src/test/java/com/jcraft/jsch/OpenSshCertificateUtilTest.java @@ -0,0 +1,920 @@ +package com.jcraft.jsch; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Test; + +public class OpenSshCertificateUtilTest { + + @Test + public void testExtractSpaceDelimitedString_nullInput() { + assertNull(OpenSshCertificateUtil.extractSpaceDelimitedString(null, 0)); + } + + @Test + public void testExtractSpaceDelimitedString_emptyInput() { + byte[] empty = new byte[0]; + assertNull(OpenSshCertificateUtil.extractSpaceDelimitedString(empty, 0)); + } + + @Test + public void testExtractSpaceDelimitedString_singleField() { + byte[] input = "field1".getBytes(StandardCharsets.UTF_8); + byte[] expected = "field1".getBytes(StandardCharsets.UTF_8); + assertArrayEquals(expected, OpenSshCertificateUtil.extractSpaceDelimitedString(input, 0)); + } + + @Test + public void testExtractSpaceDelimitedString_multipleFieldsExtractFirst() { + byte[] input = "field1 field2 field3".getBytes(StandardCharsets.UTF_8); + byte[] expected = "field1".getBytes(StandardCharsets.UTF_8); + assertArrayEquals(expected, OpenSshCertificateUtil.extractSpaceDelimitedString(input, 0)); + } + + @Test + public void testExtractSpaceDelimitedString_multipleFieldsExtractMiddle() { + byte[] input = "field1 field2 field3".getBytes(StandardCharsets.UTF_8); + byte[] expected = "field2".getBytes(StandardCharsets.UTF_8); + assertArrayEquals(expected, OpenSshCertificateUtil.extractSpaceDelimitedString(input, 1)); + } + + @Test + public void testExtractSpaceDelimitedString_multipleFieldsExtractLast() { + byte[] input = "field1 field2 field3".getBytes(StandardCharsets.UTF_8); + byte[] expected = "field3".getBytes(StandardCharsets.UTF_8); + assertArrayEquals(expected, OpenSshCertificateUtil.extractSpaceDelimitedString(input, 2)); + } + + @Test + public void testExtractSpaceDelimitedString_indexOutOfBounds() { + byte[] input = "field1 field2".getBytes(StandardCharsets.UTF_8); + assertNull(OpenSshCertificateUtil.extractSpaceDelimitedString(input, 5)); + } + + @Test + public void testExtractSpaceDelimitedString_negativeIndex() { + byte[] input = "field1 field2".getBytes(StandardCharsets.UTF_8); + assertNull(OpenSshCertificateUtil.extractSpaceDelimitedString(input, -1)); + } + + @Test + public void testExtractSpaceDelimitedString_leadingWhitespace() { + byte[] input = " field1 field2".getBytes(StandardCharsets.UTF_8); + byte[] expected = "field1".getBytes(StandardCharsets.UTF_8); + assertArrayEquals(expected, OpenSshCertificateUtil.extractSpaceDelimitedString(input, 0)); + } + + @Test + public void testExtractSpaceDelimitedString_trailingWhitespace() { + byte[] input = "field1 field2 ".getBytes(StandardCharsets.UTF_8); + byte[] expected = "field2".getBytes(StandardCharsets.UTF_8); + assertArrayEquals(expected, OpenSshCertificateUtil.extractSpaceDelimitedString(input, 1)); + } + + @Test + public void testExtractSpaceDelimitedString_multipleSpaces() { + byte[] input = "field1 field2 field3".getBytes(StandardCharsets.UTF_8); + byte[] expected = "field2".getBytes(StandardCharsets.UTF_8); + assertArrayEquals(expected, OpenSshCertificateUtil.extractSpaceDelimitedString(input, 1)); + } + + @Test + public void testExtractSpaceDelimitedString_tabDelimiter() { + byte[] input = "field1\tfield2\tfield3".getBytes(StandardCharsets.UTF_8); + byte[] expected = "field2".getBytes(StandardCharsets.UTF_8); + assertArrayEquals(expected, OpenSshCertificateUtil.extractSpaceDelimitedString(input, 1)); + } + + @Test + public void testExtractSpaceDelimitedString_mixedWhitespace() { + byte[] input = "field1 \t field2\t \tfield3".getBytes(StandardCharsets.UTF_8); + byte[] expected = "field2".getBytes(StandardCharsets.UTF_8); + assertArrayEquals(expected, OpenSshCertificateUtil.extractSpaceDelimitedString(input, 1)); + } + + @Test + public void testExtractSpaceDelimitedString_newlineDelimiter() { + byte[] input = "field1\nfield2\nfield3".getBytes(StandardCharsets.UTF_8); + byte[] expected = "field2".getBytes(StandardCharsets.UTF_8); + assertArrayEquals(expected, OpenSshCertificateUtil.extractSpaceDelimitedString(input, 1)); + } + + @Test + public void testExtractSpaceDelimitedString_carriageReturnDelimiter() { + byte[] input = "field1\rfield2\rfield3".getBytes(StandardCharsets.UTF_8); + byte[] expected = "field2".getBytes(StandardCharsets.UTF_8); + assertArrayEquals(expected, OpenSshCertificateUtil.extractSpaceDelimitedString(input, 1)); + } + + @Test + public void testExtractSpaceDelimitedString_mixedLineEndings() { + byte[] input = "field1\r\nfield2\n\rfield3".getBytes(StandardCharsets.UTF_8); + byte[] expected = "field2".getBytes(StandardCharsets.UTF_8); + assertArrayEquals(expected, OpenSshCertificateUtil.extractSpaceDelimitedString(input, 1)); + } + + @Test + public void testExtractSpaceDelimitedString_onlyWhitespace() { + byte[] input = " \t\n\r ".getBytes(StandardCharsets.UTF_8); + assertNull(OpenSshCertificateUtil.extractSpaceDelimitedString(input, 0)); + } + + @Test + public void testExtractSpaceDelimitedString_singleFieldWithWhitespace() { + byte[] input = " \t field1 \n\r ".getBytes(StandardCharsets.UTF_8); + byte[] expected = "field1".getBytes(StandardCharsets.UTF_8); + assertArrayEquals(expected, OpenSshCertificateUtil.extractSpaceDelimitedString(input, 0)); + } + + @Test + public void testExtractSpaceDelimitedString_realWorldCertificate() { + // Simulating a typical OpenSSH certificate line format + String certLine = + "ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20= user@host"; + byte[] input = certLine.getBytes(StandardCharsets.UTF_8); + + byte[] expectedKeyType = "ssh-rsa-cert-v01@openssh.com".getBytes(StandardCharsets.UTF_8); + byte[] expectedKeyData = + "AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20=".getBytes(StandardCharsets.UTF_8); + byte[] expectedComment = "user@host".getBytes(StandardCharsets.UTF_8); + + assertArrayEquals(expectedKeyType, + OpenSshCertificateUtil.extractSpaceDelimitedString(input, 0)); + assertArrayEquals(expectedKeyData, + OpenSshCertificateUtil.extractSpaceDelimitedString(input, 1)); + assertArrayEquals(expectedComment, + OpenSshCertificateUtil.extractSpaceDelimitedString(input, 2)); + } + + @Test + public void testExtractSpaceDelimitedString_lastFieldNoTrailingWhitespace() { + byte[] input = "field1 field2".getBytes(StandardCharsets.UTF_8); + byte[] expected = "field2".getBytes(StandardCharsets.UTF_8); + assertArrayEquals(expected, OpenSshCertificateUtil.extractSpaceDelimitedString(input, 1)); + } + + @Test + public void testExtractSpaceDelimitedString_twoFields() { + byte[] input = "key data".getBytes(StandardCharsets.UTF_8); + byte[] expectedFirst = "key".getBytes(StandardCharsets.UTF_8); + byte[] expectedSecond = "data".getBytes(StandardCharsets.UTF_8); + + assertArrayEquals(expectedFirst, OpenSshCertificateUtil.extractSpaceDelimitedString(input, 0)); + assertArrayEquals(expectedSecond, OpenSshCertificateUtil.extractSpaceDelimitedString(input, 1)); + assertNull(OpenSshCertificateUtil.extractSpaceDelimitedString(input, 2)); + } + + @Test + public void testExtractSpaceDelimitedString_specialCharactersInFields() { + byte[] input = "field-1 field_2 field.3@domain".getBytes(StandardCharsets.UTF_8); + byte[] expected = "field.3@domain".getBytes(StandardCharsets.UTF_8); + assertArrayEquals(expected, OpenSshCertificateUtil.extractSpaceDelimitedString(input, 2)); + } + + // ==================== Tests for isCertificateSignedByTrustedCA ==================== + + /** + * Test that isCertificateSignedByTrustedCA returns true when a matching, non-revoked CA is found. + */ + @Test + public void testIsCertificateSignedByTrustedCA_trustedCAFound() throws Exception { + // Setup + JSch jsch = new JSch(); + KnownHosts knownHosts = new KnownHosts(jsch); + String host = "public.example.com"; + String base64CaPublicKey = "AAAAB3NzaC1yc2EAAAADAQABAAABAQ=="; + + // Create a matching CA host key + byte[] keyBytes = + Util.fromBase64(Util.str2byte(base64CaPublicKey), 0, base64CaPublicKey.length()); + HostKey caHostKey = + new HostKey("@cert-authority", "*.example.com", HostKey.SSHRSA, keyBytes, null); + + knownHosts.add(caHostKey, null); + + // Execute + boolean result = + OpenSshCertificateUtil.isCertificateSignedByTrustedCA(knownHosts, host, keyBytes); + + // Verify + assertTrue(result, "Should return true when trusted CA is found"); + } + + /** + * Test that isCertificateSignedByTrustedCA returns false when the CA public key doesn't match. + */ + @Test + public void testIsCertificateSignedByTrustedCA_caKeyMismatch() throws Exception { + // Setup + JSch jsch = new JSch(); + KnownHosts knownHosts = new KnownHosts(jsch); + String host = "example.com"; + String base64CaPublicKey = "AAAAB3NzaC1yc2EAAAADAQABAAABAQ=="; + String differentCaKey = "AAAAB3NzaC1yc2EAAAADAQABAAABDIFFERENT=="; + + // Create a CA with different key + byte[] differentKeyBytes = + Util.fromBase64(Util.str2byte(differentCaKey), 0, differentCaKey.length()); + HostKey caHostKey = + new HostKey("@cert-authority", "*.example.com", HostKey.SSHRSA, differentKeyBytes, null); + + knownHosts.add(caHostKey, null); + + // Execute - pass the original (non-matching) key bytes + byte[] caPublicKeyBytes = + Util.fromBase64(Util.str2byte(base64CaPublicKey), 0, base64CaPublicKey.length()); + boolean result = + OpenSshCertificateUtil.isCertificateSignedByTrustedCA(knownHosts, host, caPublicKeyBytes); + + // Verify + assertFalse(result, "Should return false when CA key doesn't match"); + } + + /** + * Test that isCertificateSignedByTrustedCA returns false when the CA is revoked. + */ + @Test + public void testIsCertificateSignedByTrustedCA_caIsRevoked() throws Exception { + // Setup + JSch jsch = new JSch(); + KnownHosts knownHosts = new KnownHosts(jsch); + String host = "example.com"; + String base64CaPublicKey = "AAAAB3NzaC1yc2EAAAADAQABAAABAQ=="; + + byte[] keyBytes = + Util.fromBase64(Util.str2byte(base64CaPublicKey), 0, base64CaPublicKey.length()); + + // Create a @cert-authority entry + HostKey caHostKey = + new HostKey("@cert-authority", "*.example.com", HostKey.SSHRSA, keyBytes, null); + + // Create a @revoked entry with the same key + HostKey revokedHostKey = + new HostKey("@revoked", "*.example.com", HostKey.SSHRSA, keyBytes, null); + + knownHosts.add(caHostKey, null); + knownHosts.add(revokedHostKey, null); + + // Execute + boolean result = + OpenSshCertificateUtil.isCertificateSignedByTrustedCA(knownHosts, host, keyBytes); + + // Verify + assertFalse(result, "Should return false when CA is revoked (fail-closed security)"); + } + + /** + * Test that isCertificateSignedByTrustedCA returns false when host pattern doesn't match. + */ + @Test + public void testIsCertificateSignedByTrustedCA_hostPatternMismatch() throws Exception { + // Setup + JSch jsch = new JSch(); + KnownHosts knownHosts = new KnownHosts(jsch); + String host = "different.com"; + String base64CaPublicKey = "AAAAB3NzaC1yc2EAAAADAQABAAABAQ=="; + + byte[] keyBytes = + Util.fromBase64(Util.str2byte(base64CaPublicKey), 0, base64CaPublicKey.length()); + HostKey caHostKey = + new HostKey("@cert-authority", "*.example.com", HostKey.SSHRSA, keyBytes, null); + + knownHosts.add(caHostKey, null); + + // Execute + boolean result = + OpenSshCertificateUtil.isCertificateSignedByTrustedCA(knownHosts, host, keyBytes); + + // Verify + assertFalse(result, "Should return false when host pattern doesn't match"); + } + + /** + * Test that isCertificateSignedByTrustedCA returns false when repository is empty. + */ + @Test + public void testIsCertificateSignedByTrustedCA_emptyRepository() throws JSchException { + // Setup + JSch jsch = new JSch(); + KnownHosts knownHosts = new KnownHosts(jsch); + String host = "example.com"; + String base64CaPublicKey = "AAAAB3NzaC1yc2EAAAADAQABAAABAQ=="; + + byte[] caPublicKeyBytes = + Util.fromBase64(Util.str2byte(base64CaPublicKey), 0, base64CaPublicKey.length()); + + // Execute + boolean result = + OpenSshCertificateUtil.isCertificateSignedByTrustedCA(knownHosts, host, caPublicKeyBytes); + + // Verify + assertFalse(result, "Should return false when repository is empty"); + } + + /** + * Test that isCertificateSignedByTrustedCA succeeds when there are multiple CAs and one matches. + */ + @Test + public void testIsCertificateSignedByTrustedCA_multipleCAsOneMatches() throws Exception { + // Setup + JSch jsch = new JSch(); + KnownHosts knownHosts = new KnownHosts(jsch); + String host = "this.example.com"; + String base64CaPublicKey = "AAAAB3NzaC1yc2EAAAADAQABAAABAQ=="; + String differentCaKey = "AAAAB3NzaC1yc2EAAAADAQABAAABDIFFERENT=="; + + byte[] matchingKeyBytes = + Util.fromBase64(Util.str2byte(base64CaPublicKey), 0, base64CaPublicKey.length()); + byte[] differentKeyBytes = + Util.fromBase64(Util.str2byte(differentCaKey), 0, differentCaKey.length()); + + // Create multiple CA entries - one matches, one doesn't + HostKey caHostKey1 = + new HostKey("@cert-authority", "*.test.com", HostKey.SSHRSA, differentKeyBytes, null); + HostKey caHostKey2 = + new HostKey("@cert-authority", "*.example.com", HostKey.SSHRSA, matchingKeyBytes, null); + + knownHosts.add(caHostKey1, null); + knownHosts.add(caHostKey2, null); + + // Execute + boolean result = + OpenSshCertificateUtil.isCertificateSignedByTrustedCA(knownHosts, host, matchingKeyBytes); + + // Verify + assertTrue(result, "Should return true when one of multiple CAs matches (anyMatch behavior)"); + } + + /** + * Test that isCertificateSignedByTrustedCA ignores non-@cert-authority entries. + */ + @Test + public void testIsCertificateSignedByTrustedCA_ignoresNonCaEntries() throws Exception { + // Setup + JSch jsch = new JSch(); + KnownHosts knownHosts = new KnownHosts(jsch); + String host = "example.com"; + String base64CaPublicKey = "AAAAB3NzaC1yc2EAAAADAQABAAABAQ=="; + + byte[] keyBytes = + Util.fromBase64(Util.str2byte(base64CaPublicKey), 0, base64CaPublicKey.length()); + + // Create regular host key (no @cert-authority marker) + HostKey regularHostKey = new HostKey("", "example.com", HostKey.SSHRSA, keyBytes, null); + + // Create @revoked entry + HostKey revokedHostKey = new HostKey("@revoked", "example.com", HostKey.SSHRSA, keyBytes, null); + + knownHosts.add(regularHostKey, null); + knownHosts.add(revokedHostKey, null); + + // Execute + boolean result = + OpenSshCertificateUtil.isCertificateSignedByTrustedCA(knownHosts, host, keyBytes); + + // Verify + assertFalse(result, "Should ignore entries without @cert-authority marker"); + } + + /** + * Test that isCertificateSignedByTrustedCA handles wildcard host patterns correctly. + */ + @Test + public void testIsCertificateSignedByTrustedCA_wildcardHostPattern() throws Exception { + // Setup + JSch jsch = new JSch(); + KnownHosts knownHosts = new KnownHosts(jsch); + String host = "sub.example.com"; + String base64CaPublicKey = "AAAAB3NzaC1yc2EAAAADAQABAAABAQ=="; + + byte[] keyBytes = + Util.fromBase64(Util.str2byte(base64CaPublicKey), 0, base64CaPublicKey.length()); + + // Create CA with wildcard pattern + HostKey caHostKey = + new HostKey("@cert-authority", "*.example.com", HostKey.SSHRSA, keyBytes, null); + + knownHosts.add(caHostKey, null); + + // Execute + boolean result = + OpenSshCertificateUtil.isCertificateSignedByTrustedCA(knownHosts, host, keyBytes); + + // Verify + assertTrue(result, "Should match wildcard host pattern *.example.com with sub.example.com"); + } + + /** + * Test that isCertificateSignedByTrustedCA handles different key types (Ed25519). + */ + @Test + public void testIsCertificateSignedByTrustedCA_ed25519KeyType() throws Exception { + // Setup + JSch jsch = new JSch(); + KnownHosts knownHosts = new KnownHosts(jsch); + String host = "example.com"; + String base64CaPublicKey = "AAAAC3NzaC1lZDI1NTE5AAAAI=="; + + byte[] keyBytes = + Util.fromBase64(Util.str2byte(base64CaPublicKey), 0, base64CaPublicKey.length()); + + HostKey caHostKey = + new HostKey("@cert-authority", "*example.com", HostKey.ED25519, keyBytes, null); + + knownHosts.add(caHostKey, null); + + // Execute + boolean result = + OpenSshCertificateUtil.isCertificateSignedByTrustedCA(knownHosts, host, keyBytes); + + // Verify + assertTrue(result, "Should work with Ed25519 key type"); + } + + /** + * Test that isCertificateSignedByTrustedCA correctly handles the scenario where a CA exists but + * with a null key field. + */ + @Test + public void testIsCertificateSignedByTrustedCA_caWithNullKey() throws Exception { + // Setup + JSch jsch = new JSch(); + KnownHosts knownHosts = new KnownHosts(jsch); + String host = "example.com"; + String base64CaPublicKey = "AAAAB3NzaC1yc2EAAAADAQABAAABAQ=="; + + byte[] caPublicKeyBytes = + Util.fromBase64(Util.str2byte(base64CaPublicKey), 0, base64CaPublicKey.length()); + + // Create CA with null key (malformed entry) + HostKey caHostKeyWithNullKey = + new HostKey("@cert-authority", "*.example.com", HostKey.SSHRSA, null, null); + + knownHosts.add(caHostKeyWithNullKey, null); + + // Execute + boolean result = + OpenSshCertificateUtil.isCertificateSignedByTrustedCA(knownHosts, host, caPublicKeyBytes); + + // Verify + assertFalse(result, "Should return false when CA has null key (fail-closed security)"); + } + + /** + * Test complex scenario: multiple CAs, some revoked, some with different keys, one valid match. + */ + @Test + public void testIsCertificateSignedByTrustedCA_complexScenario() throws Exception { + // Setup + JSch jsch = new JSch(); + KnownHosts knownHosts = new KnownHosts(jsch); + String host = "prod.example.com"; + String validCaKey = "AAAAB3NzaC1yc2EVALIDKEY=="; + String revokedCaKey = "AAAAB3NzaC1yc2EREVOKEDKEY=="; + String differentCaKey = "AAAAB3NzaC1yc2EDIFFERENTKEY=="; + + byte[] validKeyBytes = Util.fromBase64(Util.str2byte(validCaKey), 0, validCaKey.length()); + byte[] revokedKeyBytes = Util.fromBase64(Util.str2byte(revokedCaKey), 0, revokedCaKey.length()); + byte[] differentKeyBytes = + Util.fromBase64(Util.str2byte(differentCaKey), 0, differentCaKey.length()); + + // Create multiple entries + HostKey validCa = + new HostKey("@cert-authority", "*.example.com", HostKey.SSHRSA, validKeyBytes, null); + HostKey revokedCa = + new HostKey("@cert-authority", "*.example.com", HostKey.SSHRSA, revokedKeyBytes, null); + HostKey revokedMarker = + new HostKey("@revoked", "*.example.com", HostKey.SSHRSA, revokedKeyBytes, null); + HostKey differentCa = + new HostKey("@cert-authority", "*.test.com", HostKey.SSHRSA, differentKeyBytes, null); + HostKey regularHost = new HostKey("", "prod.example.com", HostKey.SSHRSA, validKeyBytes, null); + + knownHosts.add(differentCa, null); + knownHosts.add(revokedCa, null); + knownHosts.add(revokedMarker, null); + knownHosts.add(validCa, null); + knownHosts.add(regularHost, null); + + // Execute + boolean result = + OpenSshCertificateUtil.isCertificateSignedByTrustedCA(knownHosts, host, validKeyBytes); + + // Verify + assertTrue(result, + "Should find the valid CA among multiple entries, ignoring revoked/different/null entries"); + } + + // ==================== Tests for filterUnavailableCertTypes ==================== + + /** + * Test that filterUnavailableCertTypes only removes ssh-rsa-cert when ssh-rsa is unavailable, + * leaving rsa-sha2-256-cert and rsa-sha2-512-cert available. + */ + @Test + public void testFilterUnavailableCertTypes_sshRsaUnavailable_shouldOnlyRemoveSshRsaCert() { + // Setup: server_host_key proposal with all RSA cert types + String serverHostKey = + "ssh-ed25519-cert-v01@openssh.com,ssh-rsa-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com"; + + // Only ssh-rsa (SHA1) is unavailable + String[] unavailableSignatures = {"ssh-rsa"}; + + // Execute + String result = + OpenSshCertificateUtil.filterUnavailableCertTypes(serverHostKey, unavailableSignatures); + + // Verify: ssh-rsa-cert removed, but SHA2 variants remain + assertTrue(result.contains("ssh-ed25519-cert-v01@openssh.com"), + "ssh-ed25519-cert should remain"); + assertFalse(result.contains("ssh-rsa-cert-v01@openssh.com"), "ssh-rsa-cert should be removed"); + assertTrue(result.contains("rsa-sha2-256-cert-v01@openssh.com"), + "rsa-sha2-256-cert should remain when only ssh-rsa is unavailable"); + assertTrue(result.contains("rsa-sha2-512-cert-v01@openssh.com"), + "rsa-sha2-512-cert should remain when only ssh-rsa is unavailable"); + } + + /** + * Test that filterUnavailableCertTypes removes rsa-sha2-256-cert when rsa-sha2-256 is + * unavailable. + */ + @Test + public void testFilterUnavailableCertTypes_rsaSha2256Unavailable_shouldRemoveOnlyThatCert() { + String serverHostKey = + "ssh-rsa-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com"; + + String[] unavailableSignatures = {"rsa-sha2-256"}; + + String result = + OpenSshCertificateUtil.filterUnavailableCertTypes(serverHostKey, unavailableSignatures); + + assertTrue(result.contains("ssh-rsa-cert-v01@openssh.com"), "ssh-rsa-cert should remain"); + assertFalse(result.contains("rsa-sha2-256-cert-v01@openssh.com"), + "rsa-sha2-256-cert should be removed"); + assertTrue(result.contains("rsa-sha2-512-cert-v01@openssh.com"), + "rsa-sha2-512-cert should remain"); + } + + /** + * Test that filterUnavailableCertTypes removes all RSA certs when all RSA algorithms are + * unavailable. + */ + @Test + public void testFilterUnavailableCertTypes_allRsaUnavailable_shouldRemoveAllRsaCerts() { + String serverHostKey = + "ssh-ed25519-cert-v01@openssh.com,ssh-rsa-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com"; + + String[] unavailableSignatures = {"ssh-rsa", "rsa-sha2-256", "rsa-sha2-512"}; + + String result = + OpenSshCertificateUtil.filterUnavailableCertTypes(serverHostKey, unavailableSignatures); + + assertTrue(result.contains("ssh-ed25519-cert-v01@openssh.com"), + "ssh-ed25519-cert should remain"); + assertFalse(result.contains("ssh-rsa-cert-v01@openssh.com"), "ssh-rsa-cert should be removed"); + assertFalse(result.contains("rsa-sha2-256-cert-v01@openssh.com"), + "rsa-sha2-256-cert should be removed"); + assertFalse(result.contains("rsa-sha2-512-cert-v01@openssh.com"), + "rsa-sha2-512-cert should be removed"); + } + + /** + * Test that filterUnavailableCertTypes returns serverHostKey unchanged when unavailableSignatures + * is null. + */ + @Test + public void testFilterUnavailableCertTypes_nullUnavailable_shouldReturnUnchanged() { + String serverHostKey = "ssh-ed25519-cert-v01@openssh.com,ssh-rsa-cert-v01@openssh.com"; + + String result = OpenSshCertificateUtil.filterUnavailableCertTypes(serverHostKey, null); + + assertEquals(serverHostKey, result); + } + + /** + * Test that filterUnavailableCertTypes returns serverHostKey unchanged when unavailableSignatures + * is empty. + */ + @Test + public void testFilterUnavailableCertTypes_emptyUnavailable_shouldReturnUnchanged() { + String serverHostKey = "ssh-ed25519-cert-v01@openssh.com,ssh-rsa-cert-v01@openssh.com"; + + String result = + OpenSshCertificateUtil.filterUnavailableCertTypes(serverHostKey, new String[] {}); + + assertEquals(serverHostKey, result); + } + + // ==================== Tests for isValidNow (expired/not-yet-valid certificates) + // ==================== + + /** + * Test that isValidNow returns false for an expired certificate. + */ + @Test + public void testIsValidNow_expiredCertificate() { + long now = 10000L; + OpenSshCertificate cert = createValidCertificateBuilder().validAfter(now - 7200) // Valid from + // 2 hours ago + .validBefore(now - 3600) // Expired 1 hour ago + .build(); + + assertFalse(OpenSshCertificateUtil.isValidNow(cert, now), "Certificate should be expired"); + } + + /** + * Test that isValidNow returns false for a certificate not yet valid. + */ + @Test + public void testIsValidNow_notYetValidCertificate() { + long now = 10000L; + OpenSshCertificate cert = createValidCertificateBuilder().validAfter(now + 3600) // Valid from + // 1 hour in + // the future + .validBefore(now + 7200) // Expires 2 hours in the future + .build(); + + assertFalse(OpenSshCertificateUtil.isValidNow(cert, now), + "Certificate should not be valid yet"); + } + + /** + * Test that isValidNow returns true for a currently valid certificate. + */ + @Test + public void testIsValidNow_currentlyValidCertificate() { + long now = 10000L; + OpenSshCertificate cert = createValidCertificateBuilder().validAfter(now - 3600) // Valid from + // 1 hour ago + .validBefore(now + 3600) // Expires 1 hour from now + .build(); + + assertTrue(OpenSshCertificateUtil.isValidNow(cert, now), "Certificate should be valid"); + } + + /** + * Test that isValidNow handles boundary condition: validAfter equals current time. + */ + @Test + public void testIsValidNow_validAfterEqualsNow() { + long now = 10000L; + OpenSshCertificate cert = createValidCertificateBuilder().validAfter(now) // Valid from now + .validBefore(now + 3600) // Expires 1 hour from now + .build(); + + assertTrue(OpenSshCertificateUtil.isValidNow(cert, now), + "Certificate should be valid when validAfter equals now"); + } + + /** + * Test that isValidNow handles boundary condition: validBefore equals current time. + */ + @Test + public void testIsValidNow_validBeforeEqualsNow() { + long now = 10000L; + OpenSshCertificate cert = createValidCertificateBuilder().validAfter(now - 3600) // Valid from + // 1 hour ago + .validBefore(now) // Expires now + .build(); + + assertFalse(OpenSshCertificateUtil.isValidNow(cert, now), + "Certificate should be expired when validBefore equals now"); + } + + /** + * Test isValidNow with maximum validity (forever valid certificate). + */ + @Test + public void testIsValidNow_foreverValidCertificate() { + long now = 10000L; + OpenSshCertificate cert = + createValidCertificateBuilder().validAfter(OpenSshCertificate.MIN_VALIDITY) // From epoch + .validBefore(OpenSshCertificate.MAX_VALIDITY) // Forever (max unsigned long) + .build(); + + assertTrue(OpenSshCertificateUtil.isValidNow(cert, now), + "Certificate with max validity should be valid"); + } + + // ==================== Tests for serial number edge cases ==================== + + /** + * Test certificate with maximum unsigned long serial number. + */ + @Test + public void testCertificate_maxSerialNumber() { + // Maximum unsigned 64-bit value: 0xFFFFFFFFFFFFFFFF + long maxSerial = 0xFFFF_FFFF_FFFF_FFFFL; + OpenSshCertificate cert = createValidCertificateBuilder().serial(maxSerial).build(); + + assertEquals(maxSerial, cert.getSerial(), "Should handle maximum serial number"); + } + + /** + * Test certificate with zero serial number. + */ + @Test + public void testCertificate_zeroSerialNumber() { + OpenSshCertificate cert = createValidCertificateBuilder().serial(0L).build(); + + assertEquals(0L, cert.getSerial(), "Should handle zero serial number"); + } + + /** + * Test certificate with serial number that appears negative when treated as signed long. + */ + @Test + public void testCertificate_largeSerialNumberAppearsNegative() { + // This value is negative when treated as signed long, but valid as unsigned + long largeSerial = 0x8000_0000_0000_0001L; // -9223372036854775807 as signed + OpenSshCertificate cert = createValidCertificateBuilder().serial(largeSerial).build(); + + assertEquals(largeSerial, cert.getSerial(), "Should handle large serial that appears negative"); + // Verify unsigned comparison works + assertTrue(Long.compareUnsigned(largeSerial, 0L) > 0, + "Serial should be positive when compared unsigned"); + } + + // ==================== Tests for toDateString edge cases ==================== + + /** + * Test toDateString with negative timestamp (represents infinity). + */ + @Test + public void testToDateString_negativeTimestamp() { + String result = OpenSshCertificateUtil.toDateString(-1L); + assertEquals("infinity", result, "Negative timestamp should return 'infinity'"); + } + + /** + * Test toDateString with minimum negative timestamp. + */ + @Test + public void testToDateString_minLongValue() { + String result = OpenSshCertificateUtil.toDateString(Long.MIN_VALUE); + assertEquals("infinity", result, "Long.MIN_VALUE should return 'infinity'"); + } + + /** + * Test toDateString with zero timestamp (epoch). + */ + @Test + public void testToDateString_zeroTimestamp() { + String result = OpenSshCertificateUtil.toDateString(0L); + // Should return a date string for epoch (Jan 1, 1970) + assertFalse(result.equals("infinity"), "Zero timestamp should not return 'infinity'"); + assertTrue(result.contains("1970"), "Zero timestamp should represent 1970"); + } + + /** + * Test toDateString with positive timestamp. + */ + @Test + public void testToDateString_positiveTimestamp() { + // 1704067200 = Jan 1, 2024 00:00:00 UTC + String result = OpenSshCertificateUtil.toDateString(1704067200L); + assertFalse(result.equals("infinity"), "Positive timestamp should not return 'infinity'"); + assertTrue(result.contains("2024"), "Timestamp should represent year 2024"); + } + + /** + * Test toDateString with MAX_VALIDITY (max unsigned long treated as signed). + */ + @Test + public void testToDateString_maxValidity() { + // MAX_VALIDITY is 0xFFFFFFFFFFFFFFFF which is -1 as signed long + String result = OpenSshCertificateUtil.toDateString(OpenSshCertificate.MAX_VALIDITY); + assertEquals("infinity", result, "MAX_VALIDITY should return 'infinity'"); + } + + // ==================== Helper methods ==================== + + // Dummy byte arrays for required fields in tests + private static final byte[] DUMMY_NONCE = new byte[] {1, 2, 3, 4, 5, 6, 7, 8}; + private static final byte[] DUMMY_PUBLIC_KEY = + new byte[] {0, 0, 0, 7, 's', 's', 'h', '-', 'r', 's', 'a', 0, 0, 0, 1, 35, 0, 0, 0, 1, 0}; + private static final byte[] DUMMY_SIGNATURE_KEY = + new byte[] {0, 0, 0, 7, 's', 's', 'h', '-', 'r', 's', 'a', 0, 0, 0, 1, 35, 0, 0, 0, 1, 0}; + private static final byte[] DUMMY_SIGNATURE = + new byte[] {0, 0, 0, 7, 's', 's', 'h', '-', 'r', 's', 'a', 0, 0, 0, 4, 1, 2, 3, 4}; + private static final byte[] DUMMY_MESSAGE = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + + /** + * Creates a builder pre-configured with valid host certificate defaults. + */ + private OpenSshCertificate.Builder createValidCertificateBuilder() { + return new OpenSshCertificate.Builder().keyType("ssh-rsa-cert-v01@openssh.com") + .nonce(DUMMY_NONCE).certificatePublicKey(DUMMY_PUBLIC_KEY) + .type(OpenSshCertificate.SSH2_CERT_TYPE_HOST).id("test-certificate") + .signatureKey(DUMMY_SIGNATURE_KEY).signature(DUMMY_SIGNATURE).message(DUMMY_MESSAGE); + } + + // ==================== Tests for hasBeenRevoked ==================== + + /** + * Test that hasBeenRevoked returns true when key is null (fail-closed). + */ + @Test + public void testHasBeenRevoked_nullKey_returnsTrue() throws Exception { + JSch jsch = new JSch(); + KnownHosts knownHosts = new KnownHosts(jsch); + + boolean result = OpenSshCertificateUtil.hasBeenRevoked(knownHosts, null); + + assertTrue(result, "Should return true for null key (fail-closed)"); + } + + /** + * Test that hasBeenRevoked returns false when key is not in revoked list. + */ + @Test + public void testHasBeenRevoked_keyNotRevoked_returnsFalse() throws Exception { + JSch jsch = new JSch(); + KnownHosts knownHosts = new KnownHosts(jsch); + String base64Key = "AAAAB3NzaC1yc2EAAAADAQABAAABAQ=="; + byte[] keyBytes = Util.fromBase64(Util.str2byte(base64Key), 0, base64Key.length()); + HostKey hostKey = new HostKey("example.com", HostKey.SSHRSA, keyBytes); + + boolean result = OpenSshCertificateUtil.hasBeenRevoked(knownHosts, hostKey); + + assertFalse(result, "Should return false when key is not revoked"); + } + + /** + * Test that hasBeenRevoked returns true when key is in revoked list. + */ + @Test + public void testHasBeenRevoked_keyIsRevoked_returnsTrue() throws Exception { + JSch jsch = new JSch(); + KnownHosts knownHosts = new KnownHosts(jsch); + String base64Key = "AAAAB3NzaC1yc2EAAAADAQABAAABAQ=="; + byte[] keyBytes = Util.fromBase64(Util.str2byte(base64Key), 0, base64Key.length()); + + // Create regular host key + HostKey hostKey = new HostKey("example.com", HostKey.SSHRSA, keyBytes); + + // Create revoked entry with same key + HostKey revokedKey = new HostKey("@revoked", "example.com", HostKey.SSHRSA, keyBytes, null); + knownHosts.add(revokedKey, null); + + boolean result = OpenSshCertificateUtil.hasBeenRevoked(knownHosts, hostKey); + + assertTrue(result, "Should return true when key is revoked"); + } + + /** + * Test that hasBeenRevoked handles revoked entries with null getKey() gracefully. + */ + @Test + public void testHasBeenRevoked_revokedEntryWithNullKey_handledGracefully() throws Exception { + JSch jsch = new JSch(); + KnownHosts knownHosts = new KnownHosts(jsch); + String base64Key = "AAAAB3NzaC1yc2EAAAADAQABAAABAQ=="; + byte[] keyBytes = Util.fromBase64(Util.str2byte(base64Key), 0, base64Key.length()); + + // Create a host key with valid key + HostKey hostKey = new HostKey("example.com", HostKey.SSHRSA, keyBytes); + + // The method should not throw NPE even if revoked entries have null keys + // This test verifies the fix for problem 6 + boolean result = OpenSshCertificateUtil.hasBeenRevoked(knownHosts, hostKey); + + assertFalse(result, "Should handle empty revoked list gracefully"); + } + + // ==================== Tests for getRawKeyType with centralized constants ==================== + + /** + * Test that getRawKeyType correctly extracts base key type from certificate types. + */ + @Test + public void testGetRawKeyType_certificateType_returnsBaseType() { + assertEquals("ssh-rsa", OpenSshCertificateUtil.getRawKeyType("ssh-rsa-cert-v01@openssh.com")); + assertEquals("ssh-ed25519", + OpenSshCertificateUtil.getRawKeyType("ssh-ed25519-cert-v01@openssh.com")); + assertEquals("ecdsa-sha2-nistp256", + OpenSshCertificateUtil.getRawKeyType("ecdsa-sha2-nistp256-cert-v01@openssh.com")); + } + + /** + * Test that getRawKeyType returns original for non-certificate types. + */ + @Test + public void testGetRawKeyType_nonCertificateType_returnsOriginal() { + assertEquals("ssh-rsa", OpenSshCertificateUtil.getRawKeyType("ssh-rsa")); + assertEquals("ssh-ed25519", OpenSshCertificateUtil.getRawKeyType("ssh-ed25519")); + } + + /** + * Test that getRawKeyType handles null and empty strings. + */ + @Test + public void testGetRawKeyType_nullOrEmpty_returnsNull() { + assertNull(OpenSshCertificateUtil.getRawKeyType(null)); + assertNull(OpenSshCertificateUtil.getRawKeyType("")); + } +} diff --git a/src/test/java/com/jcraft/jsch/SessionTest.java b/src/test/java/com/jcraft/jsch/SessionTest.java index ebc12d984..1764721c2 100644 --- a/src/test/java/com/jcraft/jsch/SessionTest.java +++ b/src/test/java/com/jcraft/jsch/SessionTest.java @@ -1,13 +1,18 @@ 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.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import com.jcraft.jsch.JSchTest.TestLogger; import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.MethodSource; class SessionTest { @@ -84,4 +89,74 @@ void checkLoggerFunctionality() throws Exception { JSch.setLogger(orgLogger); } } + + // ==================== Tests for CASignatureAlgorithms ==================== + + /** + * Tests that the default ca_signature_algorithms config matches OpenSSH 8.2+ defaults (excludes + * ssh-rsa/SHA-1). + */ + @Test + void testDefaultCASignatureAlgorithms() { + String defaultCaSigAlgs = JSch.getConfig("ca_signature_algorithms"); + assertTrue(defaultCaSigAlgs.contains("ssh-ed25519"), "Default should include ssh-ed25519"); + assertTrue(defaultCaSigAlgs.contains("ecdsa-sha2-nistp256"), + "Default should include ecdsa-sha2-nistp256"); + assertTrue(defaultCaSigAlgs.contains("rsa-sha2-256"), "Default should include rsa-sha2-256"); + assertTrue(defaultCaSigAlgs.contains("rsa-sha2-512"), "Default should include rsa-sha2-512"); + // ssh-rsa (SHA-1) should NOT be in the default list (OpenSSH 8.2+ behavior) + assertTrue( + !defaultCaSigAlgs.contains(",ssh-rsa,") && !defaultCaSigAlgs.endsWith(",ssh-rsa") + && !defaultCaSigAlgs.startsWith("ssh-rsa,") && !defaultCaSigAlgs.equals("ssh-rsa"), + "Default should NOT include ssh-rsa (SHA-1)"); + } + + /** + * Tests that checkCASignatureAlgorithm passes for algorithms in the allowed list. + */ + @Test + void testCheckCASignatureAlgorithm_allowedAlgorithm() throws JSchException { + Session session = new Session(jsch, null, null, 0); + // ecdsa-sha2-nistp256 is in the default ca_signature_algorithms + assertDoesNotThrow(() -> session.checkCASignatureAlgorithm("ecdsa-sha2-nistp256"), + "Algorithm in the allowed list should not throw"); + } + + /** + * Tests that checkCASignatureAlgorithm throws for algorithms not in the allowed list. + */ + @Test + void testCheckCASignatureAlgorithm_disallowedAlgorithm() throws JSchException { + Session session = new Session(jsch, null, null, 0); + // ssh-rsa (SHA-1) is NOT in the default ca_signature_algorithms + JSchException exception = + assertThrows(JSchException.class, () -> session.checkCASignatureAlgorithm("ssh-rsa"), + "Algorithm not in the allowed list should throw JSchException"); + assertTrue(exception.getMessage().contains("not in the allowed ca_signature_algorithms"), + "Exception message should indicate algorithm not allowed"); + } + + /** + * Tests that checkCASignatureAlgorithm can be configured to allow ssh-rsa. + */ + @Test + void testCheckCASignatureAlgorithm_customConfig() throws JSchException { + Session session = new Session(jsch, null, null, 0); + // Configure to allow ssh-rsa + session.setConfig("ca_signature_algorithms", "ssh-rsa,rsa-sha2-256,rsa-sha2-512"); + assertDoesNotThrow(() -> session.checkCASignatureAlgorithm("ssh-rsa"), + "ssh-rsa should be allowed when explicitly configured"); + } + + /** + * Tests that checkCASignatureAlgorithm allows all algorithms when config is empty. + */ + @Test + void testCheckCASignatureAlgorithm_emptyConfig() throws JSchException { + Session session = new Session(jsch, null, null, 0); + session.setConfig("ca_signature_algorithms", ""); + // With empty config, no restriction on which algorithms are allowed + assertDoesNotThrow(() -> session.checkCASignatureAlgorithm("ecdsa-sha2-nistp256"), + "Algorithm should be allowed when config is empty"); + } } diff --git a/src/test/java/com/jcraft/jsch/UserCertAuthIT.java b/src/test/java/com/jcraft/jsch/UserCertAuthIT.java new file mode 100644 index 000000000..d25c927be --- /dev/null +++ b/src/test/java/com/jcraft/jsch/UserCertAuthIT.java @@ -0,0 +1,187 @@ +package com.jcraft.jsch; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; +import java.util.Locale; +import com.github.valfirst.slf4jtest.LoggingEvent; +import com.github.valfirst.slf4jtest.TestLogger; +import com.github.valfirst.slf4jtest.TestLoggerFactory; +import org.apache.commons.codec.digest.DigestUtils; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.images.builder.ImageFromDockerfile; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + + +/** + * Integrated Test (IT) class to verify the JSch public key authentication mechanism using **OpenSSH + * user certificates** against a Testcontainers-managed SSHD (SSH daemon) instance. + *

    + * 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: + *

      + *
    • A host RSA key (`ssh_host_rsa_key`).
    • + *
    • A host certificate (`ssh_host_rsa_key-cert.pub`).
    • + *
    • A Certificate Authority (CA) public key (`ca_jsch_key.pub`) to validate user + * certificates.
    • + *
    • An SSH configuration file (`sshd_config`).
    • + *
    + */ + @Container + public GenericContainer sshd = new GenericContainer<>(new ImageFromDockerfile() + .withFileFromClasspath("ssh_host_rsa_key", "certificates/docker/ssh_host_rsa_key") + // .withFileFromClasspath("ssh_host_rsa_key.pub", "certificates/docker/ssh_host_rsa_key.pub") + .withFileFromClasspath("ssh_host_rsa_key-cert.pub", + "certificates/docker/ssh_host_rsa_key-cert.pub") + .withFileFromClasspath("ca_jsch_key.pub", "certificates/ca/ca_jsch_key.pub") + .withFileFromClasspath("sshd_config", "certificates/docker/sshd_config") + .withFileFromClasspath("Dockerfile", "certificates/docker/Dockerfile")).withExposedPorts(22) + .waitingFor(Wait.forLogMessage(".*Server listening on :: port 22.*", 1)); + + /** + * Provides the list of private key parameters used for the parameterized test. + *

    + * 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 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 lines = Files.readAllLines(Paths.get(fileName), StandardCharsets.UTF_8); + String[] split = lines.get(0).split("\\s+"); + String hostname = String.format(Locale.ROOT, "[%s]:%d", "localhost", 2222); + return new HostKey(hostname, Base64.getDecoder().decode(split[1])); + } + + /** + * Connects the provided {@link Session} and attempts to perform a simple SFTP operation. + *

    + * 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----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAACFwAAAAdzc2gtcn +NhAAAAAwEAAQAAAgEAx35F099kXRefAFwdezUBqsz2lYRCdUk13bhTOrAZFg0smdsobJfP +4e1WqiiulzcM24PQwbmuO2C4ZN/Wkzd4ahho2Xe5iLfB3yA9bEePNy8qdqKD6K/nbvDnwg +GnnkwMt69VEtUJk2EseqwsNZKiutxtQS45jMgnTZOyIoVEIT/+lMP1auvaJdM3jFBOy8Ka +pVpyLhdj5Ben3v7D6XGCupfqgaiMW37yi5jUNzlMyZAB0OS0JnL9RY4F29Z1AHuCXQzm8s +zSia4ljGVXQSTD9wOHrXk8VSYwwQCGS7T4xnA9M6Q7Ri8ZSyXJ/vEpaMyshnoSPC6LUPBJ +14+SlcnqonDa6FX/9f7OgP9leVeZhEtFTHN5ZDP1IRaqyOwk4vvlhUPzkFpQZAj3fQ0Hvt +CznL/UUwo5RJS9t3HZRH7gjYIcdZT5iMugqzI3E+wcgN8mkCMjTnxBe6L7N1RvpQ5XAr2f +ByQrfW00F2/8LqEYQgP8P7EIK8Fbyd97tJthDwlp2TU7qjpnVKu/2F2aGzbtkd2qLBiJXC +zOcIA43XrOi5QH+VH89CmktHk7OBFIpdlV6YMbdLUvBp4EUZsH3tHMmXkUf4o7/RkXFD15 +D5j/s8iUhNj3NBk3f5jVA8b5sXk/8G/Nop8u0C+FFcNG5FLrbhyldm4L6FR+a0u+yRNqsT +0AAAdA6rcbAuq3GwIAAAAHc3NoLXJzYQAAAgEAx35F099kXRefAFwdezUBqsz2lYRCdUk1 +3bhTOrAZFg0smdsobJfP4e1WqiiulzcM24PQwbmuO2C4ZN/Wkzd4ahho2Xe5iLfB3yA9bE +ePNy8qdqKD6K/nbvDnwgGnnkwMt69VEtUJk2EseqwsNZKiutxtQS45jMgnTZOyIoVEIT/+ +lMP1auvaJdM3jFBOy8KapVpyLhdj5Ben3v7D6XGCupfqgaiMW37yi5jUNzlMyZAB0OS0Jn +L9RY4F29Z1AHuCXQzm8szSia4ljGVXQSTD9wOHrXk8VSYwwQCGS7T4xnA9M6Q7Ri8ZSyXJ +/vEpaMyshnoSPC6LUPBJ14+SlcnqonDa6FX/9f7OgP9leVeZhEtFTHN5ZDP1IRaqyOwk4v +vlhUPzkFpQZAj3fQ0HvtCznL/UUwo5RJS9t3HZRH7gjYIcdZT5iMugqzI3E+wcgN8mkCMj +TnxBe6L7N1RvpQ5XAr2fByQrfW00F2/8LqEYQgP8P7EIK8Fbyd97tJthDwlp2TU7qjpnVK +u/2F2aGzbtkd2qLBiJXCzOcIA43XrOi5QH+VH89CmktHk7OBFIpdlV6YMbdLUvBp4EUZsH +3tHMmXkUf4o7/RkXFD15D5j/s8iUhNj3NBk3f5jVA8b5sXk/8G/Nop8u0C+FFcNG5FLrbh +yldm4L6FR+a0u+yRNqsT0AAAADAQABAAACAA7HwVWB3QN/AxyJdp9g2BYHGAjlXZrSI6/L +nueqUZLTmIzwqBrS7PJsqB8KIxSt3eHAryg/QSmskD+HDF89Z5+YCI1ThGyc+BFM5/LHDX +/ARUQ4umRfGLBHSysm/hEpdMgXhop95rb45uJS3HapUy2xiasKiUsv2XudrapxlxCN7NHU +VUad9qcWnQG5bHvGbWNCuNLHOwh/ToR5eds9+fp9qWtVqR/ZSg/uMODuOaJ1woYEf9Vub8 +CQwHM0fS2IjmolpnxxkPK68z9m6GikQvGg8miM+DDzLVRgsn0sSWyYYcHFpqTxSzgzsqF/ +zXqH2Yt1bWGbQt6qYuovPLl6K4xKU3y9tcNR7ZhsyTI3s0LSD7JCElmzWT7Y7p46rXsPKh +2UdsE3oED1CipU5rY1dEOBDlaN392gRT41WImLFf/O3st6OecKvRJ1AfPRch9v5Ir8/vgi +dH2CPfsadsyMD4A4OnsvlSPWhdseLocCR7WpmZaqyWpogTSBp8eYv52lMMn2lfQ7Unde/9 +/DYRNYQ8UbAFsdvhm0oO+fdq/n9k9/0HzOkkZL9Oxl3R6lKbAQz3zFc4mXwF6Gz1i0pGLQ +U/SmxTgyA/KQPtO+rSeT1dVmm8yieJv/zoOAqtUxkF4ow4SRvOIuGqGzr//gVxxzm2hAfq +Uo56gG3ZyUoZ6gzt6ZAAABAQCG0F0XthEFahVF6jF1cmmH2pdg2Bd06DEYs92r7TJG8BLK +wdVKlNPWSOuS2j9oCoZgbAk5dVnIOxCJClxV+ujZgQyLf2YNGsHywcJftkRAvBl4C7mYWF +ryWSH/5z8q7WNf0vYOAmeBs8jl8v21Z+C4q5jPUF86Opoq3p9+yQJnCf6Y+sOyCG36oD5d +ZGkAtSS0V9vcuwB/ZHxgxGrgskql51w6XumtxkEWab4qWsDs66+X9Z04G54f2vUNBFOPVa +r2h2GczthZT+9zXCu0eh3JRDPcjkTAw6+8Yf5RJiOiuacLvG77V51dSnJkuwsODpK5KEkk +NY+FRQcc4q20jLsBAAABAQD6PsCyDV2e9l+I2NI4BAsE2r7YNw1Iylc83w3qPNWItBWgAL +jdRpk2Sr8Yp7n/CUe9b3s3/DBcPNeVq/biAxABhiShrSWp54FmXe7L1mbVRrA2sM2eV2uy +GrsDlta8etE2Xrtv7PU11VVKvJ4zy8g9WE36yLkZc/JMUhmCkVxFKJLqUYG+S2WwtVlU/v +68pyOlJWfIAaaoE8mFAZKzfWdgIMoTt2voa5eqo3CzH+MqwtmhZL/UCpDdcE7tmqEpe/WM +XAZLTJyD+et5BWC3TLqvYZRXiG4vxHbUSKKAPmnjWV/rGhzYUAMC2TjR8fQPkIqMvbvTKG +Uyyr7ulintDtoFAAABAQDMFLuWegwc5AgnUpd2HKdnWXng37dH8c6HgrEuVzlCZdEGKG8I +g3n+AIechx2jaC0JkpHkC6j9D5rhedBwGw3Z3inYteA2q5KLI+AoJVT1uyaXlDeWDQl3On +TZgnMkoDX9J7a9v3lsYIoFJCfkZHaUJAQKJt5nTjCNTLJ5Pko9nPkxL5hMuiu8l1F0XvyU +eOkjdBiJ2JENjy6ms9oq//6HtOgtt3yDAnJWycSwvsDssluFKzwdpfM6PQRaHG6qTMQrm6 +8zG9zFCTIFIoii27O/v6KSSEhZIhajM3mFY66d1tpnmcs0Y9FzP25LdSwDTeQyEYMsU1Pm +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----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAACFwAAAAdzc2gtcn +NhAAAAAwEAAQAAAgEAthMKFgB3n3f6H+2W5M9ZTFlL2yS5i4/YL4uDY3D5l+4Tu+fBLdp9 +wmCqtGUa3+2WfQ3aCZlslxv3ZRODcitL05zY4B0B592wW2HW8/hygZErs72ESRyUmCa4Xy +arFEcCo28iWsiseCxcyv7QL3sLnFQpuVO69Ri/knK7JZVX7u+CMDQk7ThnkmEP3qZJaYU2 +vACCy3Scw48q2B9GQPW4ilwXvTju5izLQfbgGX+qaz1USu/0DzfNyN3tuxfambQENhToXk +fAbtBbccpipXLJDLjQMcXAwe7oN+Uc/Pd+Oe2fHxRrBOfCKUjbbh019F3/EYd355pPJVuU +s6T+nPzH0/55XcPGlWagk7esHjrBEsaehzw5DaCuC3FsGnetSff9o1BXjk9VGV7n7PNutP +j9nNdfMTMDlmYygpnQIUvT+gxAyP1tExc89bpuUdOmCN4NDsdPo19obzKQ3Ln0PDliuWtX +CLwyM9ccBn3xt496kg0Nt1kmiOWbTPHGqj98kMfveBa3dPmTzm97rCfDiDU93wE6GqHzPR +WnU2xQJQle42Dsff+qL9aocwcXHnKZdCPW9hLkvEy0KzcEFxlnjEcjxRNsq7AoUQTz1Cy5 +3ElqdqMie10cZj+6LQkaPZEr3/muzvvvSuAhQToV5AcTk2oDy1u2IMbvyZc4/zFkP7oyZ5 +kAAAdARQTSLUUE0i0AAAAHc3NoLXJzYQAAAgEAthMKFgB3n3f6H+2W5M9ZTFlL2yS5i4/Y +L4uDY3D5l+4Tu+fBLdp9wmCqtGUa3+2WfQ3aCZlslxv3ZRODcitL05zY4B0B592wW2HW8/ +hygZErs72ESRyUmCa4XyarFEcCo28iWsiseCxcyv7QL3sLnFQpuVO69Ri/knK7JZVX7u+C +MDQk7ThnkmEP3qZJaYU2vACCy3Scw48q2B9GQPW4ilwXvTju5izLQfbgGX+qaz1USu/0Dz +fNyN3tuxfambQENhToXkfAbtBbccpipXLJDLjQMcXAwe7oN+Uc/Pd+Oe2fHxRrBOfCKUjb +bh019F3/EYd355pPJVuUs6T+nPzH0/55XcPGlWagk7esHjrBEsaehzw5DaCuC3FsGnetSf +f9o1BXjk9VGV7n7PNutPj9nNdfMTMDlmYygpnQIUvT+gxAyP1tExc89bpuUdOmCN4NDsdP +o19obzKQ3Ln0PDliuWtXCLwyM9ccBn3xt496kg0Nt1kmiOWbTPHGqj98kMfveBa3dPmTzm +97rCfDiDU93wE6GqHzPRWnU2xQJQle42Dsff+qL9aocwcXHnKZdCPW9hLkvEy0KzcEFxln +jEcjxRNsq7AoUQTz1Cy53ElqdqMie10cZj+6LQkaPZEr3/muzvvvSuAhQToV5AcTk2oDy1 +u2IMbvyZc4/zFkP7oyZ5kAAAADAQABAAACABIgKJmin8YCF2YszKAILVNes2DEUe1dG1A2 +YxTX9x0DIDdNXcufC/R5E51kUE3ZFOlrothgS/FqIRGQpP4NZd3R6Dw9XwZyaR9byN6eTe +Xsqg1ZcU0m+XsBJcshKhhZbl+PTXv8rMDE59L0lYyYgwIj1ciDl6HVPiMJ5WwbJzcb5FnA +oylze0oU/BO1+ap1zspeAadX+1AzlTgROvj2DVJG0z0s6QmEjZKKTWp0bWaCIz1XkHQgYc +3cnBFLUWGKTH4GzFAwc+2ENApKs9HVMMMhteYykdssmbUQy7c6Ozhwma3qJjJFkSxzepbG +dSvX3K/lzM/DapwlNxNAOnGt74WV+vXjslRl+Fu5NrtgHeQC0/QdvDAdG+aBibxaP+L9m5 +k5jUmq5jSL+9KDDvC/9WlcY3g3Wv3EVGXw0qqh8iw1MDX4yH8VrMHJ+svsDCVp8rOsRJC7 +3crDm4hiCByKbWYb94aKsrAMbIwVIH1LEsW9cgLuZWhQCWyqqbDURMCYuFQKVF5dwRj/pQ +YU7pLO6JOXYjouKwGNmkm9g5uGrTADDLfmWEV+b8xTOXt/oSnWn1Hb/089COcxc/iUyDnl +hhwj6ER9DOfxN5bK8WBT7A03pm29Ke/x1UXHMN6hokFFDmUGY2kzjsB3o1p/c/HWgEKCsQ +t+r8l56zYD8p4KtLJ1AAABAGjgbYwLvqxDJPeD+u801EvMVEAnYfxbygMGTxnP4bDuViaz +VxBTv9Lcaw2dmxLlVMLXcbFtkSfb+mmUnPu7AJspKduj2FTd1J61ewJEnOb/7X3BYSvuUw +3Ac02gOv+NbVy6y33d5S0d4zhlMdybGsrK4vEbyAEYoUXvYrj7mfwrz5MN/1y3Z0TPoszk +qy+TC3W40f25YJ3lG4np+b8fHFitHqtv6tldmV7CVnwjE391St5vUZUC/3eEJxRt/LUF5f +lUV6XFeTkZ93oZq3KaKhj7VYfL6pA5Oz/zQswIAFErBntgwF0bZioheU59w7GNUeZnGlaW +SW1gTGWfVXNU6UkAAAEBANsbalouB3wT7ASLT3raEybnace3Xc4/EIiMrSTS3baPSiP64X +3xXI80T9qculS1uPDo15wTAUnx6FNHEQ7fnfk3XEFdHxL2a7fdnMMnVefisI0Q2Wd2/uLT +3/3eUUbE72Ne9WZKi6cE4jyyin4rckaQBDzVQSIV1/Co7iHkpbXaJFXeRTsmhgLWkkPPP4 +XdAZI7VsLwYKT7n0tvMxGSIHGHwV5Z9Sr3mIhQbcCktf/nyAh0utmvYDJrlIGxQ9Yd7wiP ++OfSY4XIgTfgd6MjdKfH+A/Rp1y+16QW6Wn9LnwlT/dhtPt/5X8jlL9VvmYrCZhQOnzyjF +lhB11HGfovR10AAAEBANS7VUa5YhrSbsMhGTDCukMBrgBW9lnTEACryzim/eNztf7GyUcm +BJcbCTZ3ccNcaLq/bVLQZv4rdlWeSILYb4W5Oa+YelyWXuXFVuOVhwVYnYoif4REj03GRH +gvIB+xktkLbfALFFV2B3oDCWrPE9agr3SNDTPweKjvD69PqpVtKVmnyaamYlx1UdiMp/nO +2YfrJz79iR+PzTWcvN4Uo3TuiJ4MxnLcx70LgVwRJwhVdHsGZYNMCmw6LhpQcWV6CJ6tFj +0ucc2f9ZP5ItXWveBA+6RVxvMFjP8gCdh0khFmX/CX18F0f4yc+xeoR2WGxKYxXLDrbbFV +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