diff --git a/.gitignore b/.gitignore index 106d01c7f..a10d1f1aa 100644 --- a/.gitignore +++ b/.gitignore @@ -83,6 +83,9 @@ keys/*-ossh-*cert.pub random-test.txt random-test-result.txt test.dat +# Scratch file the api tests write in the working directory. Removed on a +# clean run, left behind when one aborts. +ossh-cert-line.tmp # test output tests/*.test diff --git a/keys/include.am b/keys/include.am index 99dcb4ac0..b0d1f6751 100644 --- a/keys/include.am +++ b/keys/include.am @@ -20,6 +20,7 @@ EXTRA_DIST+= \ keys/pubkeys-rsa.txt keys/passwd.txt keys/ca-cert-ecc.der \ keys/ca-cert-ecc.pem keys/ca-key-ecc.der keys/ca-key-ecc.pem \ keys/server-cert.der keys/server-cert.pem \ + keys/server-cert-ed25519.der keys/server-cert-ed25519.pem \ keys/fred-cert.der keys/fred-cert.pem \ keys/server-key.pem keys/fred-key.der keys/fred-key.pem \ keys/id_ecdsa keys/id_ecdsa.pub keys/id_rsa keys/id_rsa.pub \ diff --git a/keys/renewcerts.sh b/keys/renewcerts.sh index 176e287a2..d6ba8d146 100755 --- a/keys/renewcerts.sh +++ b/keys/renewcerts.sh @@ -34,6 +34,16 @@ openssl req -subj '/C=US/ST=Washington/L=Seattle/O=Eliptic/OU=ECC/CN=www.wolfssl openssl x509 -req -in server-cert.csr -days 3650 -extfile "$CONFIG" -extensions v3_server -CA ca-cert-ecc.pem -CAkey ca-key-ecc.pem -text -out server-cert.pem -set_serial 8 openssl x509 -in server-cert.pem -outform DER -out server-cert.der +# renew server-cert-ed25519. Ed25519 has no x509v3-* SSH algorithm name, so +# this certificate exercises the rejection of an unmappable key type. Its key +# is not a host key, so it is dropped once the certificate is signed. +openssl genpkey -algorithm ed25519 -out server-cert-ed25519-key.pem +openssl req -subj '/C=US/ST=Washington/L=Seattle/O=Eliptic/OU=Ed25519/CN=www.wolfssl.com/emailAddress=server@example.com' -key server-cert-ed25519-key.pem -out server-cert-ed25519.csr -config "$CONFIG" -new -nodes + +openssl x509 -req -in server-cert-ed25519.csr -days 3650 -extfile "$CONFIG" -extensions v3_server -CA ca-cert-ecc.pem -CAkey ca-key-ecc.pem -text -out server-cert-ed25519.pem -set_serial 9 +openssl x509 -in server-cert-ed25519.pem -outform DER -out server-cert-ed25519.der +rm -f server-cert-ed25519-key.pem server-cert-ed25519.csr + rm index.* if [ -n "$1" ]; then rm -f "$CONFIG" diff --git a/keys/server-cert-ed25519.der b/keys/server-cert-ed25519.der new file mode 100644 index 000000000..369080789 Binary files /dev/null and b/keys/server-cert-ed25519.der differ diff --git a/keys/server-cert-ed25519.pem b/keys/server-cert-ed25519.pem new file mode 100644 index 000000000..3ffd18a77 --- /dev/null +++ b/keys/server-cert-ed25519.pem @@ -0,0 +1,50 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 9 (0x9) + Signature Algorithm: ecdsa-with-SHA256 + Issuer: C=US, ST=Washington, L=Seattle, O=wolfSSL, OU=Development, CN=www.wolfssl.com, emailAddress=ca@example.com + Validity + Not Before: Aug 3 05:11:17 2026 GMT + Not After : Jul 31 05:11:17 2036 GMT + Subject: C=US, ST=Washington, L=Seattle, O=Eliptic, OU=Ed25519, CN=www.wolfssl.com, emailAddress=server@example.com + Subject Public Key Info: + Public Key Algorithm: ED25519 + ED25519 Public-Key: + pub: + 69:97:cf:00:fe:3c:27:d0:1b:3f:14:60:5a:b9:68: + a1:8c:89:64:93:ee:89:8c:48:31:a1:05:b1:f6:77: + bb:57 + X509v3 extensions: + X509v3 Subject Key Identifier: + F7:D0:2B:CD:7D:A0:B7:1B:90:EB:F3:E8:E6:D0:A5:E9:C7:5F:E6:72 + X509v3 Authority Key Identifier: + keyid:56:8E:9A:C3:F0:42:DE:18:B9:45:55:6E:F9:93:CF:EA:C3:F3:A5:21 + DirName:/C=US/ST=Washington/L=Seattle/O=wolfSSL/OU=Development/CN=www.wolfssl.com/emailAddress=ca@example.com + serial:06 + X509v3 Subject Alternative Name: + DNS:example, IP Address:127.0.0.1 + Signature Algorithm: ecdsa-with-SHA256 + Signature Value: + 30:44:02:20:58:41:57:d7:27:fd:e0:a6:25:bf:40:11:31:5a: + e1:6d:7c:60:35:dc:04:f9:6e:2e:8c:d5:4b:41:1d:65:d8:16: + 02:20:36:9c:e1:fa:4d:e1:aa:1f:19:4e:41:99:4d:79:ee:ed: + fb:7c:68:7e:0a:48:c6:12:7d:44:12:be:59:55:d3:6f +-----BEGIN CERTIFICATE----- +MIIC7jCCApWgAwIBAgIBCTAKBggqhkjOPQQDAjCBlTELMAkGA1UEBhMCVVMxEzAR +BgNVBAgMCldhc2hpbmd0b24xEDAOBgNVBAcMB1NlYXR0bGUxEDAOBgNVBAoMB3dv +bGZTU0wxFDASBgNVBAsMC0RldmVsb3BtZW50MRgwFgYDVQQDDA93d3cud29sZnNz +bC5jb20xHTAbBgkqhkiG9w0BCQEWDmNhQGV4YW1wbGUuY29tMB4XDTI2MDgwMzA1 +MTExN1oXDTM2MDczMTA1MTExN1owgZUxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApX +YXNoaW5ndG9uMRAwDgYDVQQHDAdTZWF0dGxlMRAwDgYDVQQKDAdFbGlwdGljMRAw +DgYDVQQLDAdFZDI1NTE5MRgwFgYDVQQDDA93d3cud29sZnNzbC5jb20xITAfBgkq +hkiG9w0BCQEWEnNlcnZlckBleGFtcGxlLmNvbTAqMAUGAytlcAMhAGmXzwD+PCfQ +Gz8UYFq5aKGMiWST7omMSDGhBbH2d7tXo4IBATCB/jAdBgNVHQ4EFgQU99ArzX2g +txuQ6/Po5tCl6cdf5nIwgcIGA1UdIwSBujCBt4AUVo6aw/BC3hi5RVVu+ZPP6sPz +pSGhgZukgZgwgZUxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApXYXNoaW5ndG9uMRAw +DgYDVQQHDAdTZWF0dGxlMRAwDgYDVQQKDAd3b2xmU1NMMRQwEgYDVQQLDAtEZXZl +bG9wbWVudDEYMBYGA1UEAwwPd3d3LndvbGZzc2wuY29tMR0wGwYJKoZIhvcNAQkB +Fg5jYUBleGFtcGxlLmNvbYIBBjAYBgNVHREEETAPggdleGFtcGxlhwR/AAABMAoG +CCqGSM49BAMCA0cAMEQCIFhBV9cn/eCmJb9AETFa4W18YDXcBPluLozVS0EdZdgW +AiA2nOH6TeGqHxlOQZlNee7t+3xofgpIxhJ9RBK+WVXTbw== +-----END CERTIFICATE----- diff --git a/src/internal.c b/src/internal.c index 8680bf5a7..2de2e45cc 100644 --- a/src/internal.c +++ b/src/internal.c @@ -1951,13 +1951,9 @@ int IdentifyAsn1Key(const byte* in, word32 inSz, int isPrivate, void* heap, ret = wc_MlDsaKey_PublicKeyDecode(&key->ks.mldsa.key, in, inSz, &idx); if (ret != 0) { - /* Length-only fallback for local key/cert loading - * (wolfSSH_ReadKey_buffer_ex / IdentifyCert) when SPKI - * OID decode fails: wc_MlDsaKey_ImportPubRaw accepts an - * ML-DSA blob of exactly 1312/1952/2592 bytes and tags - * it ID_MLDSA44/65/87. Not used on the remote user-auth - * path; do not wire this probe into remote auth assuming - * it cryptographically validates the key. */ + /* Local loading only: size alone tags a raw ML-DSA + * blob when SPKI OID decode fails. This does not + * validate the key, so keep it off remote auth. */ struct { byte level; byte id; } kProbe[3]; word32 nProbe = 0, li; #ifndef WOLFSSH_NO_MLDSA44 @@ -2076,17 +2072,10 @@ int IdentifyAsn1Key(const byte* in, word32 inSz, int isPrivate, void* heap, #ifdef WOLFSSH_CERTS -/* - * Identifies the flavor of an X.509 certificate, RSA, ML-DSA or ECDSA, returns - * the key type ID. The process is to decode the certificate and pass the - * public key to IdentifyAsn1Key. - * - * @param in certificate to identify - * @param inSz size of certificate - * @param heap heap to use for memory allocation - * @return keyId as int, WS_MEMORY_E, WS_UNIMPLEMENTED_E - */ -static int IdentifyCert(const byte* in, word32 inSz, void* heap) +/* Identifies the key held inside an X.509 certificate, returning its plain + key type ID or a WS_ error. See IdentifyCert() for the x509v3-* algorithm + ID sent on the wire. */ +static int IdentifyCertKey(const byte* in, word32 inSz, void* heap) { struct DecodedCert* cert = NULL; #ifndef WOLFSSH_SMALL_STACK @@ -2106,13 +2095,22 @@ static int IdentifyCert(const byte* in, word32 inSz, void* heap) } #endif + /* Each wolfSSL result below is mapped where it is produced, so this + function returns only a key ID or a WS_ code. */ if (ret == 0) { wc_InitDecodedCert(cert, in, inSz, heap); ret = wc_ParseCert(cert, CERT_TYPE, 0, NULL); + if (ret != 0) { + ret = WS_PARSE_E; + } } if (ret == 0) { + /* Asking with no buffer answers with the length and LENGTH_ONLY_E. */ ret = wc_GetPubKeyDerFromCert(cert, NULL, &keySz); - if (ret == LENGTH_ONLY_E) { + if (ret != LENGTH_ONLY_E) { + ret = WS_PARSE_E; + } + else { ret = 0; key = (byte*)WMALLOC(keySz, heap, DYNTYPE_PUBKEY); if (key == NULL) { @@ -2123,6 +2121,9 @@ static int IdentifyCert(const byte* in, word32 inSz, void* heap) if (ret == 0) { ret = wc_GetPubKeyDerFromCert(cert, key, &keySz); + if (ret != 0) { + ret = WS_PARSE_E; + } } if (ret == 0) { @@ -2233,6 +2234,31 @@ static INLINE byte CertTypeForId(byte id) return id; } + +/* Identifies an X.509 certificate, returning the x509v3-* algorithm ID sent + on the wire. A key type with no x509v3 name in this build is rejected + rather than reported under its plain key name. */ +int IdentifyCert(const byte* in, word32 inSz, void* heap) +{ + byte certId; + int ret; + + ret = IdentifyCertKey(in, inSz, heap); + + if (ret >= 0) { + certId = CertTypeForId((byte)ret); + if (certId == (byte)ret) { + WLOG(WS_LOG_DEBUG, "No x509v3 algorithm for this certificate"); + ret = WS_INVALID_ALGO_ID; + } + else { + ret = (int)certId; + } + } + + return ret; +} + #define HINTISSET(x) ((x) != WOLFSSH_MAX_PVT_KEYS) static int UpdateHostCertificates(WOLFSSH_CTX* ctx, @@ -2326,18 +2352,14 @@ static int UpdateHostCertificates(WOLFSSH_CTX* ctx, } static int SetHostCertificate(WOLFSSH_CTX* ctx, - byte keyId, byte* der, word32 derSz, int dynamicType) + byte certId, byte* der, word32 derSz, int dynamicType) { - /* - * The keyId is for the key inside the certificate. wolfSSH_ProcessBuffer - * will decode the certificate, get the public key inside, and identify - * that. keyId will be: ssh-rsa, ecdsa-sha2-nistp256, etc. - */ + /* The certId is the x509v3-* algorithm the certificate is presented as, + * identified by wolfSSH_ProcessBuffer before calling here. */ word32 destIdx, certIdx = WOLFSSH_MAX_PVT_KEYS, keyIdx = WOLFSSH_MAX_PVT_KEYS; int ret = WS_SUCCESS; - byte certId = CertTypeForId(keyId); /* Look for the specified certId. Add it if not present, * replace it if present. Call UpdateHostCertificate(). @@ -2347,7 +2369,10 @@ static int SetHostCertificate(WOLFSSH_CTX* ctx, if (ctx->privateKey[destIdx].publicKeyFmt == certId) { certIdx = destIdx; } - if (ctx->privateKey[destIdx].publicKeyFmt == keyId) { + /* The key for this certificate sits in the slot whose plain + algorithm maps onto certId. */ + else if (CertTypeForId(ctx->privateKey[destIdx].publicKeyFmt) + == certId) { keyIdx = destIdx; } } @@ -2618,12 +2643,16 @@ int wolfSSH_ProcessBuffer(WOLFSSH_CTX* ctx, WFREE(der, heap, dynamicType); return ret; } - keyId = (byte)ret; - ret = SetHostCertificate(ctx, keyId, der, derSz, dynamicType); + ret = SetHostCertificate(ctx, (byte)ret, der, derSz, dynamicType); } else if (type == BUFTYPE_CA) { if (ctx->certMan != NULL) { ret = wolfSSH_CERTMAN_LoadRootCA_buffer(ctx->certMan, der, derSz); + /* That call answers in wolfSSL codes apart from these two, so + anything else becomes one of ours. */ + if (ret != WS_SUCCESS && ret != WS_BAD_ARGUMENT) { + ret = WS_PARSE_E; + } } else { WLOG(WS_LOG_DEBUG, "Error no cert manager set"); diff --git a/src/ssh.c b/src/ssh.c index 1ca8771b6..8bd609fc7 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -1786,6 +1786,9 @@ char* wolfSSH_GetUsername(WOLFSSH* ssh) #ifndef WOLFSSH_NO_MLDSA #include #endif +#ifdef WOLFSSH_OSSH_CERTS + #include +#endif union wolfSSH_key { #ifndef WOLFSSH_NO_RSA @@ -2373,34 +2376,265 @@ int wolfSSH_ReadPublicKey_buffer(const byte* in, word32 inSz, int format, } -#if !defined(NO_FILESYSTEM) && !defined(WOLFSSH_USER_FILESYSTEM) +#if defined(WOLFSSH_CERTS) || defined(WOLFSSH_OSSH_CERTS) -/* Reads a key from the file name into a buffer. If the key starts with the - string "ssh-rsa" or "ecdsa-sha2-nistp256", it is considered an SSH format - public key, if it has "----BEGIN" it is considered PEM formatted, - otherwise it is considered an ASN.1 private key. The buffer is passed to - wolfSSH_ReadKey_buffer() for processing. */ -int wolfSSH_ReadKey_file(const char* name, +#ifdef WOLFSSH_CERTS + static const char* CertBeginPrefix = "-----BEGIN CERTIFICATE-----"; +#endif + +/* Longest algorithm name is ecdsa-sha2-nistp521-cert-v01@openssh.com. */ +#define WOLFSSH_MAX_CERT_ALGO_NAME_SZ 48 + +/* Identifies a certificate from its content, without decoding or allocating. + An x509v3-* line holds a wire chain, not a certificate, so it is declined. */ +static int SniffCertForm(const byte* in, word32 inSz, byte* flavor, + byte* certId) +{ +#ifdef WOLFSSH_OSSH_CERTS + word32 tokenSz = 0; + byte id; +#endif + int ret = WS_BAD_FILETYPE_E; + +#ifdef WOLFSSH_CERTS + if (in[0] == 0x30) { + *flavor = WOLFSSH_CERT_FLAVOR_X509; + ret = WOLFSSH_FORMAT_ASN1; + } + /* Searched for, not anchored: openssl writes a dump ahead of the header. */ + else if (WSTRNSTR((const char*)in, CertBeginPrefix, inSz) != NULL) { + *flavor = WOLFSSH_CERT_FLAVOR_X509; + ret = WOLFSSH_FORMAT_PEM; + } +#endif /* WOLFSSH_CERTS */ + +#ifdef WOLFSSH_OSSH_CERTS + if (ret == WS_BAD_FILETYPE_E) { + while (tokenSz < inSz && tokenSz < WOLFSSH_MAX_CERT_ALGO_NAME_SZ + && in[tokenSz] != ' ' && in[tokenSz] != '\t' + && in[tokenSz] != '\r' && in[tokenSz] != '\n') { + tokenSz++; + } + + if (tokenSz > 0 && tokenSz < WOLFSSH_MAX_CERT_ALGO_NAME_SZ) { + id = NameToId((const char*)in, tokenSz); + + switch (id) { + case ID_OSSH_CERT_RSA: + case ID_OSSH_CERT_ECDSA_SHA2_NISTP256: + case ID_OSSH_CERT_ECDSA_SHA2_NISTP384: + case ID_OSSH_CERT_ECDSA_SHA2_NISTP521: + case ID_OSSH_CERT_ED25519: + *flavor = WOLFSSH_CERT_FLAVOR_OSSH; + *certId = id; + ret = WOLFSSH_FORMAT_SSH; + break; + default: + break; + } + } + } +#else + WOLFSSH_UNUSED(certId); +#endif /* WOLFSSH_OSSH_CERTS */ + + return ret; +} + + +#ifdef WOLFSSH_CERTS + +/* Decodes a PEM certificate to DER and identifies it. */ +static int DoPemCert(const byte* in, word32 inSz, byte** out, word32* outSz, + const byte** outType, word32* outTypeSz, void* heap) +{ + byte* der; + word32 derSz = 0; + int ret; + + der = (byte*)WMALLOC(inSz, heap, DYNTYPE_CERT); + if (der == NULL) { + return WS_MEMORY_E; + } + + ret = wc_CertPemToDer(in, (int)inSz, der, (int)inSz, CERT_TYPE); + if (ret <= 0) { + WLOG(WS_LOG_DEBUG, "PEM to DER of certificate failed."); + WFREE(der, heap, DYNTYPE_CERT); + return WS_BAD_FILE_E; + } + derSz = (word32)ret; + + ret = IdentifyCert(der, derSz, heap); + if (ret < 0) { + WFREE(der, heap, DYNTYPE_CERT); + } + else { + *out = der; + *outSz = derSz; + *outType = (const byte*)IdToName((byte)ret); + *outTypeSz = (word32)WSTRLEN((const char*)*outType); + ret = WS_SUCCESS; + } + + return ret; +} + + +/* Identifies a DER certificate before copying it out. */ +static int DoDerCert(const byte* in, word32 inSz, byte** out, word32* outSz, + const byte** outType, word32* outTypeSz, void* heap) +{ + byte* der; + int ret; + + ret = IdentifyCert(in, inSz, heap); + + if (ret >= 0) { + der = (byte*)WMALLOC(inSz, heap, DYNTYPE_CERT); + if (der == NULL) { + ret = WS_MEMORY_E; + } + else { + WMEMCPY(der, in, inSz); + *out = der; + *outSz = inSz; + *outType = (const byte*)IdToName((byte)ret); + *outTypeSz = (word32)WSTRLEN((const char*)*outType); + ret = WS_SUCCESS; + } + } + + return ret; +} + +#endif /* WOLFSSH_CERTS */ + + +#ifdef WOLFSSH_OSSH_CERTS + +/* Decodes an OpenSSH certificate line and checks that it parses. */ +static int DoOsshCert(const byte* in, word32 inSz, byte certId, byte** out, + word32* outSz, const byte** outType, word32* outTypeSz, void* heap) +{ + WS_OsshCert cert; + byte* blob; + int ret; + + blob = (byte*)WMALLOC(inSz, heap, DYNTYPE_CERT); + if (blob == NULL) { + return WS_MEMORY_E; + } + + /* DoSshPubKey decodes into a non-NULL *out rather than allocating, so it + fills blob and leaves it ours to free. Were it to allocate instead, + blob would leak and the free below would take the wrong type. */ + *out = blob; + *outSz = inSz; + + ret = DoSshPubKey(in, inSz, out, outSz, outType, outTypeSz, heap); + + if (ret == WS_SUCCESS) { + /* The blob has no envelope, so only a parse tells it from a key. */ + ret = OsshCertParse(&cert, certId, *out, *outSz); + if (ret != WS_SUCCESS) { + WLOG(WS_LOG_DEBUG, "OpenSSH certificate is malformed."); + } + } + + if (ret != WS_SUCCESS) { + WFREE(blob, heap, DYNTYPE_CERT); + *out = NULL; + *outSz = 0; + } + + return ret; +} + +#endif /* WOLFSSH_OSSH_CERTS */ + + +/* Reads a certificate from the buffer in to out, detecting its form from the + content. See ssh.h for what the caller owns. */ +int wolfSSH_ReadCert_buffer(const byte* in, word32 inSz, byte** out, word32* outSz, const byte** outType, word32* outTypeSz, - byte* isPrivate, void* heap) + byte* flavor, void* heap) { + byte certId = ID_UNKNOWN; + int format; + int ret; + + if (in == NULL || inSz == 0 || out == NULL || outSz == NULL || + outType == NULL || outTypeSz == NULL || flavor == NULL) { + return WS_BAD_ARGUMENT; + } + + *out = NULL; + *outSz = 0; + *outType = NULL; + *outTypeSz = 0; + *flavor = WOLFSSH_CERT_FLAVOR_UNKNOWN; + + format = SniffCertForm(in, inSz, flavor, &certId); + if (format < 0) { + WLOG(WS_LOG_DEBUG, "Unable to identify the certificate"); + return format; + } + ret = WS_BAD_FILETYPE_E; + + switch (format) { + #ifdef WOLFSSH_CERTS + case WOLFSSH_FORMAT_PEM: + ret = DoPemCert(in, inSz, out, outSz, outType, outTypeSz, heap); + break; + + case WOLFSSH_FORMAT_ASN1: + ret = DoDerCert(in, inSz, out, outSz, outType, outTypeSz, heap); + break; + #endif /* WOLFSSH_CERTS */ + #ifdef WOLFSSH_OSSH_CERTS + case WOLFSSH_FORMAT_SSH: + ret = DoOsshCert(in, inSz, certId, out, outSz, outType, outTypeSz, + heap); + break; + #endif /* WOLFSSH_OSSH_CERTS */ + default: + break; + } + + /* The sniff names the form and a decoder may fill an out parameter before + failing, so a failure clears them all rather than leave a partial + answer. Decoders free whatever they allocated. */ + if (ret != WS_SUCCESS) { + *out = NULL; + *outSz = 0; + *outType = NULL; + *outTypeSz = 0; + *flavor = WOLFSSH_CERT_FLAVOR_UNKNOWN; + } + + return ret; +} + +#endif /* WOLFSSH_CERTS || WOLFSSH_OSSH_CERTS */ + + +#if !defined(NO_FILESYSTEM) && !defined(WOLFSSH_USER_FILESYSTEM) + +/* Reads the file name into a buffer allocated with an extra byte holding a + nul terminator. The caller frees out with DYNTYPE_FILE. */ +static int ReadFileIntoBuffer(const char* name, byte** out, word32* outSz, + void* heap) +{ WFILE* file; #ifdef MICROCHIP_MPLAB_HARMONY WFILE f = WBADFILE; #endif byte* in; word32 inSz; - int format; int ret; - if (name == NULL) - return WS_BAD_FILE_E; - - if (out == NULL || outSz == NULL || outType == NULL || outTypeSz == NULL || - isPrivate == NULL) - return WS_BAD_ARGUMENT; - #ifdef MICROCHIP_MPLAB_HARMONY file = &f; ret = WFOPEN(NULL, &file, name, WOLFSSH_O_RDONLY); @@ -2429,10 +2663,43 @@ int wolfSSH_ReadKey_file(const char* name, } ret = (int)WFREAD(NULL, in, 1, inSz, file); + WFCLOSE(NULL, file); + if (ret <= 0 || (word32)ret != inSz) { - ret = WS_BAD_FILE_E; + WS_FORCEZERO(in, inSz); + WFREE(in, heap, DYNTYPE_FILE); + return WS_BAD_FILE_E; } - else { + + in[inSz] = 0; + *out = in; + *outSz = inSz; + + return WS_SUCCESS; +} + + +/* Reads a key from the file name into a buffer. An SSH algorithm name marks + an SSH format public key and "-----BEGIN " a PEM one, otherwise it is an + ASN.1 private key. The buffer goes to wolfSSH_ReadKey_buffer_ex(). */ +int wolfSSH_ReadKey_file(const char* name, + byte** out, word32* outSz, const byte** outType, word32* outTypeSz, + byte* isPrivate, void* heap) +{ + byte* in; + word32 inSz; + int format; + int ret; + + if (name == NULL) + return WS_BAD_FILE_E; + + if (out == NULL || outSz == NULL || outType == NULL || outTypeSz == NULL || + isPrivate == NULL) + return WS_BAD_ARGUMENT; + + ret = ReadFileIntoBuffer(name, &in, &inSz, heap); + if (ret == WS_SUCCESS) { if (WSTRNSTR((const char*)in, "ssh-rsa", inSz) == (const char*)in || WSTRNSTR((const char*)in, "ecdsa-sha2-nistp", inSz) == (const char*)in @@ -2444,7 +2711,6 @@ int wolfSSH_ReadKey_file(const char* name, "x509v3-ssh-mldsa-", inSz) == (const char*)in) { *isPrivate = 0; format = WOLFSSH_FORMAT_SSH; - in[inSz] = 0; } else if (WSTRNSTR((const char*)in, PrivBeginOpenSSH, inSz) != NULL) { *isPrivate = 1; @@ -2464,15 +2730,55 @@ int wolfSSH_ReadKey_file(const char* name, ret = wolfSSH_ReadKey_buffer_ex(in, inSz, format, out, outSz, outType, outTypeSz, *isPrivate, heap); + + WS_FORCEZERO(in, inSz); + WFREE(in, heap, DYNTYPE_FILE); } - WFCLOSE(NULL, file); - WS_FORCEZERO(in, inSz); - WFREE(in, heap, DYNTYPE_FILE); + return ret; +} + + +#if defined(WOLFSSH_CERTS) || defined(WOLFSSH_OSSH_CERTS) + +/* Reads a certificate file and passes it to wolfSSH_ReadCert_buffer(). */ +int wolfSSH_ReadCert_file(const char* name, + byte** out, word32* outSz, const byte** outType, word32* outTypeSz, + byte* flavor, void* heap) +{ + byte* in; + word32 inSz; + int ret; + + if (name == NULL) + return WS_BAD_FILE_E; + + if (out == NULL || outSz == NULL || outType == NULL || outTypeSz == NULL || + flavor == NULL) + return WS_BAD_ARGUMENT; + + *out = NULL; + *outSz = 0; + *outType = NULL; + *outTypeSz = 0; + *flavor = WOLFSSH_CERT_FLAVOR_UNKNOWN; + + ret = ReadFileIntoBuffer(name, &in, &inSz, heap); + if (ret == WS_SUCCESS) { + ret = wolfSSH_ReadCert_buffer(in, inSz, + out, outSz, outType, outTypeSz, flavor, heap); + + /* A certificate is public, but the file may hold a private key + beside it, so scrub it like the key reader does. */ + WS_FORCEZERO(in, inSz); + WFREE(in, heap, DYNTYPE_FILE); + } return ret; } +#endif /* WOLFSSH_CERTS || WOLFSSH_OSSH_CERTS */ + #endif @@ -2930,6 +3236,81 @@ int wolfSSH_CTX_AddRootCert_buffer(WOLFSSH_CTX* ctx, return ret; } + +#if !defined(NO_FILESYSTEM) && !defined(WOLFSSH_USER_FILESYSTEM) + +/* Reads a certificate file and hands the PEM or DER it holds to the CTX. */ +static int UseCertFile(WOLFSSH_CTX* ctx, const char* name, int type) +{ + byte* in; + word32 inSz; + byte certId = ID_UNKNOWN; + byte flavor = WOLFSSH_CERT_FLAVOR_UNKNOWN; + int format; + int ret; + + if (ctx == NULL || name == NULL) { + return WS_BAD_ARGUMENT; + } + + ret = ReadFileIntoBuffer(name, &in, &inSz, ctx->heap); + if (ret == WS_SUCCESS) { + format = SniffCertForm(in, inSz, &flavor, &certId); + + if (format < 0) { + ret = format; + } + else if (format == WOLFSSH_FORMAT_SSH) { + /* A CTX takes the certificate itself, not a public key line. */ + WLOG(WS_LOG_DEBUG, "Certificate file is not PEM or DER"); + ret = WS_BAD_FILETYPE_E; + } + else { + ret = wolfSSH_ProcessBuffer(ctx, in, inSz, format, type); + } + + WS_FORCEZERO(in, inSz); + WFREE(in, ctx->heap, DYNTYPE_FILE); + } + + return ret; +} + + +/* Load in a X509 certificate file that has public key to use + * return WS_SUCCESS on success + */ +int wolfSSH_CTX_UseCert_file(WOLFSSH_CTX* ctx, const char* name) +{ + int ret; + + WLOG(WS_LOG_DEBUG, "Entering wolfSSH_CTX_UseCert_file()"); + + ret = UseCertFile(ctx, name, BUFTYPE_CERT); + + WLOG(WS_LOG_DEBUG, "Leaving wolfSSH_CTX_UseCert_file(), ret = %d", ret); + return ret; +} + + +/* Add a CA file for verifying the peer's certificate with. + * returns WS_SUCCESS on success + */ +int wolfSSH_CTX_AddRootCert_file(WOLFSSH_CTX* ctx, const char* name) +{ + int ret; + + WLOG(WS_LOG_DEBUG, "Entering wolfSSH_CTX_AddRootCert_file()"); + + ret = UseCertFile(ctx, name, BUFTYPE_CA); + + WLOG(WS_LOG_DEBUG, + "Leaving wolfSSH_CTX_AddRootCert_file(), ret = %d", ret); + return ret; +} + +#endif /* !NO_FILESYSTEM && !WOLFSSH_USER_FILESYSTEM */ + #endif /* WOLFSSH_CERTS */ diff --git a/tests/api.c b/tests/api.c index 6e3359e98..d2c25e407 100644 --- a/tests/api.c +++ b/tests/api.c @@ -53,6 +53,12 @@ #endif #ifdef WOLFSSH_OSSH_CERTS #include + #ifdef WOLFSSL_BASE64_ENCODE + /* Declared rather than including coding.h, whose Base16_Decode + * collides with the one wolfssh/test.h defines. */ + WOLFSSL_API int Base64_Encode_NoNl(const byte* in, word32 inLen, + byte* out, word32* outLen); + #endif #endif #if defined(WOLFSSH_SFTP) || defined(WOLFSSH_SCP) @@ -686,8 +692,10 @@ static void test_wolfSSH_CTX_UseCert_buffer(void) AssertIntEQ(WS_BAD_ARGUMENT, wolfSSH_CTX_UseCert_buffer(ctx, NULL, 0, WOLFSSH_FORMAT_PEM)); +#ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256 AssertIntEQ(WS_SUCCESS, wolfSSH_CTX_UseCert_buffer(ctx, cert, certSz, WOLFSSH_FORMAT_PEM)); +#endif AssertIntEQ(WS_BAD_FILETYPE_E, wolfSSH_CTX_UseCert_buffer(ctx, cert, certSz, WOLFSSH_FORMAT_ASN1)); @@ -703,8 +711,10 @@ static void test_wolfSSH_CTX_UseCert_buffer(void) AssertNotNull(cert); AssertIntNE(0, certSz); +#ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256 AssertIntEQ(WS_SUCCESS, wolfSSH_CTX_UseCert_buffer(ctx, cert, certSz, WOLFSSH_FORMAT_ASN1)); +#endif wolfSSH_CTX_free(ctx); free(cert); @@ -712,6 +722,364 @@ static void test_wolfSSH_CTX_UseCert_buffer(void) } +#if defined(WOLFSSH_CERTS) || defined(WOLFSSH_OSSH_CERTS) + +/* Public key lines. An x509v3-* line carries an RFC 6187 wire chain rather + * than a certificate, so this API declines it the same as a plain key. */ +static const char x509v3EccLine[] = + "x509v3-ecdsa-sha2-nistp256 AAAAB3NzaC1yc2EAAAA=\n"; +static const char sshRsaLine[] = "ssh-rsa AAAAB3NzaC1yc2EAAAA=\n"; +static const byte notACert[] = { 0x01, 0x02, 0x03, 0x04, 0x05 }; + +#if defined(WOLFSSH_OSSH_CERTS) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_FILESYSTEM) && !defined(WOLFSSH_USER_FILESYSTEM) +#define WOLFSSH_TEST_OSSH_CERT_FILE + +/* The name routes the line. The blob behind it is not a certificate, so a + * reader that decodes the blob rejects it there instead. */ +static const char osshCertLine[] = + "ssh-ed25519-cert-v01@openssh.com AAAAB3NzaC1yc2EAAAA=\n"; +static const char osshCertPath[] = "./ossh-cert-line.tmp"; + +/* Stages content in a file, the only form the file APIs take. Returns 0 on + * success. */ +static int writeTmpFile(const char* path, const void* data, size_t sz) +{ + FILE* f = NULL; + int ret = 0; + + f = fopen(path, "wb"); + if (f == NULL) + ret = -1; + + if (ret == 0) { + if (fwrite(data, 1, sz, f) != sz) + ret = -2; + /* Close either way, but keep the write error as the reason. */ + if (fclose(f) != 0 && ret == 0) + ret = -3; + } + + return ret; +} + +#endif /* OSSH_CERTS && ED25519 && FILESYSTEM */ + + +static void test_wolfSSH_ReadCert_buffer(void) +{ + byte* out = NULL; + word32 outSz = 0; + const byte* outType = NULL; + word32 outTypeSz = 0; + byte flavor = 0xFF; +#ifdef WOLFSSH_CERTS + byte* cert = NULL; + word32 certSz = 0; +#ifndef WOLFSSH_NO_ED25519 + int ret; +#endif +#endif + + /* Every out parameter is required, and so is a non-empty input. */ + AssertIntEQ(WS_BAD_ARGUMENT, wolfSSH_ReadCert_buffer(NULL, + sizeof(notACert), &out, &outSz, &outType, &outTypeSz, + &flavor, NULL)); + AssertIntEQ(WS_BAD_ARGUMENT, wolfSSH_ReadCert_buffer(notACert, 0, + &out, &outSz, &outType, &outTypeSz, &flavor, NULL)); + AssertIntEQ(WS_BAD_ARGUMENT, wolfSSH_ReadCert_buffer(notACert, + sizeof(notACert), NULL, &outSz, &outType, &outTypeSz, + &flavor, NULL)); + AssertIntEQ(WS_BAD_ARGUMENT, wolfSSH_ReadCert_buffer(notACert, + sizeof(notACert), &out, &outSz, &outType, &outTypeSz, + NULL, NULL)); + + /* Content that is not a certificate, a public key line included. */ + AssertIntEQ(WS_BAD_FILETYPE_E, wolfSSH_ReadCert_buffer(notACert, + sizeof(notACert), &out, &outSz, &outType, &outTypeSz, + &flavor, NULL)); + AssertIntEQ(WS_BAD_FILETYPE_E, wolfSSH_ReadCert_buffer( + (const byte*)sshRsaLine, (word32)WSTRLEN(sshRsaLine), + &out, &outSz, &outType, &outTypeSz, &flavor, NULL)); + AssertIntEQ(WS_BAD_FILETYPE_E, wolfSSH_ReadCert_buffer( + (const byte*)x509v3EccLine, (word32)WSTRLEN(x509v3EccLine), + &out, &outSz, &outType, &outTypeSz, &flavor, NULL)); + /* A rejection must not leave the caller's flavor standing as an answer. */ + AssertIntEQ(flavor, WOLFSSH_CERT_FLAVOR_UNKNOWN); + +#ifdef WOLFSSH_CERTS + AssertIntEQ(0, load_file("./keys/server-cert.pem", &cert, &certSz)); +#ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256 + AssertIntEQ(WS_SUCCESS, wolfSSH_ReadCert_buffer(cert, certSz, + &out, &outSz, &outType, &outTypeSz, &flavor, NULL)); + AssertNotNull(out); + AssertIntGT(outSz, 0); + AssertIntEQ(out[0], 0x30); + AssertIntEQ(flavor, WOLFSSH_CERT_FLAVOR_X509); + AssertStrEQ((const char*)outType, "x509v3-ecdsa-sha2-nistp256"); + AssertIntEQ(outTypeSz, (word32)WSTRLEN((const char*)outType)); + WFREE(out, NULL, DYNTYPE_CERT); + out = NULL; +#else + /* Every fixture is ECDSA P-256. With its x509v3 name compiled out the key + * identifies but has no name to be reported under. */ + AssertIntEQ(WS_INVALID_ALGO_ID, wolfSSH_ReadCert_buffer(cert, certSz, + &out, &outSz, &outType, &outTypeSz, &flavor, NULL)); + AssertNull(out); +#endif + + /* Half a PEM loses the CERTIFICATE header, so the sniff declines it. */ + AssertIntEQ(WS_BAD_FILETYPE_E, wolfSSH_ReadCert_buffer(cert, certSz / 2, + &out, &outSz, &outType, &outTypeSz, &flavor, NULL)); + free(cert); + cert = NULL; + + AssertIntEQ(0, load_file("./keys/server-cert.der", &cert, &certSz)); +#ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256 + AssertIntEQ(WS_SUCCESS, wolfSSH_ReadCert_buffer(cert, certSz, + &out, &outSz, &outType, &outTypeSz, &flavor, NULL)); + AssertNotNull(out); + AssertIntEQ(outSz, certSz); + AssertIntEQ(0, WMEMCMP(out, cert, certSz)); + AssertIntEQ(flavor, WOLFSSH_CERT_FLAVOR_X509); + AssertStrEQ((const char*)outType, "x509v3-ecdsa-sha2-nistp256"); + WFREE(out, NULL, DYNTYPE_CERT); + out = NULL; +#else + AssertIntEQ(WS_INVALID_ALGO_ID, wolfSSH_ReadCert_buffer(cert, certSz, + &out, &outSz, &outType, &outTypeSz, &flavor, NULL)); + AssertNull(out); +#endif + free(cert); + cert = NULL; + + /* Not a certificate behind the DER header. The wolfSSL error is mapped, + * so a WS_ code reaches the caller. The read above left a name behind, so + * this also shows a failure clearing one. */ + AssertIntEQ(WS_PARSE_E, wolfSSH_ReadCert_buffer( + (const byte*)"\x30\x82\x01\x02", + 4, &out, &outSz, &outType, &outTypeSz, &flavor, NULL)); + AssertNull(out); + AssertIntEQ(outSz, 0); + AssertNull(outType); + AssertIntEQ(outTypeSz, 0); + + /* A private key is the likeliest mistake, and the DER one also leads with + * 0x30, so only the parse tells them apart. */ + AssertIntEQ(0, load_file("./keys/server-key-ecc.der", &cert, &certSz)); + AssertIntEQ(WS_PARSE_E, wolfSSH_ReadCert_buffer(cert, certSz, + &out, &outSz, &outType, &outTypeSz, &flavor, NULL)); + AssertNull(out); + free(cert); + cert = NULL; + + /* The PEM one has no CERTIFICATE header, so the sniff declines it. Every + * out param goes in set to prove a rejection clears them all. */ + AssertIntEQ(0, load_file("./keys/server-key-ecc.pem", &cert, &certSz)); + out = cert; + outSz = 0xDEADBEEF; + outType = cert; + outTypeSz = 0xDEADBEEF; + flavor = WOLFSSH_CERT_FLAVOR_X509; + AssertIntEQ(WS_BAD_FILETYPE_E, wolfSSH_ReadCert_buffer(cert, certSz, + &out, &outSz, &outType, &outTypeSz, &flavor, NULL)); + AssertNull(out); + AssertIntEQ(outSz, 0); + AssertNull(outType); + AssertIntEQ(outTypeSz, 0); + AssertIntEQ(flavor, WOLFSSH_CERT_FLAVOR_UNKNOWN); + free(cert); + cert = NULL; + +#ifndef WOLFSSH_NO_ED25519 + /* Ed25519 must never load: it has no x509v3 name, and today the key + * inside is not identified either. Which of the two rejections lands + * depends on how wolfSSL returns a cert's public key, so accept both. */ + AssertIntEQ(0, load_file("./keys/server-cert-ed25519.der", + &cert, &certSz)); + ret = wolfSSH_ReadCert_buffer(cert, certSz, + &out, &outSz, &outType, &outTypeSz, &flavor, NULL); + AssertTrue(ret == WS_UNIMPLEMENTED_E || ret == WS_INVALID_ALGO_ID); + AssertNull(out); + free(cert); + cert = NULL; + + /* The PEM form makes that same DER first, so rejecting it must free it. */ + AssertIntEQ(0, load_file("./keys/server-cert-ed25519.pem", + &cert, &certSz)); + ret = wolfSSH_ReadCert_buffer(cert, certSz, + &out, &outSz, &outType, &outTypeSz, &flavor, NULL); + AssertTrue(ret == WS_UNIMPLEMENTED_E || ret == WS_INVALID_ALGO_ID); + AssertNull(out); + AssertIntEQ(flavor, WOLFSSH_CERT_FLAVOR_UNKNOWN); + free(cert); + cert = NULL; +#endif /* WOLFSSH_NO_ED25519 */ +#endif /* WOLFSSH_CERTS */ +} + + +static void test_wolfSSH_ReadCert_file(void) +{ +/* The arguments are checked ahead of any certificate, so those cases hold for + * an OpenSSH-only build too. */ +#if !defined(NO_FILESYSTEM) && !defined(WOLFSSH_USER_FILESYSTEM) + byte* out = NULL; + word32 outSz = 0; + const byte* outType = NULL; + word32 outTypeSz = 0; + byte flavor = 0xFF; + byte stale[1]; + + AssertIntEQ(WS_BAD_FILE_E, wolfSSH_ReadCert_file(NULL, + &out, &outSz, &outType, &outTypeSz, &flavor, NULL)); + AssertIntEQ(WS_BAD_ARGUMENT, wolfSSH_ReadCert_file("./keys/server-cert.pem", + NULL, &outSz, &outType, &outTypeSz, &flavor, NULL)); + + /* A file that never opens still clears every out parameter. Sentinels go + in, so a stale pointer surviving the call would fail here. */ + out = stale; + outSz = 0xDEADBEEF; + outType = stale; + outTypeSz = 0xDEADBEEF; + flavor = 0xFF; + AssertIntEQ(WS_BAD_FILE_E, wolfSSH_ReadCert_file("./keys/no-such-cert.pem", + &out, &outSz, &outType, &outTypeSz, &flavor, NULL)); + AssertNull(out); + AssertIntEQ(outSz, 0); + AssertNull(outType); + AssertIntEQ(outTypeSz, 0); + AssertIntEQ(flavor, WOLFSSH_CERT_FLAVOR_UNKNOWN); + + /* A directory fails further in, opening but not reading. */ + out = stale; + outSz = 0xDEADBEEF; + flavor = 0xFF; + AssertIntEQ(WS_BAD_FILE_E, wolfSSH_ReadCert_file("./keys", + &out, &outSz, &outType, &outTypeSz, &flavor, NULL)); + AssertNull(out); + AssertIntEQ(outSz, 0); + AssertIntEQ(flavor, WOLFSSH_CERT_FLAVOR_UNKNOWN); + +#if defined(WOLFSSH_CERTS) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) + AssertIntEQ(WS_SUCCESS, wolfSSH_ReadCert_file("./keys/server-cert.pem", + &out, &outSz, &outType, &outTypeSz, &flavor, NULL)); + AssertNotNull(out); + AssertIntEQ(out[0], 0x30); + AssertIntEQ(flavor, WOLFSSH_CERT_FLAVOR_X509); + AssertStrEQ((const char*)outType, "x509v3-ecdsa-sha2-nistp256"); + WFREE(out, NULL, DYNTYPE_CERT); + out = NULL; + + AssertIntEQ(WS_SUCCESS, wolfSSH_ReadCert_file("./keys/server-cert.der", + &out, &outSz, &outType, &outTypeSz, &flavor, NULL)); + AssertNotNull(out); + AssertIntEQ(out[0], 0x30); + AssertIntEQ(flavor, WOLFSSH_CERT_FLAVOR_X509); + WFREE(out, NULL, DYNTYPE_CERT); +#endif /* WOLFSSH_CERTS && !WOLFSSH_NO_ECDSA_SHA2_NISTP256 */ +#endif +} + + +static void test_wolfSSH_CTX_UseCert_file(void) +{ +#if defined(WOLFSSH_CERTS) && !defined(NO_FILESYSTEM) && \ + !defined(WOLFSSH_USER_FILESYSTEM) && !defined(WOLFSSH_NO_SERVER) + WOLFSSH_CTX* ctx = NULL; +#ifndef WOLFSSH_NO_ED25519 + int ret; +#endif + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + AssertNotNull(ctx); + + AssertIntEQ(WS_BAD_ARGUMENT, + wolfSSH_CTX_UseCert_file(NULL, "./keys/server-cert.pem")); + AssertIntEQ(WS_BAD_ARGUMENT, wolfSSH_CTX_UseCert_file(ctx, NULL)); + AssertIntEQ(WS_BAD_FILE_E, + wolfSSH_CTX_UseCert_file(ctx, "./keys/no-such-cert.pem")); + + /* Both encodings load through the same call. */ +#ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256 + AssertIntEQ(WS_SUCCESS, + wolfSSH_CTX_UseCert_file(ctx, "./keys/server-cert.pem")); + AssertIntEQ(WS_SUCCESS, + wolfSSH_CTX_UseCert_file(ctx, "./keys/server-cert.der")); +#endif + + /* A CTX takes the certificate itself, not a public key line. */ + AssertIntEQ(WS_BAD_FILETYPE_E, + wolfSSH_CTX_UseCert_file(ctx, "./keys/id_ecdsa.pub")); +#ifdef WOLFSSH_TEST_OSSH_CERT_FILE + AssertIntEQ(0, writeTmpFile(osshCertPath, osshCertLine, + WSTRLEN(osshCertLine))); + AssertIntEQ(WS_BAD_FILETYPE_E, wolfSSH_CTX_UseCert_file(ctx, osshCertPath)); + AssertIntEQ(0, remove(osshCertPath)); +#endif + +#ifndef WOLFSSH_NO_ED25519 + /* Refused here too, on the same codes as wolfSSH_ReadCert_file(). */ + ret = wolfSSH_CTX_UseCert_file(ctx, "./keys/server-cert-ed25519.der"); + AssertTrue(ret == WS_UNIMPLEMENTED_E || ret == WS_INVALID_ALGO_ID); + ret = wolfSSH_CTX_UseCert_file(ctx, "./keys/server-cert-ed25519.pem"); + AssertTrue(ret == WS_UNIMPLEMENTED_E || ret == WS_INVALID_ALGO_ID); +#endif + + wolfSSH_CTX_free(ctx); +#endif +} + + +static void test_wolfSSH_CTX_AddRootCert_file(void) +{ +#if defined(WOLFSSH_CERTS) && !defined(NO_FILESYSTEM) && \ + !defined(WOLFSSH_USER_FILESYSTEM) && !defined(WOLFSSH_NO_SERVER) + WOLFSSH_CTX* ctx = NULL; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + AssertNotNull(ctx); + + AssertIntEQ(WS_BAD_ARGUMENT, + wolfSSH_CTX_AddRootCert_file(NULL, "./keys/ca-cert-ecc.pem")); + AssertIntEQ(WS_BAD_ARGUMENT, wolfSSH_CTX_AddRootCert_file(ctx, NULL)); + AssertIntEQ(WS_BAD_FILE_E, + wolfSSH_CTX_AddRootCert_file(ctx, "./keys/no-such-ca.pem")); + + AssertIntEQ(WS_SUCCESS, + wolfSSH_CTX_AddRootCert_file(ctx, "./keys/ca-cert-ecc.pem")); + AssertIntEQ(WS_SUCCESS, + wolfSSH_CTX_AddRootCert_file(ctx, "./keys/ca-cert-ecc.der")); + + /* A CTX takes the certificate itself, not a public key line. */ + AssertIntEQ(WS_BAD_FILETYPE_E, + wolfSSH_CTX_AddRootCert_file(ctx, "./keys/id_ecdsa.pub")); + + /* The cert manager rejects a non-CA in wolfSSL's codes; this path maps. */ + AssertIntEQ(WS_PARSE_E, + wolfSSH_CTX_AddRootCert_file(ctx, "./keys/server-key-ecc.der")); +#ifdef WOLFSSH_TEST_OSSH_CERT_FILE + AssertIntEQ(0, writeTmpFile(osshCertPath, osshCertLine, + WSTRLEN(osshCertLine))); + AssertIntEQ(WS_BAD_FILETYPE_E, + wolfSSH_CTX_AddRootCert_file(ctx, osshCertPath)); + AssertIntEQ(0, remove(osshCertPath)); +#endif + + wolfSSH_CTX_free(ctx); +#endif +} + +#else + +static void test_wolfSSH_ReadCert_buffer(void) { ; } +static void test_wolfSSH_ReadCert_file(void) { ; } +static void test_wolfSSH_CTX_UseCert_file(void) { ; } +static void test_wolfSSH_CTX_AddRootCert_file(void) { ; } + +#endif /* WOLFSSH_CERTS || WOLFSSH_OSSH_CERTS */ + + static void test_wolfSSH_CTX_UsePrivateKey_buffer_pem(void) { #if defined(WOLFSSH_CERTS) && !defined(WOLFSSH_NO_SERVER) @@ -2204,6 +2572,124 @@ static const byte ossh_vec_ecc[] = { }; #endif /* WOLFSSH_NO_ECDSA_SHA2_NISTP256 */ +#ifdef WOLFSSL_BASE64_ENCODE + +/* Renders a blob as the "name base64" line an authorized_keys style file + * holds. Returns the length written to line. */ +static word32 buildOsshCertLine(byte* line, word32 lineCap, + const byte* vec, word32 vecSz) +{ + static const char name[] = "ssh-ed25519-cert-v01@openssh.com "; + word32 nameSz, b64Sz; + + nameSz = (word32)WSTRLEN(name); + AssertIntGT(lineCap, nameSz + 2); + WMEMCPY(line, name, nameSz); + + b64Sz = lineCap - nameSz - 2; + AssertIntEQ(0, Base64_Encode_NoNl(vec, vecSz, line + nameSz, &b64Sz)); + line[nameSz + b64Sz] = '\n'; + + return nameSz + b64Sz + 1; +} + + +/* Reads a certificate vector back through the public API in the "name base64" + * line form an authorized_keys style file holds. */ +static void test_wolfSSH_ReadCert_buffer_ossh(void) +{ + byte line[2048]; + byte* out = NULL; + const byte* outType = NULL; + word32 outSz = 0, outTypeSz = 0, lineSz; + byte flavor = 0xFF; + + lineSz = buildOsshCertLine(line, (word32)sizeof(line), + ossh_vec_ed, (word32)sizeof(ossh_vec_ed)); + + AssertIntEQ(WS_SUCCESS, wolfSSH_ReadCert_buffer(line, lineSz, + &out, &outSz, &outType, &outTypeSz, &flavor, NULL)); + AssertNotNull(out); + AssertIntEQ(outSz, (word32)sizeof(ossh_vec_ed)); + AssertIntEQ(0, WMEMCMP(out, ossh_vec_ed, sizeof(ossh_vec_ed))); + AssertIntEQ(flavor, WOLFSSH_CERT_FLAVOR_OSSH); + AssertStrEQ((const char*)outType, "ssh-ed25519-cert-v01@openssh.com"); + AssertIntEQ(outTypeSz, (word32)WSTRLEN((const char*)outType)); + WFREE(out, NULL, DYNTYPE_CERT); + out = NULL; + + /* Half a blob still forms a valid line, so the parse must reject it. The + * line names its algorithm, so outType is set before that parse runs and + * has to be taken back with the rest. */ + lineSz = buildOsshCertLine(line, (word32)sizeof(line), + ossh_vec_ed, (word32)sizeof(ossh_vec_ed) / 2); + + AssertIntLT(wolfSSH_ReadCert_buffer(line, lineSz, + &out, &outSz, &outType, &outTypeSz, &flavor, NULL), 0); + AssertNull(out); + AssertIntEQ(outSz, 0); + AssertNull(outType); + AssertIntEQ(outTypeSz, 0); + AssertIntEQ(flavor, WOLFSSH_CERT_FLAVOR_UNKNOWN); +} + +#else + +static void test_wolfSSH_ReadCert_buffer_ossh(void) { ; } + +#endif /* WOLFSSL_BASE64_ENCODE */ + +#ifdef WOLFSSH_TEST_OSSH_CERT_FILE + +/* Reads the same line form from a file. This is the only certificate the + * file reader takes in a build without X.509 support. */ +static void test_wolfSSH_ReadCert_file_ossh(void) +{ + byte* out = NULL; + const byte* outType = NULL; + word32 outSz = 0, outTypeSz = 0; + byte flavor = 0xFF; +#ifdef WOLFSSL_BASE64_ENCODE + byte line[2048]; + word32 lineSz; + + lineSz = buildOsshCertLine(line, (word32)sizeof(line), + ossh_vec_ed, (word32)sizeof(ossh_vec_ed)); + AssertIntEQ(0, writeTmpFile(osshCertPath, line, lineSz)); + + AssertIntEQ(WS_SUCCESS, wolfSSH_ReadCert_file(osshCertPath, + &out, &outSz, &outType, &outTypeSz, &flavor, NULL)); + AssertNotNull(out); + AssertIntEQ(outSz, (word32)sizeof(ossh_vec_ed)); + AssertIntEQ(0, WMEMCMP(out, ossh_vec_ed, sizeof(ossh_vec_ed))); + AssertIntEQ(flavor, WOLFSSH_CERT_FLAVOR_OSSH); + AssertStrEQ((const char*)outType, "ssh-ed25519-cert-v01@openssh.com"); + AssertIntEQ(outTypeSz, (word32)WSTRLEN((const char*)outType)); + WFREE(out, NULL, DYNTYPE_CERT); + out = NULL; + AssertIntEQ(0, remove(osshCertPath)); +#endif /* WOLFSSL_BASE64_ENCODE */ + + /* The name only routes the file; the blob still has to parse. Rejecting + * it clears the name the line had already supplied. */ + AssertIntEQ(0, writeTmpFile(osshCertPath, osshCertLine, + WSTRLEN(osshCertLine))); + AssertIntLT(wolfSSH_ReadCert_file(osshCertPath, + &out, &outSz, &outType, &outTypeSz, &flavor, NULL), 0); + AssertNull(out); + AssertIntEQ(outSz, 0); + AssertNull(outType); + AssertIntEQ(outTypeSz, 0); + AssertIntEQ(flavor, WOLFSSH_CERT_FLAVOR_UNKNOWN); + AssertIntEQ(0, remove(osshCertPath)); +} + +#else + +static void test_wolfSSH_ReadCert_file_ossh(void) { ; } + +#endif /* WOLFSSH_TEST_OSSH_CERT_FILE */ + /* Parse, verify the CA signature, and validate the options of each committed * certificate vector; then flip the final signature byte and confirm the * verification fails while the parse still succeeds. */ @@ -5773,6 +6259,10 @@ int wolfSSH_ApiTest(int argc, char** argv) test_wolfSSH_ConvertConsole(); test_wolfSSH_CTX_UsePrivateKey_buffer(); test_wolfSSH_CTX_UseCert_buffer(); + test_wolfSSH_CTX_UseCert_file(); + test_wolfSSH_CTX_AddRootCert_file(); + test_wolfSSH_ReadCert_buffer(); + test_wolfSSH_ReadCert_file(); test_wolfSSH_CTX_UsePrivateKey_buffer_pem(); test_wolfSSH_CTX_SetWindowPacketSize(); test_wolfSSH_CertMan(); @@ -5801,6 +6291,8 @@ int wolfSSH_ApiTest(int argc, char** argv) #endif #ifdef WOLFSSH_OSSH_CERTS #ifndef WOLFSSH_NO_ED25519 + test_wolfSSH_ReadCert_buffer_ossh(); + test_wolfSSH_ReadCert_file_ossh(); test_wolfSSH_OsshCert_valid(); #ifdef WOLFSSH_TEST_OSSH_VEC_ECC test_wolfSSH_OsshCert_ecc_curve_mismatch(); diff --git a/wolfssh/internal.h b/wolfssh/internal.h index eb6554f44..c5e6a5974 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -1328,6 +1328,9 @@ WOLFSSH_LOCAL int GetOpenSshKey(WS_KeySignature *key, WOLFSSH_LOCAL int GetOpenSshPublicKey(WS_KeySignature *key, const byte* buf, word32 len, word32* idx); #endif +#ifdef WOLFSSH_CERTS +WOLFSSH_LOCAL int IdentifyCert(const byte* in, word32 inSz, void* heap); +#endif /* Parsing functions */ diff --git a/wolfssh/ssh.h b/wolfssh/ssh.h index 57e8f2452..96fef949e 100644 --- a/wolfssh/ssh.h +++ b/wolfssh/ssh.h @@ -104,6 +104,19 @@ WOLFSSH_API int wolfSSH_ReadKey_file(const char* name, byte** out, word32* outSz, const byte** outType, word32* outTypeSz, byte* isPrivate, void* heap); +#if defined(WOLFSSH_CERTS) || defined(WOLFSSH_OSSH_CERTS) +/* Decodes a PEM/DER X.509 cert or OpenSSH cert line, detected from content. + * Caller frees out via heap; on failure every out param is cleared. */ +WOLFSSH_API int wolfSSH_ReadCert_buffer(const byte* in, word32 inSz, + byte** out, word32* outSz, const byte** outType, word32* outTypeSz, + byte* flavor, void* heap); +#if !defined(NO_FILESYSTEM) && !defined(WOLFSSH_USER_FILESYSTEM) +WOLFSSH_API int wolfSSH_ReadCert_file(const char* name, + byte** out, word32* outSz, const byte** outType, word32* outTypeSz, + byte* flavor, void* heap); +#endif +#endif /* WOLFSSH_CERTS || WOLFSSH_OSSH_CERTS */ + /* SetAlgoList* validate the list, returning WS_SUCCESS, WS_INVALID_ALGO_ID for * a bad list, or WS_SSH_CTX_NULL_E / WS_SSH_NULL_E for a NULL ctx / ssh. * Kex/Cipher/Mac reject NULL. Key accepts NULL only on a server, restoring the @@ -494,6 +507,13 @@ WOLFSSH_API int wolfSSH_CTX_UsePrivateKey_buffer(WOLFSSH_CTX* ctx, const byte* cert, word32 certSz, int format); WOLFSSH_API int wolfSSH_CTX_AddRootCert_buffer(WOLFSSH_CTX* ctx, const byte* cert, word32 certSz, int format); + #if !defined(NO_FILESYSTEM) && !defined(WOLFSSH_USER_FILESYSTEM) + /* PEM or DER is detected from the file's content. */ + WOLFSSH_API int wolfSSH_CTX_UseCert_file(WOLFSSH_CTX* ctx, + const char* name); + WOLFSSH_API int wolfSSH_CTX_AddRootCert_file(WOLFSSH_CTX* ctx, + const char* name); + #endif #endif /* WOLFSSH_CERTS */ WOLFSSH_API int wolfSSH_CTX_SetWindowPacketSize(WOLFSSH_CTX* ctx, word32 windowSz, word32 maxPacketSz); @@ -600,6 +620,16 @@ enum WS_FormatTypes { }; +/* X.509 certificates are consumed by UseCert/AddRootCert, OpenSSH ones by + the trusted user CA machinery. UNKNOWN leads so that a zeroed flavor is + not a claim about the content. */ +enum WS_CertFlavors { + WOLFSSH_CERT_FLAVOR_UNKNOWN, + WOLFSSH_CERT_FLAVOR_X509, + WOLFSSH_CERT_FLAVOR_OSSH +}; + + /* bit map */ #define WOLFSSH_USERAUTH_PASSWORD 0x01 #define WOLFSSH_USERAUTH_PUBLICKEY 0x02