diff --git a/src/internal.c b/src/internal.c index ad91ee0ed..ed530c940 100644 --- a/src/internal.c +++ b/src/internal.c @@ -2708,6 +2708,216 @@ 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; +} + + +#ifdef WOLFSSH_HAVE_TRUSTED_CERT_PEM +/* Reports whether the buffer holds only trusted-certificate PEM. A plain + certificate anywhere in it wins, the way wc_PemToDer() finds one first. */ +int IsTrustedCertPem(const byte* in, word32 inSz) +{ + const char* certHeader = NULL; + const char* trustedHeader = NULL; + + if (wc_PemGetHeaderFooter(CA_TYPE, &certHeader, NULL) != 0 + || certHeader == NULL + || wc_PemGetHeaderFooter(TRUSTED_CERT_TYPE, &trustedHeader, + NULL) != 0 + || trustedHeader == NULL) { + return 0; + } + + return FindInBuffer(in, inSz, certHeader) == NULL + && FindInBuffer(in, inSz, trustedHeader) != NULL; +} +#endif /* WOLFSSH_HAVE_TRUSTED_CERT_PEM */ + + +/* Loads every PEM certificate block in the buffer as a root CA, skipping the + ones that fail, the way wolfSSL's own chain loader treats a CA file. */ +static int LoadRootCaPemBuffer(WOLFSSH_CTX* ctx, const byte* in, word32 inSz) +{ + DerBuffer* der = NULL; + const char* certHeader = NULL; + const char* certFooter = NULL; + const char* footer; + const byte* found; + const byte* foundEnd; + const byte* nextHeader; + const byte* nextCert; + word32 certHeaderSz; + word32 certFooterSz; + word32 headerSz; + word32 footerSz; + word32 bodyStart; + word32 searchSz; + word32 blockSz; + word32 used = 0; + word32 loaded = 0; + word32 failed = 0; + int wcType = CA_TYPE; + int ret; +#ifdef WOLFSSH_HAVE_TRUSTED_CERT_PEM + const char* trustedHeader = NULL; + const char* trustedFooter = NULL; + const byte* nextTrusted; + word32 trustedHeaderSz; + word32 trustedFooterSz; +#endif + + if (ctx->certMan == NULL) { + WLOG(WS_LOG_DEBUG, "Error no cert manager set"); + return WS_MEMORY_E; + } + + if (wc_PemGetHeaderFooter(CA_TYPE, &certHeader, &certFooter) != 0 + || certHeader == NULL || certFooter == NULL) { + return WS_BAD_FILE_E; + } + certHeaderSz = (word32)WSTRLEN(certHeader); + certFooterSz = (word32)WSTRLEN(certFooter); +#ifdef WOLFSSH_HAVE_TRUSTED_CERT_PEM + if (wc_PemGetHeaderFooter(TRUSTED_CERT_TYPE, &trustedHeader, + &trustedFooter) != 0 + || trustedHeader == NULL || trustedFooter == NULL) { + return WS_BAD_FILE_E; + } + trustedHeaderSz = (word32)WSTRLEN(trustedHeader); + trustedFooterSz = (word32)WSTRLEN(trustedFooter); +#endif + + /* Each form's header is re-sought only from behind the last one found. */ + nextCert = FindInBuffer(in, inSz, certHeader); +#ifdef WOLFSSH_HAVE_TRUSTED_CERT_PEM + nextTrusted = FindInBuffer(in, inSz, trustedHeader); +#endif + + while (used < inSz) { + found = nextCert; + footer = certFooter; + headerSz = certHeaderSz; + footerSz = certFooterSz; + wcType = CA_TYPE; +#ifdef WOLFSSH_HAVE_TRUSTED_CERT_PEM + /* wc_PemToDer() takes either form, but only finds the one its type + names first, so whichever header leads picks the type. */ + if (found == NULL || (nextTrusted != NULL && nextTrusted < found)) { + found = nextTrusted; + footer = trustedFooter; + headerSz = trustedHeaderSz; + footerSz = trustedFooterSz; + wcType = TRUSTED_CERT_TYPE; + } +#endif + + if (found == NULL) { + break; + } + used = (word32)(found - in); + bodyStart = used + headerSz; + + /* Step this form's cursor past the header being read. */ +#ifdef WOLFSSH_HAVE_TRUSTED_CERT_PEM + if (wcType == TRUSTED_CERT_TYPE) { + nextTrusted = FindInBuffer(in + bodyStart, inSz - bodyStart, + trustedHeader); + } + else { + nextCert = FindInBuffer(in + bodyStart, inSz - bodyStart, + certHeader); + } +#else + nextCert = FindInBuffer(in + bodyStart, inSz - bodyStart, certHeader); +#endif + + nextHeader = (nextCert != NULL && nextCert >= in + bodyStart) ? + nextCert : NULL; +#ifdef WOLFSSH_HAVE_TRUSTED_CERT_PEM + if (nextTrusted != NULL && nextTrusted >= in + bodyStart + && (nextHeader == NULL || nextTrusted < nextHeader)) { + nextHeader = nextTrusted; + } +#endif + + /* A block runs to its footer, and the next header caps the search + for one. Bounding the search keeps the walk linear. */ + searchSz = (nextHeader != NULL) ? + (word32)(nextHeader - (in + bodyStart)) : inSz - bodyStart; + foundEnd = FindInBuffer(in + bodyStart, searchSz, footer); + + if (foundEnd == NULL) { + WLOG(WS_LOG_ERROR, "Skipping CA %u, nothing closes its header", + loaded + failed); + failed++; + used = bodyStart; + continue; + } + blockSz = (word32)(foundEnd - found) + footerSz; + + if (wc_PemToDer(found, (long)blockSz, wcType, &der, ctx->heap, + NULL, NULL) != 0) { + /* A bad body is reported after the buffer is allocated. */ + wc_FreeDer(&der); + WLOG(WS_LOG_ERROR, "Skipping CA %u, PEM to DER failed", + loaded + failed); + failed++; + } + else { + ret = wolfSSH_CERTMAN_LoadRootCA_buffer(ctx->certMan, + der->buffer, der->length); + wc_FreeDer(&der); + + if (ret != WS_SUCCESS) { + WLOG(WS_LOG_ERROR, "Skipping CA %u, error %d loading it", + loaded + failed, ret); + failed++; + } + else { + loaded++; + } + } + + used += blockSz; + } + + if (loaded > 0) { + ret = WS_SUCCESS; + } + else if (failed > 0) { + ret = WS_PARSE_E; + } + else { + WLOG(WS_LOG_ERROR, "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) @@ -2784,6 +2994,20 @@ int wolfSSH_ProcessBuffer(WOLFSSH_CTX* ctx, } } 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); + } + #ifdef WOLFSSH_HAVE_TRUSTED_CERT_PEM + if (type == BUFTYPE_CERT && IsTrustedCertPem(in, inSz)) { + /* The trust data behind the certificate means nothing to a peer. */ + WLOG(WS_LOG_DEBUG, "Trusted certificate form is for root CAs"); + return WS_BAD_FILETYPE_E; + } + #endif /* WOLFSSH_HAVE_TRUSTED_CERT_PEM */ + #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/src/ssh.c b/src/ssh.c index fb87e12aa..1f3f7cc49 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -2350,12 +2350,17 @@ int wolfSSH_ReadPublicKey_buffer(const byte* in, word32 inSz, int format, #ifdef WOLFSSH_CERTS static const char* CertBeginPrefix = "-----BEGIN CERTIFICATE-----"; #endif +#ifdef WOLFSSH_HAVE_TRUSTED_CERT_PEM + static const char* TrustedCertBeginPrefix = + "-----BEGIN TRUSTED 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. */ + An x509v3-* line holds a wire chain, not a certificate, so it is declined. + The trusted form is PEM too; the decoders decide who takes it. */ static int SniffCertForm(const byte* in, word32 inSz, byte* flavor, byte* certId) { @@ -2375,6 +2380,12 @@ static int SniffCertForm(const byte* in, word32 inSz, byte* flavor, *flavor = WOLFSSH_CERT_FLAVOR_X509; ret = WOLFSSH_FORMAT_PEM; } +#ifdef WOLFSSH_HAVE_TRUSTED_CERT_PEM + else if (WSTRNSTR((const char*)in, TrustedCertBeginPrefix, inSz) != NULL) { + *flavor = WOLFSSH_CERT_FLAVOR_X509; + ret = WOLFSSH_FORMAT_PEM; + } +#endif /* WOLFSSH_HAVE_TRUSTED_CERT_PEM */ #endif /* WOLFSSH_CERTS */ #ifdef WOLFSSH_OSSH_CERTS @@ -2421,6 +2432,14 @@ static int DoPemCert(const byte* in, word32 inSz, byte** out, word32* outSz, word32 derSz = 0; int ret; +#ifdef WOLFSSH_HAVE_TRUSTED_CERT_PEM + if (IsTrustedCertPem(in, inSz)) { + /* The trust data behind the certificate means nothing to a peer. */ + WLOG(WS_LOG_DEBUG, "Trusted certificate form is for root CAs"); + return WS_BAD_FILETYPE_E; + } +#endif + der = (byte*)WMALLOC(inSz, heap, DYNTYPE_CERT); if (der == NULL) { return WS_MEMORY_E; diff --git a/tests/api.c b/tests/api.c index 6bf387804..4210f0723 100644 --- a/tests/api.c +++ b/tests/api.c @@ -1328,6 +1328,86 @@ 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 + +/* Relabels a certificate PEM as the trusted form. The body is the plain + * certificate, without the trust settings OpenSSL appends, so this covers + * how the header is read rather than what follows it. 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. */ @@ -1471,6 +1551,341 @@ static void test_wolfSSH_CertMan(void) } +#if defined(WOLFSSH_CERTS) && !defined(NO_WOLFSSH_SERVER) && \ + !defined(WOLFSSH_NO_ECDSA) + +/* The CA signed fred's certificate, so a chain check tells an installed CA + * from a missing one. */ +static void assertCaInstalled(WOLFSSH_CTX* ctx) +{ + byte* leafDer = NULL; + byte* chain = NULL; + word32 leafDerSz = 0; + word32 chainSz = 0; + + 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); +} + +#endif /* WOLFSSH_CERTS && !NO_WOLFSSH_SERVER && !WOLFSSH_NO_ECDSA */ + + +/* A CA buffer may hold a bundle, so every PEM block in it is loaded. A block + * that will not load is skipped rather than failing the bundle. */ +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"; + /* A header with nothing closing it, so the walk has to resume from behind + * it rather than let it swallow the block that follows. */ + static const char headerOnlyPem[] = + "-----BEGIN 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* ctxOne = NULL; + byte* ca = NULL; + byte* leaf = NULL; +#ifdef WOLFSSH_HAVE_TRUSTED_CERT_PEM + byte* trusted = NULL; + byte* trustedLeaf = NULL; + word32 trustedSz = 0; + word32 trustedLeafSz = 0; +#endif + byte* bundle = NULL; + word32 caSz = 0; + word32 leafSz = 0; + word32 bundleSz = 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; + assertCaInstalled(ctx); + +#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)); + + /* A presented certificate carries no trust data, so the certificate + * path declines the form however the caller reached it. A plain block + * ahead of a trusted one is the certificate, so it is still taken. */ + AssertIntEQ(WS_BAD_FILETYPE_E, + wolfSSH_CTX_UseCert_buffer(ctx, trusted, trustedSz, + WOLFSSH_FORMAT_PEM)); + AssertIntEQ(0, catBuffers(leaf, leafSz, trusted, trustedSz, &bundle, + &bundleSz)); + AssertIntEQ(WS_SUCCESS, + wolfSSH_CTX_UseCert_buffer(ctx, bundle, bundleSz, + WOLFSSH_FORMAT_PEM)); + free(bundle); + bundle = NULL; + + AssertIntEQ(0, catBuffers(trusted, trustedSz, ca, caSz, &bundle, + &bundleSz)); + AssertIntEQ(WS_SUCCESS, + wolfSSH_CTX_AddRootCert_buffer(ctx, bundle, bundleSz, + WOLFSSH_FORMAT_PEM)); + free(bundle); + bundle = NULL; + + /* Two trusted blocks with the CA second, so only a walk that looks for + * a trusted header again after passing the first installs it. */ + ctxOne = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + AssertNotNull(ctxOne); + AssertIntEQ(0, makeTrustedPem(leaf, leafSz, &trustedLeaf, &trustedLeafSz)); + AssertIntEQ(0, catBuffers(trustedLeaf, trustedLeafSz, trusted, trustedSz, + &bundle, &bundleSz)); + AssertIntEQ(WS_SUCCESS, + wolfSSH_CTX_AddRootCert_buffer(ctxOne, bundle, bundleSz, + WOLFSSH_FORMAT_PEM)); + free(bundle); + bundle = NULL; + assertCaInstalled(ctxOne); + wolfSSH_CTX_free(ctxOne); + + /* A trusted block behind a bad plain one. Only a walk that stepped over + * the failure reaches it, and installing the CA is what shows it did. */ + ctxOne = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + AssertNotNull(ctxOne); + AssertIntEQ(0, catBuffers((const byte*)badPem, (word32)(sizeof(badPem) - 1), + trusted, trustedSz, &bundle, &bundleSz)); + AssertIntEQ(WS_SUCCESS, + wolfSSH_CTX_AddRootCert_buffer(ctxOne, bundle, bundleSz, + WOLFSSH_FORMAT_PEM)); + free(bundle); + bundle = NULL; + assertCaInstalled(ctxOne); + wolfSSH_CTX_free(ctxOne); +#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. */ + ctxOne = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + AssertNotNull(ctxOne); + AssertIntEQ(0, catBuffers(ca, caSz, leaf, leafSz, &bundle, &bundleSz)); + AssertIntEQ(WS_SUCCESS, + wolfSSH_CTX_AddRootCert_buffer(ctxOne, bundle, bundleSz, + WOLFSSH_FORMAT_PEM)); + free(bundle); + bundle = NULL; + assertCaInstalled(ctxOne); + wolfSSH_CTX_free(ctxOne); + + /* A block the manager turns down leaves the ones around it installed. */ + ctxOne = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + AssertNotNull(ctxOne); + AssertIntEQ(0, catBuffers((const byte*)notACertPem, + (word32)(sizeof(notACertPem) - 1), ca, caSz, &bundle, + &bundleSz)); + AssertIntEQ(WS_SUCCESS, + wolfSSH_CTX_AddRootCert_buffer(ctxOne, bundle, bundleSz, + WOLFSSH_FORMAT_PEM)); + free(bundle); + bundle = NULL; + assertCaInstalled(ctxOne); + wolfSSH_CTX_free(ctxOne); + + /* An unterminated header ahead of the CA. The block it opens has no end, + * so only a walk that steps past the header alone reaches the CA. */ + ctxOne = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + AssertNotNull(ctxOne); + AssertIntEQ(0, catBuffers((const byte*)headerOnlyPem, + (word32)(sizeof(headerOnlyPem) - 1), ca, caSz, &bundle, + &bundleSz)); + AssertIntEQ(WS_SUCCESS, + wolfSSH_CTX_AddRootCert_buffer(ctxOne, bundle, bundleSz, + WOLFSSH_FORMAT_PEM)); + free(bundle); + bundle = NULL; + assertCaInstalled(ctxOne); + wolfSSH_CTX_free(ctxOne); + + /* A block that will not decode is skipped wherever it sits, and the walk + * carries on from behind its header. */ + ctxOne = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + AssertNotNull(ctxOne); + AssertIntEQ(0, catBuffers((const byte*)badPem, (word32)(sizeof(badPem) - 1), + ca, caSz, &bundle, &bundleSz)); + AssertIntEQ(WS_SUCCESS, + wolfSSH_CTX_AddRootCert_buffer(ctxOne, bundle, bundleSz, + WOLFSSH_FORMAT_PEM)); + free(bundle); + bundle = NULL; + assertCaInstalled(ctxOne); + wolfSSH_CTX_free(ctxOne); + + AssertIntEQ(0, catBuffers(ca, caSz, (const byte*)badPem, + (word32)(sizeof(badPem) - 1), &bundle, &bundleSz)); + AssertIntEQ(WS_SUCCESS, + 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_SUCCESS, + 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); + bundle = NULL; + + /* Nothing installed is what fails the call. Blocks that all fail are a + * parse error, and a buffer holding no block at all is a bad file. */ + AssertIntEQ(WS_PARSE_E, + wolfSSH_CTX_AddRootCert_buffer(ctx, (const byte*)badPem, + (word32)(sizeof(badPem) - 1), WOLFSSH_FORMAT_PEM)); + AssertIntEQ(0, catBuffers((const byte*)badPem, (word32)(sizeof(badPem) - 1), + (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); + 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); + free(trustedLeaf); +#endif +#endif /* WOLFSSH_CERTS && !NO_WOLFSSH_SERVER && !WOLFSSH_NO_ECDSA */ +} + + +/* A CA file may be in the trusted form, which the certificate path still + * declines. */ +static void test_wolfSSH_CTX_AddRootCert_file_trusted(void) +{ +#if defined(WOLFSSH_CERTS) && !defined(NO_WOLFSSH_SERVER) && \ + !defined(WOLFSSH_NO_ECDSA) && defined(WOLFSSH_HAVE_TRUSTED_CERT_PEM) && \ + !defined(NO_FILESYSTEM) && !defined(WOLFSSH_USER_FILESYSTEM) + static const char trustedPath[] = "./trusted-ca-test.pem"; + WOLFSSH_CTX* ctx = NULL; + WFILE* fp = NULL; + byte* ca = NULL; + byte* trusted = NULL; + word32 caSz = 0; + word32 trustedSz = 0; + + AssertIntEQ(0, load_file("./keys/ca-cert-ecc.pem", &ca, &caSz)); + AssertIntEQ(0, makeTrustedPem(ca, caSz, &trusted, &trustedSz)); + + AssertIntEQ(WFOPEN(NULL, &fp, trustedPath, "wb"), 0); + AssertNotNull(fp); + AssertIntEQ((int)WFWRITE(NULL, trusted, 1, trustedSz, fp), (int)trustedSz); + WFCLOSE(NULL, fp); + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + AssertNotNull(ctx); + AssertIntEQ(WS_SUCCESS, wolfSSH_CTX_AddRootCert_file(ctx, trustedPath)); + assertCaInstalled(ctx); + + /* A presented certificate carries no trust data, so this form is not one + * of the certificate path's. */ + AssertIntEQ(WS_BAD_FILETYPE_E, wolfSSH_CTX_UseCert_file(ctx, trustedPath)); + + wolfSSH_CTX_free(ctx); + AssertIntEQ(0, remove(trustedPath)); + free(trusted); + free(ca); +#endif +} + + +/* Trust settings say what a root may be trusted for, which means nothing in + * a certificate sent to a peer, so the readers name the form and 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 @@ -6498,6 +6913,9 @@ 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_CTX_AddRootCert_file_trusted(); + 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 e0f516bcd..2afa59892 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -112,8 +112,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. */ @@ -1291,6 +1298,9 @@ WOLFSSH_LOCAL int ChannelCreditWindow(WOLFSSH* ssh, WOLFSSH_CHANNEL* channel, WOLFSSH_LOCAL int wolfSSH_ProcessBuffer(WOLFSSH_CTX* ctx, const byte* in, word32 inSz, int format, int type); +#ifdef WOLFSSH_HAVE_TRUSTED_CERT_PEM +WOLFSSH_LOCAL int IsTrustedCertPem(const byte* in, word32 inSz); +#endif #ifdef WOLFSSH_TPM WOLFSSH_LOCAL int wolfSSH_SetHostTpmKey(WOLFSSH_CTX* ctx, byte keyId); #endif diff --git a/wolfssh/ssh.h b/wolfssh/ssh.h index 2108013ee..dfb45ab60 100644 --- a/wolfssh/ssh.h +++ b/wolfssh/ssh.h @@ -106,8 +106,9 @@ 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. A body - * that will not decode is WS_PARSE_E; WS_BAD_FILE_E is from _file alone. */ + * Of several PEM certs, only the first is read. Caller frees out via heap; on + * failure out params are cleared. An undecodable body gives WS_PARSE_E, + * WS_BAD_FILE_E comes from _file alone. */ WOLFSSH_API int wolfSSH_ReadCert_buffer(const byte* in, word32 inSz, byte** out, word32* outSz, const byte** outType, word32* outTypeSz, byte* flavor, void* heap); @@ -535,8 +536,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. The + * trusted form is for root CAs, so it is declined here. */ WOLFSSH_API int wolfSSH_CTX_UseCert_buffer(WOLFSSH_CTX* ctx, const byte* cert, word32 certSz, int format); + /* Loads every PEM cert, plain or trusted (wolfSSL 5.8.0+), so a bundle + * installs all its CAs. A failing block is skipped; no CA fails the call. */ 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)