From 3957f626e639e343d06d9bd11ed71d481775b501 Mon Sep 17 00:00:00 2001 From: Yosuke Shimizu Date: Thu, 6 Aug 2026 16:43:01 +0900 Subject: [PATCH] Load every PEM block in a root CA buffer - wolfSSH_ProcessBuffer() hands a PEM BUFTYPE_CA buffer to a new LoadRootCaPemBuffer(), which walks every block, decodes each with wc_PemToDer(), and gives the DER to wolfSSH_CERTMAN_LoadRootCA_buffer(). - A block that will not decode, or that the cert manager refuses, is logged and skipped. The call returns WS_PARSE_E when no block loaded, and WS_BAD_FILE_E when the buffer holds no block at all. - wc_PemGetHeaderFooter() supplies the CERTIFICATE and TRUSTED CERTIFICATE headers, and a new FindInBuffer() seeks the next one without stopping at an embedded NUL. - LoadRootCaPemBuffer() takes its EncryptedInfo from the heap under WOLFSSH_SMALL_STACK. - internal.h defines WOLFSSH_HAVE_TRUSTED_CERT_PEM for wolfSSL 5.8.0 and later, along with the WOLFSSL_V5_8_0 constant it tests. - SniffCertForm() reports the trusted certificate form through a new out parameter. UseCertFile() takes that form for BUFTYPE_CA and answers WS_BAD_FILETYPE_E otherwise, as does wolfSSH_ReadCert_buffer(). - ssh.h records what each buffer entry point reads and when it fails. - tests/api.c adds test_wolfSSH_CTX_AddRootCert_bundle(), test_wolfSSH_CTX_AddRootCert_file_trusted(), and test_wolfSSH_ReadCert_buffer_trusted(). --- src/internal.c | 164 ++++++++++++++++++++ src/ssh.c | 36 ++++- tests/api.c | 367 +++++++++++++++++++++++++++++++++++++++++++++ wolfssh/internal.h | 7 + wolfssh/ssh.h | 9 +- 5 files changed, 577 insertions(+), 6 deletions(-) diff --git a/src/internal.c b/src/internal.c index ad91ee0ed..b489704d4 100644 --- a/src/internal.c +++ b/src/internal.c @@ -2708,6 +2708,163 @@ 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. A block that + will not decode or that the cert manager refuses is logged and skipped, the + way wolfSSL's own chain loader treats a CA file. */ +static int LoadRootCaPemBuffer(WOLFSSH_CTX* ctx, const byte* in, word32 inSz) +{ + EncryptedInfo* info; +#ifndef WOLFSSH_SMALL_STACK + EncryptedInfo info_s; +#endif + DerBuffer* der = NULL; + const char* certHeader = NULL; + const char* header; + const byte* found; + word32 used = 0; + word32 loaded = 0; + word32 failed = 0; + word32 headerSz; + 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; + } + + /* 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 + +#ifndef WOLFSSH_SMALL_STACK + info = &info_s; +#else + info = (EncryptedInfo*)WMALLOC(sizeof(EncryptedInfo), ctx->heap, + DYNTYPE_TEMP); + if (info == NULL) { + return WS_MEMORY_E; + } +#endif + + while (used < inSz) { + /* Text may sit between blocks, so seek the next header. */ + found = FindInBuffer(in + used, inSz - used, certHeader); + header = 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; + header = trustedHeader; + wcType = TRUSTED_CERT_TYPE; + } + else { + wcType = CA_TYPE; + } +#endif + + if (found == NULL) { + break; + } + used = (word32)(found - in); + headerSz = (word32)WSTRLEN(header); + + WMEMSET(info, 0, sizeof(EncryptedInfo)); + 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_ERROR, "Skipping CA %u, PEM to DER failed", + loaded + failed); + failed++; + /* The block length is unknown, so resume behind its header. */ + used += headerSz; + continue; + } + + 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++; + } + + /* A block that consumes nothing would stall the walk. */ + used += (info->consumed > 0) ? (word32)info->consumed : headerSz; + } + +#ifdef WOLFSSH_SMALL_STACK + WFREE(info, ctx->heap, DYNTYPE_TEMP); +#endif + + 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 +2941,13 @@ 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); + } + #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..252c09d0b 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -2350,14 +2350,19 @@ int wolfSSH_ReadPublicKey_buffer(const byte* in, word32 inSz, int format, #ifdef WOLFSSH_CERTS static const char* CertBeginPrefix = "-----BEGIN CERTIFICATE-----"; #endif +#if defined(WOLFSSH_CERTS) && defined(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. + Sets trusted for the auxiliary form, which only a root CA store takes. */ static int SniffCertForm(const byte* in, word32 inSz, byte* flavor, - byte* certId) + byte* certId, byte* trusted) { #ifdef WOLFSSH_OSSH_CERTS word32 tokenSz = 0; @@ -2365,6 +2370,8 @@ static int SniffCertForm(const byte* in, word32 inSz, byte* flavor, #endif int ret = WS_BAD_FILETYPE_E; + *trusted = 0; + #ifdef WOLFSSH_CERTS if (in[0] == 0x30) { *flavor = WOLFSSH_CERT_FLAVOR_X509; @@ -2375,6 +2382,13 @@ 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; + *trusted = 1; + ret = WOLFSSH_FORMAT_PEM; + } +#endif /* WOLFSSH_HAVE_TRUSTED_CERT_PEM */ #endif /* WOLFSSH_CERTS */ #ifdef WOLFSSH_OSSH_CERTS @@ -2530,6 +2544,7 @@ int wolfSSH_ReadCert_buffer(const byte* in, word32 inSz, byte* flavor, void* heap) { byte certId = ID_UNKNOWN; + byte trusted = 0; int format; int ret; @@ -2544,12 +2559,19 @@ int wolfSSH_ReadCert_buffer(const byte* in, word32 inSz, *outTypeSz = 0; *flavor = WOLFSSH_CERT_FLAVOR_UNKNOWN; - format = SniffCertForm(in, inSz, flavor, &certId); + format = SniffCertForm(in, inSz, flavor, &certId, &trusted); if (format < 0) { WLOG(WS_LOG_DEBUG, "Unable to identify the certificate"); return format; } + if (trusted) { + /* The trust data behind the certificate means nothing to a peer. */ + WLOG(WS_LOG_DEBUG, "Trusted certificate form is for root CAs"); + *flavor = WOLFSSH_CERT_FLAVOR_UNKNOWN; + return WS_BAD_FILETYPE_E; + } + ret = WS_BAD_FILETYPE_E; switch (format) { @@ -3215,6 +3237,7 @@ static int UseCertFile(WOLFSSH_CTX* ctx, const char* name, int type) word32 inSz; byte certId = ID_UNKNOWN; byte flavor = WOLFSSH_CERT_FLAVOR_UNKNOWN; + byte trusted = 0; int format; int ret; @@ -3224,7 +3247,7 @@ static int UseCertFile(WOLFSSH_CTX* ctx, const char* name, int type) ret = ReadFileIntoBuffer(name, &in, &inSz, ctx->heap); if (ret == WS_SUCCESS) { - format = SniffCertForm(in, inSz, &flavor, &certId); + format = SniffCertForm(in, inSz, &flavor, &certId, &trusted); if (format < 0) { ret = format; @@ -3234,6 +3257,11 @@ static int UseCertFile(WOLFSSH_CTX* ctx, const char* name, int type) WLOG(WS_LOG_DEBUG, "Certificate file is not PEM or DER"); ret = WS_BAD_FILETYPE_E; } + else if (trusted && type != BUFTYPE_CA) { + /* The trust data behind the certificate means nothing to a peer. */ + WLOG(WS_LOG_DEBUG, "Trusted certificate form is for root CAs"); + ret = WS_BAD_FILETYPE_E; + } else { ret = wolfSSH_ProcessBuffer(ctx, in, inSz, format, type); } diff --git a/tests/api.c b/tests/api.c index 6bf387804..e18dc97b0 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,290 @@ 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"; + /* 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; + word32 trustedSz = 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)); + + 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; + + /* 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); + + /* 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); +#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 +6862,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..830143700 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. */ diff --git a/wolfssh/ssh.h b/wolfssh/ssh.h index 2108013ee..01ec4714e 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. */ 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 is logged and skipped; the call fails + * only when the buffer leaves no CA installed. */ 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)