diff --git a/src/internal.c b/src/internal.c index 2de2e45cc..771db22fd 100644 --- a/src/internal.c +++ b/src/internal.c @@ -2533,6 +2533,141 @@ int wolfSSH_SetHostTpmKey(WOLFSSH_CTX* ctx, byte keyId) #endif /* WOLFSSH_TPM */ +#ifdef WOLFSSH_CERTS + +/* Finds needle in the first inSz bytes of in. Unlike WSTRNSTR() an embedded + NUL does not end the search, the buffer being length delimited. */ +static const byte* FindInBuffer(const byte* in, word32 inSz, const char* needle) +{ + word32 needleSz; + word32 i; + + needleSz = (word32)WSTRLEN(needle); + if (needleSz == 0 || inSz < needleSz) { + return NULL; + } + + for (i = 0; i <= inSz - needleSz; i++) { + if (WMEMCMP(in + i, needle, needleSz) == 0) { + return in + i; + } + } + + return NULL; +} + + +/* Loads every PEM certificate block in the buffer as a root CA. Stops at the + first block that will not decode or that the cert manager refuses; what the + blocks ahead of it left installed is unspecified. */ +static int LoadRootCaPemBuffer(WOLFSSH_CTX* ctx, const byte* in, word32 inSz) +{ + EncryptedInfo info; + DerBuffer* der = NULL; + const char* certHeader = NULL; + const byte* found; + word32 used = 0; + word32 count = 0; + int wcType = CA_TYPE; + int ret; +#ifdef WOLFSSH_HAVE_TRUSTED_CERT_PEM + const char* trustedHeader = NULL; + const byte* foundTrusted; +#endif + + if (ctx->certMan == NULL) { + WLOG(WS_LOG_DEBUG, "Error no cert manager set"); + return WS_MEMORY_E; + } + + if (wc_PemGetHeaderFooter(CA_TYPE, &certHeader, NULL) != 0 + || certHeader == NULL) { + return WS_BAD_FILE_E; + } +#ifdef WOLFSSH_HAVE_TRUSTED_CERT_PEM + if (wc_PemGetHeaderFooter(TRUSTED_CERT_TYPE, &trustedHeader, NULL) != 0 + || trustedHeader == NULL) { + return WS_BAD_FILE_E; + } +#endif + + ret = WS_SUCCESS; + +#ifdef WOLFSSH_HAVE_TRUSTED_CERT_PEM + /* The trusted form is rare, so find it once and look again only after the + walk has passed it, rather than rescanning the tail every block. */ + foundTrusted = FindInBuffer(in, inSz, trustedHeader); +#endif + + while (used < inSz) { + /* Text may sit between blocks, so seek the next header. */ + found = FindInBuffer(in + used, inSz - used, certHeader); +#ifdef WOLFSSH_HAVE_TRUSTED_CERT_PEM + if (foundTrusted != NULL && foundTrusted < in + used) { + foundTrusted = FindInBuffer(in + used, inSz - used, trustedHeader); + } + + /* wc_PemToDer() takes either form, but only finds the one its type + names first, so whichever header leads picks the type. */ + if (found == NULL || (foundTrusted != NULL && foundTrusted < found)) { + found = foundTrusted; + wcType = TRUSTED_CERT_TYPE; + } + else { + wcType = CA_TYPE; + } +#endif + + if (found == NULL) { + break; + } + used = (word32)(found - in); + + WMEMSET(&info, 0, sizeof(info)); + if (wc_PemToDer(in + used, (long)(inSz - used), wcType, &der, + ctx->heap, &info, NULL) != 0) { + /* A body that will not decode is reported after the buffer is + allocated, so free it here too. */ + wc_FreeDer(&der); + WLOG(WS_LOG_DEBUG, "PEM to DER of CA %u failed", count); + ret = WS_PARSE_E; + break; + } + + ret = wolfSSH_CERTMAN_LoadRootCA_buffer(ctx->certMan, + der->buffer, der->length); + /* 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; + } + wc_FreeDer(&der); + + if (ret != WS_SUCCESS) { + WLOG(WS_LOG_DEBUG, "Error %d loading in CA %u", ret, count); + break; + } + + /* A block that consumes nothing would stall the walk, so stop. */ + if (info.consumed <= 0) { + ret = WS_BAD_FILE_E; + break; + } + used += (word32)info.consumed; + count++; + } + + if (ret == WS_SUCCESS && count == 0) { + WLOG(WS_LOG_DEBUG, "No certificate in the CA buffer"); + ret = WS_BAD_FILE_E; + } + + return ret; +} + +#endif /* WOLFSSH_CERTS */ + + int wolfSSH_ProcessBuffer(WOLFSSH_CTX* ctx, const byte* in, word32 inSz, int format, int type) @@ -2585,6 +2720,13 @@ int wolfSSH_ProcessBuffer(WOLFSSH_CTX* ctx, derSz = inSz; } else if (format == WOLFSSH_FORMAT_PEM) { + #ifdef WOLFSSH_CERTS + if (type == BUFTYPE_CA) { + /* A CA buffer may hold a bundle, so every block is loaded. */ + return LoadRootCaPemBuffer(ctx, in, inSz); + } + #endif /* WOLFSSH_CERTS */ + /* The der size will be smaller than the pem size. */ der = (byte*)WMALLOC(inSz, heap, dynamicType); if (der == NULL) diff --git a/tests/api.c b/tests/api.c index d2c25e407..13b917035 100644 --- a/tests/api.c +++ b/tests/api.c @@ -1168,6 +1168,85 @@ static void test_wolfSSH_CTX_SetWindowPacketSize(void) } +#if defined(WOLFSSH_CERTS) && !defined(NO_WOLFSSH_SERVER) && \ + !defined(WOLFSSH_NO_ECDSA) + +/* Joins two buffers so a multi-block PEM can be built in memory. Returns 0 on + * success. */ +static int catBuffers(const byte* a, word32 aSz, const byte* b, word32 bSz, + byte** out, word32* outSz) +{ + byte* buf; + int ret = -1; + + *out = NULL; + *outSz = 0; + + buf = (byte*)malloc(aSz + bSz); + if (buf != NULL) { + memcpy(buf, a, aSz); + memcpy(buf + aSz, b, bSz); + *out = buf; + *outSz = aSz + bSz; + ret = 0; + } + + return ret; +} + +#ifdef WOLFSSH_HAVE_TRUSTED_CERT_PEM + +/* Rewrites a certificate PEM into the trusted-certificate form wolfSSL also + * accepts for a CA. Returns 0 on success. */ +static int makeTrustedPem(const byte* pem, word32 pemSz, byte** out, + word32* outSz) +{ + static const char begin[] = "-----BEGIN CERTIFICATE-----"; + static const char end[] = "-----END CERTIFICATE-----"; + static const char tBegin[] = "-----BEGIN TRUSTED CERTIFICATE-----"; + static const char tEnd[] = "-----END TRUSTED CERTIFICATE-----\n"; + const char* b; + const char* e; + byte* buf; + word32 bodySz; + word32 sz; + + *out = NULL; + *outSz = 0; + + b = WSTRNSTR((const char*)pem, begin, pemSz); + e = WSTRNSTR((const char*)pem, end, pemSz); + if (b == NULL || e == NULL) { + return -1; + } + + b += sizeof(begin) - 1; + if (e <= b) { + return -1; + } + bodySz = (word32)(e - b); + sz = (word32)(sizeof(tBegin) - 1) + bodySz + (word32)(sizeof(tEnd) - 1); + + buf = (byte*)malloc(sz); + if (buf == NULL) { + return -1; + } + + memcpy(buf, tBegin, sizeof(tBegin) - 1); + memcpy(buf + sizeof(tBegin) - 1, b, bodySz); + memcpy(buf + sizeof(tBegin) - 1 + bodySz, tEnd, sizeof(tEnd) - 1); + + *out = buf; + *outSz = sz; + + return 0; +} + +#endif /* WOLFSSH_HAVE_TRUSTED_CERT_PEM */ + +#endif /* WOLFSSH_CERTS && !NO_WOLFSSH_SERVER && !WOLFSSH_NO_ECDSA */ + + #if defined(WOLFSSH_CERTS) && !defined(WOLFSSH_NO_ECDSA) /* Build the length-prefixed single-cert chain buffer that * wolfSSH_CERTMAN_VerifyCerts_buffer expects. Caller frees *chain. */ @@ -1311,6 +1390,232 @@ static void test_wolfSSH_CertMan(void) } +/* A CA buffer may hold a bundle, so every PEM block in it is loaded. */ +static void test_wolfSSH_CTX_AddRootCert_bundle(void) +{ +#if defined(WOLFSSH_CERTS) && !defined(NO_WOLFSSH_SERVER) && \ + !defined(WOLFSSH_NO_ECDSA) + static const char junk[] = "Bag Attributes: not a certificate\n"; + static const char badPem[] = + "-----BEGIN CERTIFICATE-----\n" + "$$$$ not base64 $$$$\n" + "-----END CERTIFICATE-----\n"; + /* Valid base64, but no certificate, so the manager is what refuses it. */ + static const char notACertPem[] = + "-----BEGIN CERTIFICATE-----\n" + "bm90IGEgY2VydGlmaWNhdGUgYXQgYWxsLCBqdXN0IHRleHQ=\n" + "-----END CERTIFICATE-----\n"; + /* Same block behind text holding a NUL. */ + static const char nulBadPem[] = + "Bag Attributes\0more text\n" + "-----BEGIN CERTIFICATE-----\n" + "$$$$ not base64 $$$$\n" + "-----END CERTIFICATE-----\n"; + WOLFSSH_CTX* ctx = NULL; + WOLFSSH_CTX* ctxFirst = NULL; + byte* ca = NULL; + byte* leaf = NULL; +#ifdef WOLFSSH_HAVE_TRUSTED_CERT_PEM + byte* trusted = NULL; + byte* trustedBad = NULL; + byte* triple = NULL; + word32 trustedSz = 0; + word32 trustedBadSz = 0; + word32 tripleSz = 0; +#endif + byte* bundle = NULL; + byte* leafDer = NULL; + byte* chain = NULL; + word32 caSz = 0; + word32 leafSz = 0; + word32 bundleSz = 0; + word32 leafDerSz = 0; + word32 chainSz = 0; + + AssertIntEQ(0, load_file("./keys/ca-cert-ecc.pem", &ca, &caSz)); + AssertIntEQ(0, load_file("./keys/server-cert.pem", &leaf, &leafSz)); + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + AssertNotNull(ctx); + + /* The CA is the second block, so anything it signs verifies only if the + * walk got past the first. */ + AssertIntEQ(0, catBuffers(leaf, leafSz, ca, caSz, &bundle, &bundleSz)); + AssertIntEQ(WS_SUCCESS, + wolfSSH_CTX_AddRootCert_buffer(ctx, bundle, bundleSz, + WOLFSSH_FORMAT_PEM)); + free(bundle); + bundle = NULL; + + AssertIntEQ(0, load_file("./keys/fred-cert.der", &leafDer, &leafDerSz)); + AssertIntEQ(0, certman_make_chain(leafDer, leafDerSz, &chain, &chainSz)); +#ifdef WOLFSSH_NO_FPKI + AssertIntEQ(WS_SUCCESS, + wolfSSH_CERTMAN_VerifyCerts_buffer(ctx->certMan, chain, chainSz, 1)); +#else + /* An FPKI build rejects this leaf's profile, but only after finding its + * signer, so the code still tells the CA apart from a missing one. */ + AssertIntEQ(WS_CERT_PROFILE_E, + wolfSSH_CERTMAN_VerifyCerts_buffer(ctx->certMan, chain, chainSz, 1)); +#endif + free(chain); + free(leafDer); + +#ifdef WOLFSSH_HAVE_TRUSTED_CERT_PEM + /* A trusted-certificate block names a CA too, alone and beside a plain + * one. */ + AssertIntEQ(0, makeTrustedPem(ca, caSz, &trusted, &trustedSz)); + AssertIntEQ(WS_SUCCESS, + wolfSSH_CTX_AddRootCert_buffer(ctx, trusted, trustedSz, + WOLFSSH_FORMAT_PEM)); + + AssertIntEQ(0, catBuffers(trusted, trustedSz, ca, caSz, &bundle, + &bundleSz)); + AssertIntEQ(WS_SUCCESS, + wolfSSH_CTX_AddRootCert_buffer(ctx, bundle, bundleSz, + WOLFSSH_FORMAT_PEM)); + + /* A second trusted block behind a plain one. The walk has to find it + * after consuming the first, and refusing it is what shows it did. */ + AssertIntEQ(0, makeTrustedPem((const byte*)badPem, + (word32)(sizeof(badPem) - 1), &trustedBad, &trustedBadSz)); + AssertIntEQ(0, catBuffers(bundle, bundleSz, trustedBad, trustedBadSz, + &triple, &tripleSz)); + AssertIntEQ(WS_PARSE_E, + wolfSSH_CTX_AddRootCert_buffer(ctx, triple, tripleSz, + WOLFSSH_FORMAT_PEM)); + free(triple); + free(trustedBad); + + free(bundle); + bundle = NULL; +#endif /* WOLFSSH_HAVE_TRUSTED_CERT_PEM */ + + /* The CA leads this bundle rather than closing it, so a walk that kept + * only the last block would leave the leaf without a signer. */ + ctxFirst = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + AssertNotNull(ctxFirst); + AssertIntEQ(0, catBuffers(ca, caSz, leaf, leafSz, &bundle, &bundleSz)); + AssertIntEQ(WS_SUCCESS, + wolfSSH_CTX_AddRootCert_buffer(ctxFirst, bundle, bundleSz, + WOLFSSH_FORMAT_PEM)); + free(bundle); + bundle = NULL; + + AssertIntEQ(0, load_file("./keys/fred-cert.der", &leafDer, &leafDerSz)); + AssertIntEQ(0, certman_make_chain(leafDer, leafDerSz, &chain, &chainSz)); +#ifdef WOLFSSH_NO_FPKI + AssertIntEQ(WS_SUCCESS, + wolfSSH_CERTMAN_VerifyCerts_buffer(ctxFirst->certMan, chain, + chainSz, 1)); +#else + AssertIntEQ(WS_CERT_PROFILE_E, + wolfSSH_CERTMAN_VerifyCerts_buffer(ctxFirst->certMan, chain, + chainSz, 1)); +#endif + free(chain); + free(leafDer); + wolfSSH_CTX_free(ctxFirst); + + /* A block the manager turns down is a different failure from one that + * will not decode. */ + AssertIntEQ(0, catBuffers(ca, caSz, (const byte*)notACertPem, + (word32)(sizeof(notACertPem) - 1), &bundle, &bundleSz)); + AssertIntEQ(WS_PARSE_E, + wolfSSH_CTX_AddRootCert_buffer(ctx, bundle, bundleSz, + WOLFSSH_FORMAT_PEM)); + free(bundle); + bundle = NULL; + + /* A block that will not decode fails the load wherever it sits. A + * trailing one used to go unread and the load answered WS_SUCCESS. */ + AssertIntEQ(0, catBuffers(ca, caSz, (const byte*)badPem, + (word32)(sizeof(badPem) - 1), &bundle, &bundleSz)); + AssertIntEQ(WS_PARSE_E, + wolfSSH_CTX_AddRootCert_buffer(ctx, bundle, bundleSz, + WOLFSSH_FORMAT_PEM)); + free(bundle); + bundle = NULL; + + /* A NUL is not the end of the buffer, so the bad block behind it is + * still read. */ + AssertIntEQ(0, catBuffers(ca, caSz, (const byte*)nulBadPem, + (word32)(sizeof(nulBadPem) - 1), &bundle, &bundleSz)); + AssertIntEQ(WS_PARSE_E, + wolfSSH_CTX_AddRootCert_buffer(ctx, bundle, bundleSz, + WOLFSSH_FORMAT_PEM)); + free(bundle); + bundle = NULL; + + AssertIntEQ(0, catBuffers((const byte*)badPem, (word32)(sizeof(badPem) - 1), + ca, caSz, &bundle, &bundleSz)); + AssertIntEQ(WS_PARSE_E, + wolfSSH_CTX_AddRootCert_buffer(ctx, bundle, bundleSz, + WOLFSSH_FORMAT_PEM)); + free(bundle); + bundle = NULL; + + /* Text around the blocks is not a certificate, so it is stepped over. */ + AssertIntEQ(0, catBuffers((const byte*)junk, (word32)(sizeof(junk) - 1), + ca, caSz, &bundle, &bundleSz)); + AssertIntEQ(WS_SUCCESS, + wolfSSH_CTX_AddRootCert_buffer(ctx, bundle, bundleSz, + WOLFSSH_FORMAT_PEM)); + free(bundle); + bundle = NULL; + + AssertIntEQ(0, catBuffers(ca, caSz, (const byte*)junk, + (word32)(sizeof(junk) - 1), &bundle, &bundleSz)); + AssertIntEQ(WS_SUCCESS, + wolfSSH_CTX_AddRootCert_buffer(ctx, bundle, bundleSz, + WOLFSSH_FORMAT_PEM)); + free(bundle); + + /* A buffer with no certificate in it is still a bad file. */ + AssertIntEQ(WS_BAD_FILE_E, + wolfSSH_CTX_AddRootCert_buffer(ctx, (const byte*)junk, + (word32)(sizeof(junk) - 1), WOLFSSH_FORMAT_PEM)); + + wolfSSH_CTX_free(ctx); + free(ca); + free(leaf); +#ifdef WOLFSSH_HAVE_TRUSTED_CERT_PEM + free(trusted); +#endif +#endif /* WOLFSSH_CERTS && !NO_WOLFSSH_SERVER && !WOLFSSH_NO_ECDSA */ +} + + +/* OpenSSL writes the trusted form as the certificate followed by its trust + * settings, which nothing here strips, so the readers decline it. */ +static void test_wolfSSH_ReadCert_buffer_trusted(void) +{ +#if defined(WOLFSSH_CERTS) && !defined(NO_WOLFSSH_SERVER) && \ + !defined(WOLFSSH_NO_ECDSA) && defined(WOLFSSH_HAVE_TRUSTED_CERT_PEM) + byte* cert = NULL; + byte* trusted = NULL; + byte* out = NULL; + const byte* outType = NULL; + word32 certSz = 0; + word32 trustedSz = 0; + word32 outSz = 0; + word32 outTypeSz = 0; + byte flavor = WOLFSSH_CERT_FLAVOR_UNKNOWN; + + AssertIntEQ(0, load_file("./keys/server-cert.pem", &cert, &certSz)); + AssertIntEQ(0, makeTrustedPem(cert, certSz, &trusted, &trustedSz)); + + AssertIntEQ(WS_BAD_FILETYPE_E, wolfSSH_ReadCert_buffer(trusted, trustedSz, + &out, &outSz, &outType, &outTypeSz, &flavor, NULL)); + AssertNull(out); + AssertIntEQ(flavor, WOLFSSH_CERT_FLAVOR_UNKNOWN); + + free(trusted); + free(cert); +#endif +} + + #define KEY_BUF_SZ 2048 #ifndef WOLFSSH_NO_RSA @@ -6261,6 +6566,8 @@ int wolfSSH_ApiTest(int argc, char** argv) test_wolfSSH_CTX_UseCert_buffer(); test_wolfSSH_CTX_UseCert_file(); test_wolfSSH_CTX_AddRootCert_file(); + test_wolfSSH_CTX_AddRootCert_bundle(); + test_wolfSSH_ReadCert_buffer_trusted(); test_wolfSSH_ReadCert_buffer(); test_wolfSSH_ReadCert_file(); test_wolfSSH_CTX_UsePrivateKey_buffer_pem(); diff --git a/wolfssh/internal.h b/wolfssh/internal.h index c5e6a5974..898a61c4c 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -109,8 +109,15 @@ extern "C" { #define WOLFSSL_V5_0_0 0x05000000 #define WOLFSSL_V5_7_0 0x05007000 #define WOLFSSL_V5_7_2 0x05007002 +#define WOLFSSL_V5_8_0 0x05008000 #define WOLFSSL_V5_9_2 0x05009002 +/* wolfSSL 5.8.0 added the trusted-certificate PEM header, both the + * TRUSTED_CERT_TYPE enum and PemToDer()'s fallback to it. */ +#if defined(WOLFSSH_CERTS) && (LIBWOLFSSL_VERSION_HEX >= WOLFSSL_V5_8_0) + #define WOLFSSH_HAVE_TRUSTED_CERT_PEM +#endif + /* wc_MlDsaKey_* / WC_MLDSA_* naming replaced the wc_Dilithium_* API in * wolfSSL 5.9.2. HAVE_DILITHIUM alone doesn't distinguish the two, so * require the version that has the new API too. */ diff --git a/wolfssh/ssh.h b/wolfssh/ssh.h index 96fef949e..a98b7de67 100644 --- a/wolfssh/ssh.h +++ b/wolfssh/ssh.h @@ -106,7 +106,8 @@ WOLFSSH_API int wolfSSH_ReadKey_file(const char* name, #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. */ + * Of several PEM certs, only the first is read. Caller frees out via heap; + * outType points at constant storage. 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); @@ -503,8 +504,12 @@ WOLFSSH_API int wolfSSH_CTX_UsePrivateKey_buffer(WOLFSSH_CTX* ctx, const byte* in, word32 inSz, int format); #ifdef WOLFSSH_CERTS + /* Takes the leaf; of several PEM certs, only the first is read. */ WOLFSSH_API int wolfSSH_CTX_UseCert_buffer(WOLFSSH_CTX* ctx, const byte* cert, word32 certSz, int format); + /* Loads every PEM cert in the buffer, so a bundle installs all of its + * CAs. A block that will not load fails the call; what the blocks ahead + * of it left installed is unspecified. */ 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)