diff --git a/src/certman.c b/src/certman.c index 6a09c0884..e95c37bff 100644 --- a/src/certman.c +++ b/src/certman.c @@ -334,7 +334,7 @@ static int CertManIntermediateIsCA(WOLFSSH_CERTMAN* cm, if (decoded != NULL) { wc_InitDecodedCert(decoded, der, derSz, cm->heap); - if (wc_ParseCert(decoded, WOLFSSL_FILETYPE_ASN1, NO_VERIFY, NULL) == 0) { + if (wc_ParseCert(decoded, CERT_TYPE, NO_VERIFY, NULL) == 0) { isCA = decoded->isCA; #ifndef ALLOW_INVALID_CERTSIGN if (isCA && decoded->extKeyUsageSet && @@ -360,6 +360,126 @@ static int CertManIntermediateIsCA(WOLFSSH_CERTMAN* cm, return isCA; } +/* Verifies one cert against cm->cm, mapping the result to a WS_ code. */ +static int CertManVerifyOne(WOLFSSH_CERTMAN* cm, const unsigned char* der, + word32 derSz) +{ + int ret; + + ret = wolfSSL_CertManagerVerifyBuffer(cm->cm, der, derSz, + WOLFSSL_FILETYPE_ASN1); + if (ret == WOLFSSL_SUCCESS) { + ret = WS_SUCCESS; + } + else if (ret == ASN_NO_SIGNER_E) { + WLOG(WS_LOG_CERTMAN, "cert verify: no signer"); + ret = WS_CERT_NO_SIGNER_E; + } + else if (ret == ASN_AFTER_DATE_E) { + WLOG(WS_LOG_CERTMAN, "cert verify: expired"); + ret = WS_CERT_EXPIRED_E; + } + else if (ret == ASN_SIG_CONFIRM_E) { + WLOG(WS_LOG_CERTMAN, "cert verify: bad sig"); + ret = WS_CERT_SIG_CONFIRM_E; + } + else { + WLOG(WS_LOG_CERTMAN, "cert verify: other error (%d)", ret); + ret = WS_CERT_OTHER_E; + } + + return ret; +} + +/* Returns 1 when issuer is the signer wolfSSL looks up for child: its + * subject is child's issuer name and, when child carries an authority key + * ID, its subject key ID, which wolfSSL derives for a cert without one, + * matches it. */ +static int CertManIsIssuer(WOLFSSH_CERTMAN* cm, + const unsigned char* issuer, word32 issuerSz, + const unsigned char* child, word32 childSz) +{ + DecodedCert* decoded = NULL; +#ifndef WOLFSSH_SMALL_STACK + DecodedCert decoded_s[2]; +#endif + int match = 0; + +#ifndef WOLFSSH_SMALL_STACK + decoded = decoded_s; +#else + decoded = (DecodedCert*)WMALLOC(2 * sizeof(DecodedCert), cm->heap, + DYNTYPE_CERT); +#endif + + if (decoded != NULL) { + wc_InitDecodedCert(&decoded[0], issuer, issuerSz, cm->heap); + wc_InitDecodedCert(&decoded[1], child, childSz, cm->heap); + if (wc_ParseCert(&decoded[0], CERT_TYPE, NO_VERIFY, NULL) == 0 && + wc_ParseCert(&decoded[1], CERT_TYPE, NO_VERIFY, NULL) == 0) { + match = WMEMCMP(decoded[0].subjectHash, decoded[1].issuerHash, + sizeof(decoded[0].subjectHash)) == 0; + #ifndef NO_SKID + if (match && decoded[1].extAuthKeyIdSet) { + match = WMEMCMP(decoded[0].extSubjKeyId, + decoded[1].extAuthKeyId, + sizeof(decoded[0].extSubjKeyId)) == 0; + } + #endif + } + wc_FreeDecodedCert(&decoded[1]); + wc_FreeDecodedCert(&decoded[0]); + #ifdef WOLFSSH_SMALL_STACK + WFREE(decoded, cm->heap, DYNTYPE_CERT); + #endif + } + else { + WLOG(WS_LOG_CERTMAN, "could not allocate certs to match issuer"); + } + + return match; +} + +/* Loads the verified CA cert above child into cm->cm as child's signer and + * returns child's verification result. The typed load retags whichever + * signer has the cert's key ID, so the cert is first loaded untyped and + * tagged WOLFSSL_CHAIN_CA, as TLS adds chain CAs, only once child finding + * a signer shows that load added it. Older wolfSSL and NO_SKID builds keep + * it as a trusted root. */ +static int CertManPromote(WOLFSSH_CERTMAN* cm, + const unsigned char* der, word32 derSz, + const unsigned char* child, word32 childSz) +{ + int ret; + + if (!CertManIsIssuer(cm, der, derSz, child, childSz)) { + WLOG(WS_LOG_CERTMAN, "cert above is not the issuer; not promoting"); + return WS_CERT_NO_SIGNER_E; + } + + ret = wolfSSH_CERTMAN_LoadRootCA_buffer(cm, der, derSz); + if (ret != WS_SUCCESS) { + WLOG(WS_LOG_CERTMAN, "couldn't add intermediate cert (%d)", ret); + return WS_CERT_OTHER_E; + } + + ret = CertManVerifyOne(cm, child, childSz); +#ifdef WOLFSSH_CERTMAN_CHAIN_CA + if (ret != WS_CERT_NO_SIGNER_E && + wolfSSL_CertManagerLoadCABufferType(cm->cm, der, (long)derSz, + WOLFSSL_FILETYPE_ASN1, 0, WOLFSSL_LOAD_VERIFY_DEFAULT_FLAGS, + WOLFSSL_CHAIN_CA) != WOLFSSL_SUCCESS) { + WLOG(WS_LOG_ERROR, "couldn't mark intermediate cert as chain CA; " + "it is left as a trusted root"); + if (ret == WS_SUCCESS) { + ret = WS_CERT_OTHER_E; + } + } +#endif + + return ret; +} + /* Reads a DER tag and definite length at *idx. On success *idx is advanced * to the content, which is *len bytes and lies within sz. */ static int DerGetHeader(const byte* in, word32 sz, word32* idx, byte* tag, @@ -627,26 +747,16 @@ int wolfSSH_CERTMAN_VerifyCerts_buffer(WOLFSSH_CERTMAN* cm, if (ret == WS_SUCCESS) { for (certIdx = certsCount - 1; certIdx >= 0; certIdx--) { WLOG(WS_LOG_CERTMAN, "verifying cert at index %d", certIdx); - ret = wolfSSL_CertManagerVerifyBuffer(cm->cm, certLoc[certIdx], - certLen[certIdx], WOLFSSL_FILETYPE_ASN1); - if (ret == WOLFSSL_SUCCESS) { - ret = WS_SUCCESS; - } - else if (ret == ASN_NO_SIGNER_E) { - WLOG(WS_LOG_CERTMAN, "cert verify: no signer"); - ret = WS_CERT_NO_SIGNER_E; - } - else if (ret == ASN_AFTER_DATE_E) { - WLOG(WS_LOG_CERTMAN, "cert verify: expired"); - ret = WS_CERT_EXPIRED_E; - } - else if (ret == ASN_SIG_CONFIRM_E) { - WLOG(WS_LOG_CERTMAN, "cert verify: bad sig"); - ret = WS_CERT_SIG_CONFIRM_E; - } - else { - WLOG(WS_LOG_CERTMAN, "cert verify: other error (%d)", ret); - ret = WS_CERT_OTHER_E; + ret = CertManVerifyOne(cm, certLoc[certIdx], certLen[certIdx]); + + /* Load the cert above as a signer only when this one has none + * without it. */ + if (ret == WS_CERT_NO_SIGNER_E && + certIdx + 1 < (int)certsCount) { + WLOG(WS_LOG_CERTMAN, "adding intermediate cert as signer"); + ret = CertManPromote(cm, certLoc[certIdx + 1], + certLen[certIdx + 1], certLoc[certIdx], + certLen[certIdx]); } #ifdef HAVE_OCSP @@ -676,20 +786,14 @@ int wolfSSH_CERTMAN_VerifyCerts_buffer(WOLFSSH_CERTMAN* cm, } #endif /* HAVE_OCSP */ - /* promote the intermediate so the next cert has a signer, but - * only if it's actually a CA */ - if (ret == WS_SUCCESS && certIdx > 0) { - if (CertManIntermediateIsCA(cm, certLoc[certIdx], + /* every cert above the leaf must be a CA, before the next + * pass can load it as a signer */ + if (ret == WS_SUCCESS && certIdx > 0 && + !CertManIntermediateIsCA(cm, certLoc[certIdx], certLen[certIdx])) { - WLOG(WS_LOG_CERTMAN, "adding intermediate cert as trusted"); - ret = wolfSSH_CERTMAN_LoadRootCA_buffer(cm, - certLoc[certIdx], certLen[certIdx]); - } - else { - WLOG(WS_LOG_CERTMAN, - "peer intermediate is not a CA; not promoting"); - ret = WS_CERT_NO_SIGNER_E; - } + WLOG(WS_LOG_CERTMAN, + "peer intermediate is not a CA; not promoting"); + ret = WS_CERT_NO_SIGNER_E; } if (ret != WS_SUCCESS) { @@ -719,7 +823,7 @@ int wolfSSH_CERTMAN_VerifyCerts_buffer(WOLFSSH_CERTMAN* cm, /* NULL only when the small-stack allocation above failed */ if (decoded != NULL) { wc_InitDecodedCert(decoded, certLoc[0], certLen[0], cm->heap); - if (wc_ParseCert(decoded, WOLFSSL_FILETYPE_ASN1, NO_VERIFY, cm->cm) + if (wc_ParseCert(decoded, CERT_TYPE, NO_VERIFY, cm->cm) != 0) { WLOG(WS_LOG_CERTMAN, "unable to parse leaf certificate"); ret = WS_CERT_OTHER_E; @@ -850,6 +954,11 @@ static int CheckProfile(DecodedCert* cert, int profile) } /* encoding of issuer DN must be exact match to CA subject DN */ + if (valid && cert->ca == NULL) { + /* a self-issued leaf parsed NO_VERIFY has no signer looked up */ + WLOG(WS_LOG_CERTMAN, "CA for issuer DN match not found"); + valid = 0; + } if (valid) { int sz = min(SIGNER_DIGEST_SIZE, KEYID_SIZE); if (XMEMCMP(cert->ca->subjectNameHash, cert->issuerHash, sz) != 0) { diff --git a/tests/unit.c b/tests/unit.c index 82adcd5e6..81d4598f7 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -18791,12 +18791,21 @@ static int certmanForgeChild(const byte* issuerCert, word32 issuerCertSz, * the same for ExtendedKeyUsage, taking wc_SetExtKeyUsage() names * ("serverAuth", "clientAuth", "any", ...) or, with WOLFSSL_EKU_OID, a * dotted OID. The issuer name is taken from issuerCert, the subject public - * key from subjectKey, and the cert is signed with issuerKey. Fills - * der/derSz on success. */ -static int certmanForgeCert(const char* cn, int isCA, const char* keyUsage, - const char* extKeyUsage, + * key from subjectKey, and the cert is signed with issuerKey. keyIds of + * CERTMAN_NO_SKID or CERTMAN_NO_AKID leaves out that key ID, and a non-NULL + * akidKey takes the authority key ID from that key instead of from + * issuerCert: by RFC 5280 method 1, or with CERTMAN_AKID_SPKI as the SHA-1 + * of its whole SubjectPublicKeyInfo, the ID wolfSSL derives for a cert + * without a subject key ID. Fills der/derSz on success. */ +#define CERTMAN_NO_SKID 0x01 +#define CERTMAN_NO_AKID 0x02 +#define CERTMAN_AKID_SPKI 0x04 + +static int certmanForgeCertKeyIds(const char* cn, int isCA, + const char* keyUsage, const char* extKeyUsage, const byte* issuerCert, word32 issuerCertSz, - ecc_key* issuerKey, ecc_key* subjectKey, byte* der, word32* derSz) + ecc_key* issuerKey, ecc_key* subjectKey, int keyIds, + ecc_key* akidKey, byte* der, word32* derSz) { WC_RNG rng; Cert cert; @@ -18853,10 +18862,28 @@ static int certmanForgeCert(const char* cn, int isCA, const char* keyUsage, #ifdef WOLFSSL_CERT_EXT /* Key ids make the signer lookup go by key, not by the issuer name, * which a self-issued intermediate shares with the root. */ - if (ret == 0) + if (ret == 0 && !(keyIds & CERTMAN_NO_SKID)) ret = wc_SetSubjectKeyIdFromPublicKey_ex(&cert, ECC_TYPE, subjectKey); - if (ret == 0) - ret = wc_SetAuthKeyIdFromCert(&cert, issuerCert, (int)issuerCertSz); + if (ret == 0 && !(keyIds & CERTMAN_NO_AKID)) { + if (akidKey != NULL && (keyIds & CERTMAN_AKID_SPKI)) { + byte spki[256]; + + sz = wc_EccPublicKeyToDer(akidKey, spki, sizeof(spki), 1); + if (sz < 0) + ret = sz; + else + ret = wc_ShaHash(spki, (word32)sz, cert.akid); + cert.akidSz = WC_SHA_DIGEST_SIZE; + } + else if (akidKey != NULL) + ret = wc_SetAuthKeyIdFromPublicKey_ex(&cert, ECC_TYPE, akidKey); + else + ret = wc_SetAuthKeyIdFromCert(&cert, issuerCert, + (int)issuerCertSz); + } +#else + (void)keyIds; + (void)akidKey; #endif if (ret == 0) { sz = wc_MakeCert(&cert, der, *derSz, NULL, subjectKey, &rng); @@ -18878,6 +18905,16 @@ static int certmanForgeCert(const char* cn, int isCA, const char* keyUsage, return ret; } +static int certmanForgeCert(const char* cn, int isCA, const char* keyUsage, + const char* extKeyUsage, + const byte* issuerCert, word32 issuerCertSz, + ecc_key* issuerKey, ecc_key* subjectKey, byte* der, word32* derSz) +{ + return certmanForgeCertKeyIds(cn, isCA, keyUsage, extKeyUsage, + issuerCert, issuerCertSz, issuerKey, subjectKey, 0, NULL, + der, derSz); +} + static void certmanPutU32(byte* p, word32 v) { p[0] = (byte)(v >> 24); @@ -19051,7 +19088,7 @@ static int certmanIsSelfIssued(const byte* der, word32 derSz) * own key and the root's signature, the shape of a CA key rollover cert. * * appendRoot also sends the trusted root at the end of the chain, so it goes - * through CertManIntermediateIsCA too and must be promoted as well. + * through CertManIntermediateIsCA too and must stay trusted afterwards. * * expectPromote==1 asserts the intermediate was promoted and the leaf * verified through it: verify returns WS_SUCCESS, or WS_CERT_PROFILE_E when @@ -19171,6 +19208,19 @@ static int certmanCheckIntermediate(const char* interKeyUsage, int selfIssued, result = -934; goto done; } + if (expectPromote) { + /* The intermediate stays in the manager, so the leaf alone now + * finds its signer. */ + chainSz = certmanAppendCert(chain, (word32)sizeof(chain), 0, + leaf, leafSz); + ret = wolfSSH_CERTMAN_VerifyCerts_buffer(cm, chain, chainSz, 1); + if (ret != WS_SUCCESS && ret != WS_CERT_PROFILE_E) { + printf("CertMan: promoted intermediate CA not kept, ret=%d\n", + ret); + result = -936; goto done; + } + } + done: if (cm != NULL) wolfSSH_CERTMAN_free(cm); @@ -19193,8 +19243,8 @@ static int test_CertMan_PromoteValidCaIntermediate(void) /* A CA intermediate with no KeyUsage extension must still be promoted. */ result = certmanCheckIntermediate(NULL, 0, 0, 1); - /* A peer may send the trusted root too; it asserts keyCertSign, so it is - * promoted like any other CA. */ + /* A peer may send the trusted root too; it asserts keyCertSign, so it + * passes the CA check like any other cert above the leaf. */ if (result == 0) result = certmanCheckIntermediate(NULL, 0, 1, 1); #ifdef WOLFSSL_CERT_EXT @@ -19218,6 +19268,408 @@ static int test_CertMan_PromoteValidCaIntermediate(void) #ifdef WOLFSSL_CERT_EXT +/* An intermediate CA with no subject key ID extension is found as the + * signer of a leaf whose authority key ID is the key ID wolfSSL derives for + * it, the SHA-1 of its SubjectPublicKeyInfo. wolfSSL finds no signer for a + * leaf whose authority key ID is anything else, here the RFC 5280 method 1 + * ID of the intermediate's key or the ID of another key, and does not fall + * back to the issuer name; the intermediate must then not be left in the + * manager, so a leaf without an authority key ID, found by issuer name + * alone, still has no signer afterwards. */ +static int test_CertMan_IssuerWithoutSkid(void) +{ + int result = 0; + int ret; + byte* root = NULL; + byte* rootKeyBuf = NULL; + word32 rootSz = 0, rootKeySz = 0; + byte inter[2048]; + word32 interSz = sizeof(inter); + byte leaf[2048]; + word32 leafSz; + byte chain[4096]; + word32 chainSz; + word32 idx; + ecc_key rootKey, interKey, leafKey; + int haveRootKey = 0, haveInterKey = 0, haveLeafKey = 0; + WC_RNG rng; + int haveRng = 0; + WOLFSSH_CERTMAN* cm = NULL; + int i; + + if (certmanLoadFile("./keys/ca-cert-ecc.der", &root, &rootSz) != 0 || + certmanLoadFile("./keys/ca-key-ecc.der", + &rootKeyBuf, &rootKeySz) != 0) { + printf("CertMan: can't load test certs\n"); + result = -980; goto done; + } + if (wc_InitRng(&rng) != 0) { + result = -981; goto done; + } + haveRng = 1; + if (wc_ecc_init(&rootKey) != 0) { + result = -982; goto done; + } + haveRootKey = 1; + idx = 0; + if (wc_EccPrivateKeyDecode(rootKeyBuf, &idx, &rootKey, rootKeySz) != 0) { + result = -983; goto done; + } + if (wc_ecc_init(&interKey) != 0) { + result = -984; goto done; + } + haveInterKey = 1; + if (wc_ecc_init(&leafKey) != 0) { + result = -985; goto done; + } + haveLeafKey = 1; + if (wc_ecc_make_key(&rng, 32, &interKey) != 0 || + wc_ecc_make_key(&rng, 32, &leafKey) != 0) { + result = -986; goto done; + } + if (certmanForgeCertKeyIds("NoSkidCA", 1, NULL, NULL, root, rootSz, + &rootKey, &interKey, CERTMAN_NO_SKID, NULL, + inter, &interSz) != 0) { + result = -987; goto done; + } + + /* 0: wolfSSL's ID for the intermediate, 1: the method 1 ID of its key, + * 2: an ID of another key */ + for (i = 0; i < 3; i++) { + cm = wolfSSH_CERTMAN_new(NULL); + if (cm == NULL || wolfSSH_CERTMAN_LoadRootCA_buffer(cm, root, rootSz) + != WS_SUCCESS) { + result = -988; goto done; + } + leafSz = sizeof(leaf); + if (certmanForgeCertKeyIds("NoSkidLeaf", 0, NULL, NULL, inter, + interSz, &interKey, &leafKey, + i == 0 ? CERTMAN_AKID_SPKI : 0, + i == 2 ? &leafKey : &interKey, leaf, &leafSz) != 0) { + result = -989; goto done; + } + chainSz = certmanAppendCert(chain, (word32)sizeof(chain), 0, + leaf, leafSz); + chainSz = certmanAppendCert(chain, (word32)sizeof(chain), chainSz, + inter, interSz); + ret = wolfSSH_CERTMAN_VerifyCerts_buffer(cm, chain, chainSz, 2); + if (i == 0 && ret != WS_SUCCESS && ret != WS_CERT_PROFILE_E) { + printf("CertMan: issuer without SKID not found, ret=%d\n", ret); + result = -990; goto done; + } + if (i > 0) { + if (ret != WS_CERT_NO_SIGNER_E) { + printf("CertMan: AKID case %d got %d\n", i, ret); + result = -991; goto done; + } + leafSz = sizeof(leaf); + if (certmanForgeCertKeyIds("NameLeaf", 0, NULL, NULL, inter, + interSz, &interKey, &leafKey, CERTMAN_NO_AKID, NULL, + leaf, &leafSz) != 0) { + result = -992; goto done; + } + chainSz = certmanAppendCert(chain, (word32)sizeof(chain), 0, + leaf, leafSz); + ret = wolfSSH_CERTMAN_VerifyCerts_buffer(cm, chain, chainSz, 1); + if (ret != WS_CERT_NO_SIGNER_E) { + printf("CertMan: intermediate left loaded, ret=%d\n", ret); + result = -993; goto done; + } + } + wolfSSH_CERTMAN_free(cm); + cm = NULL; + } + +done: + if (cm != NULL) + wolfSSH_CERTMAN_free(cm); + if (haveRootKey) + wc_ecc_free(&rootKey); + if (haveInterKey) + wc_ecc_free(&interKey); + if (haveLeafKey) + wc_ecc_free(&leafKey); + if (haveRng) + wc_FreeRng(&rng); + free(root); + free(rootKeyBuf); + return result; +} + +#endif /* WOLFSSL_CERT_EXT */ + +#ifdef WOLFSSH_TEST_SET_CERTMAN + +/* Verifies chain and expects expected, then, after the application clears + * the chain CAs, expects goodLeaf, signed by the root, to still verify. */ +static int certmanCheckRootKept(WOLFSSH_CERTMAN* cm, WOLFSSL_CERT_MANAGER* wcm, + const byte* chain, word32 chainSz, word32 count, int expected, + const byte* goodLeaf, word32 goodLeafSz) +{ + byte one[2048 + UINT32_SZ]; + word32 oneSz; + int ret; + + ret = wolfSSH_CERTMAN_VerifyCerts_buffer(cm, chain, chainSz, count); + if (ret != expected) { + printf("CertMan: chain got %d, expected %d\n", ret, expected); + return -940; + } +#ifdef WOLFSSH_CERTMAN_CHAIN_CA + if (wolfSSL_CertManagerUnloadIntermediateCerts(wcm) != WOLFSSL_SUCCESS) { + printf("CertMan: unloading intermediate certs failed\n"); + return -941; + } +#else + (void)wcm; +#endif + oneSz = certmanAppendCert(one, (word32)sizeof(one), 0, + goodLeaf, goodLeafSz); + ret = wolfSSH_CERTMAN_VerifyCerts_buffer(cm, one, oneSz, 1); + if (ret != WS_SUCCESS && ret != WS_CERT_PROFILE_E) { + printf("CertMan: trusted root lost, ret=%d\n", ret); + return -942; + } + + return 0; +} + +/* A peer chain that puts an already-trusted CA above a cert it does not + * sign must leave that CA a trusted root: wolfSSL's typed CA load retags + * an existing signer with the same key ID, which a later + * wolfSSL_CertManagerUnloadIntermediateCerts() would then remove. + * 1. [leaf not issued by the root, root] + * 2. [leaf, cert named as its issuer that shares the root's key ID] + * 3. a valid [leaf, intermediate] chain: the intermediate is a chain CA, + * cleared by the unload, while the root stays. Before that, it is not + * loaded above a leaf it did not issue, or above one the root signed. */ +static int test_CertMan_TrustedRootNotRetagged(void) +{ + int result = 0; + int ret; + byte* root = NULL; + byte* rootKeyBuf = NULL; + byte* fred = NULL; + word32 rootSz = 0, rootKeySz = 0, fredSz = 0; + byte goodLeaf[2048]; + word32 goodLeafSz = sizeof(goodLeaf); + byte leaf[2048]; + word32 leafSz; + byte ca[2048]; + word32 caSz; + byte chain[6144]; + word32 chainSz; + word32 idx; + ecc_key rootKey, leafKey; + int haveRootKey = 0, haveLeafKey = 0; + WC_RNG rng; + int haveRng = 0; + WOLFSSH_CTX* ctx = NULL; + WOLFSSL_CERT_MANAGER* wcm = NULL; + WOLFSSH_CERTMAN* cm = NULL; + + if (certmanLoadFile("./keys/ca-cert-ecc.der", &root, &rootSz) != 0 || + certmanLoadFile("./keys/ca-key-ecc.der", + &rootKeyBuf, &rootKeySz) != 0 || + certmanLoadFile("./keys/fred-cert.der", &fred, &fredSz) != 0) { + printf("CertMan: can't load test certs\n"); + result = -943; goto done; + } + if (wc_InitRng(&rng) != 0) { + result = -944; goto done; + } + haveRng = 1; + if (wc_ecc_init(&rootKey) != 0) { + result = -945; goto done; + } + haveRootKey = 1; + idx = 0; + if (wc_EccPrivateKeyDecode(rootKeyBuf, &idx, &rootKey, rootKeySz) != 0) { + result = -946; goto done; + } + if (wc_ecc_init(&leafKey) != 0) { + result = -947; goto done; + } + haveLeafKey = 1; + if (wc_ecc_make_key(&rng, 32, &leafKey) != 0) { + result = -948; goto done; + } + if (certmanForgeCert("GoodLeaf", 0, NULL, NULL, root, rootSz, &rootKey, + &leafKey, goodLeaf, &goodLeafSz) != 0) { + result = -949; goto done; + } + + /* Import the wolfSSL manager so the test can unload from it. */ + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + wcm = wolfSSL_CertManagerNew(); + if (ctx == NULL || wcm == NULL || + wolfSSH_SetCertManager(ctx, wcm) != WS_SUCCESS) { + result = -950; goto done; + } + cm = ctx->certMan; + if (wolfSSH_CERTMAN_LoadRootCA_buffer(cm, root, rootSz) != WS_SUCCESS) { + result = -951; goto done; + } + + /* 1. A leaf issued under Fred's name, with the trusted root above. */ + leafSz = sizeof(leaf); + if (certmanForgeCert("BogusLeaf", 0, NULL, NULL, fred, fredSz, &leafKey, + &leafKey, leaf, &leafSz) != 0) { + result = -952; goto done; + } + chainSz = certmanAppendCert(chain, (word32)sizeof(chain), 0, + leaf, leafSz); + chainSz = certmanAppendCert(chain, (word32)sizeof(chain), chainSz, + root, rootSz); + result = certmanCheckRootKept(cm, wcm, chain, chainSz, 2, + WS_CERT_NO_SIGNER_E, goodLeaf, goodLeafSz); + if (result != 0) + goto done; + +#ifdef WOLFSSL_CERT_EXT + /* 2. A CA under a new name with the root's key, so the same key ID, + * signed by the root, above a leaf issued under that name. The root + * is the signer the load finds by that key ID. */ + caSz = sizeof(ca); + if (certmanForgeCert("RenamedRoot", 1, NULL, NULL, root, rootSz, + &rootKey, &rootKey, ca, &caSz) != 0) { + result = -953; goto done; + } + leafSz = sizeof(leaf); + if (certmanForgeCert("RenamedLeaf", 0, NULL, NULL, ca, caSz, &rootKey, + &leafKey, leaf, &leafSz) != 0) { + result = -954; goto done; + } + { + DecodedCert a, b; + int same = 0; + + wc_InitDecodedCert(&a, root, rootSz, NULL); + wc_InitDecodedCert(&b, ca, caSz, NULL); + if (wc_ParseCert(&a, CERT_TYPE, NO_VERIFY, NULL) == 0 && + wc_ParseCert(&b, CERT_TYPE, NO_VERIFY, NULL) == 0) { + same = WMEMCMP(a.extSubjKeyId, b.extSubjKeyId, + sizeof(a.extSubjKeyId)) == 0; + } + wc_FreeDecodedCert(&b); + wc_FreeDecodedCert(&a); + if (!same) { + printf("CertMan: renamed root has its own key ID\n"); + result = -955; goto done; + } + } + chainSz = certmanAppendCert(chain, (word32)sizeof(chain), 0, + leaf, leafSz); + chainSz = certmanAppendCert(chain, (word32)sizeof(chain), chainSz, + ca, caSz); + result = certmanCheckRootKept(cm, wcm, chain, chainSz, 2, + WS_CERT_NO_SIGNER_E, goodLeaf, goodLeafSz); + if (result != 0) + goto done; +#endif /* WOLFSSL_CERT_EXT */ + + /* 3. A valid intermediate, cleared by the unload. */ + caSz = sizeof(ca); + if (certmanForgeCert("IntermediateCA", 1, NULL, NULL, root, rootSz, + &rootKey, &leafKey, ca, &caSz) != 0) { + result = -956; goto done; + } + leafSz = sizeof(leaf); + if (certmanForgeCert("InterLeaf", 0, NULL, NULL, ca, caSz, &leafKey, + &leafKey, leaf, &leafSz) != 0) { + result = -957; goto done; + } + /* Above a leaf it did not issue, the intermediate is not loaded. */ + { + byte bogus[2048]; + word32 bogusSz = sizeof(bogus); + + if (certmanForgeCert("BogusLeaf", 0, NULL, NULL, fred, fredSz, + &leafKey, &leafKey, bogus, &bogusSz) != 0) { + result = -960; goto done; + } + chainSz = certmanAppendCert(chain, (word32)sizeof(chain), 0, + bogus, bogusSz); + chainSz = certmanAppendCert(chain, (word32)sizeof(chain), chainSz, + ca, caSz); + ret = wolfSSH_CERTMAN_VerifyCerts_buffer(cm, chain, chainSz, 2); + if (ret != WS_CERT_NO_SIGNER_E) { + printf("CertMan: bogus intermediate chain got %d\n", ret); + result = -961; goto done; + } + chainSz = certmanAppendCert(chain, (word32)sizeof(chain), 0, + leaf, leafSz); + ret = wolfSSH_CERTMAN_VerifyCerts_buffer(cm, chain, chainSz, 1); + if (ret != WS_CERT_NO_SIGNER_E) { + printf("CertMan: intermediate loaded for a cert it did not " + "issue, ret=%d\n", ret); + result = -962; goto done; + } + } + /* Nor above a leaf that has a signer without it. */ + chainSz = certmanAppendCert(chain, (word32)sizeof(chain), 0, + goodLeaf, goodLeafSz); + chainSz = certmanAppendCert(chain, (word32)sizeof(chain), chainSz, + ca, caSz); + ret = wolfSSH_CERTMAN_VerifyCerts_buffer(cm, chain, chainSz, 2); + if (ret != WS_SUCCESS && ret != WS_CERT_PROFILE_E) { + printf("CertMan: root-signed leaf under an intermediate got %d\n", + ret); + result = -963; goto done; + } + chainSz = certmanAppendCert(chain, (word32)sizeof(chain), 0, + leaf, leafSz); + ret = wolfSSH_CERTMAN_VerifyCerts_buffer(cm, chain, chainSz, 1); + if (ret != WS_CERT_NO_SIGNER_E) { + printf("CertMan: intermediate loaded for a leaf with a signer, " + "ret=%d\n", ret); + result = -964; goto done; + } + + chainSz = certmanAppendCert(chain, (word32)sizeof(chain), 0, + leaf, leafSz); + chainSz = certmanAppendCert(chain, (word32)sizeof(chain), chainSz, + ca, caSz); + ret = wolfSSH_CERTMAN_VerifyCerts_buffer(cm, chain, chainSz, 2); + if (ret != WS_SUCCESS && ret != WS_CERT_PROFILE_E) { + printf("CertMan: intermediate chain failed, ret=%d\n", ret); + result = -958; goto done; + } + result = certmanCheckRootKept(cm, wcm, chain, chainSz, 2, ret, + goodLeaf, goodLeafSz); + if (result != 0) + goto done; +#ifdef WOLFSSH_CERTMAN_CHAIN_CA + chainSz = certmanAppendCert(chain, (word32)sizeof(chain), 0, + leaf, leafSz); + ret = wolfSSH_CERTMAN_VerifyCerts_buffer(cm, chain, chainSz, 1); + if (ret != WS_CERT_NO_SIGNER_E) { + printf("CertMan: chain CA survived the unload, ret=%d\n", ret); + result = -959; goto done; + } +#endif + +done: + if (ctx != NULL) + wolfSSH_CTX_free(ctx); + if (wcm != NULL) + wolfSSL_CertManagerFree(wcm); + if (haveRootKey) + wc_ecc_free(&rootKey); + if (haveLeafKey) + wc_ecc_free(&leafKey); + if (haveRng) + wc_FreeRng(&rng); + free(root); + free(rootKeyBuf); + free(fred); + return result; +} + +#endif /* WOLFSSH_TEST_SET_CERTMAN */ + +#ifdef WOLFSSL_CERT_EXT + /* The FPKI profile check runs after the RFC 6187 usage check and never * matches a forged leaf, so in an FPKI build a leaf that passes the usage * check surfaces as WS_CERT_PROFILE_E rather than WS_SUCCESS. Either way it @@ -19405,6 +19857,156 @@ static int test_CertMan_LeafKeyUsage(void) return result; } +#if !defined(WOLFSSH_NO_FPKI) && defined(WOLFSSL_SUBJ_DIR_ATTR) + +/* A leaf that names its issuer as its own subject is self-issued, so the + * NO_VERIFY parse before CheckProfile() looks up no signer for it. Give it + * what the FPKI worksheet 6 profile checks ahead of the issuer-DN match and + * expect WS_CERT_PROFILE_E, not a NULL dereference. wolfSSL cannot write a + * subjectDirectoryAttributes extension, so the leaf carries the + * countryOfCitizenship value as a CRL distribution points extension whose + * OID is rewritten to 2.5.29.9 before signing. */ +static int test_CertMan_ProfileLeafNoSigner(void) +{ + static const byte sda[] = { + 0x30, 0x12, 0x30, 0x10, + 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x09, 0x04, + 0x31, 0x04, 0x13, 0x02, 'U', 'S' + }; + static const byte crlDpOid[] = { 0x06, 0x03, 0x55, 0x1d, 0x1f }; + int result = 0; + int ret; + int sz; + byte* root = NULL; + byte* rootKeyBuf = NULL; + word32 rootSz = 0, rootKeySz = 0; + byte leaf[2048]; + word32 leafSz = 0; + byte chain[4096]; + word32 chainSz; + word32 idx; + ecc_key rootKey, leafKey; + int haveRootKey = 0, haveLeafKey = 0; + WC_RNG rng; + int haveRng = 0; + Cert cert; + DecodedCert decoded; + WOLFSSH_CERTMAN* cm = NULL; + + if (certmanLoadFile("./keys/ca-cert-ecc.der", &root, &rootSz) != 0 || + certmanLoadFile("./keys/ca-key-ecc.der", + &rootKeyBuf, &rootKeySz) != 0) { + printf("CertMan: can't load test certs\n"); + result = -970; goto done; + } + if (wc_InitRng(&rng) != 0) { + result = -971; goto done; + } + haveRng = 1; + if (wc_ecc_init(&rootKey) != 0) { + result = -972; goto done; + } + haveRootKey = 1; + idx = 0; + if (wc_EccPrivateKeyDecode(rootKeyBuf, &idx, &rootKey, rootKeySz) != 0) { + result = -973; goto done; + } + if (wc_ecc_init(&leafKey) != 0) { + result = -974; goto done; + } + haveLeafKey = 1; + if (wc_ecc_make_key(&rng, 32, &leafKey) != 0) { + result = -975; goto done; + } + + ret = wc_InitCert(&cert); + if (ret == 0) + ret = wc_SetSubjectBuffer(&cert, root, (int)rootSz); + if (ret == 0) + ret = wc_SetIssuerBuffer(&cert, root, (int)rootSz); + if (ret == 0) + ret = wc_SetKeyUsage(&cert, "digitalSignature"); + if (ret == 0) + ret = wc_SetSubjectKeyIdFromPublicKey_ex(&cert, ECC_TYPE, &leafKey); + if (ret == 0) + ret = wc_SetAuthKeyIdFromCert(&cert, root, (int)rootSz); + if (ret == 0) { + cert.sigType = CTC_SHA256wECDSA; + cert.daysValid = 365; + cert.isCA = 0; + WSTRNCPY(cert.certPolicies[0], "2.16.840.1.101.3.2.1.3.13", + CTC_MAX_CERTPOL_SZ - 1); + cert.certPoliciesNb = 1; + WMEMCPY(cert.crlInfo, sda, sizeof(sda)); + cert.crlInfoSz = (int)sizeof(sda); + sz = wc_MakeCert(&cert, leaf, sizeof(leaf), NULL, &leafKey, &rng); + if (sz < 0) + ret = sz; + } + if (ret == 0) { + ret = -1; + for (idx = 0; idx + sizeof(crlDpOid) <= (word32)cert.bodySz; idx++) { + if (WMEMCMP(leaf + idx, crlDpOid, sizeof(crlDpOid)) == 0) { + leaf[idx + sizeof(crlDpOid) - 1] = 0x09; + ret = 0; + break; + } + } + } + if (ret == 0) { + sz = wc_SignCert(cert.bodySz, cert.sigType, leaf, sizeof(leaf), NULL, + &rootKey, &rng); + if (sz < 0) + ret = sz; + else + leafSz = (word32)sz; + } + if (ret != 0) { + printf("CertMan: forge profile leaf failed, ret=%d\n", ret); + result = -976; goto done; + } + + /* The leaf must get as far as the issuer-DN match. */ + wc_InitDecodedCert(&decoded, leaf, leafSz, NULL); + ret = wc_ParseCert(&decoded, CERT_TYPE, NO_VERIFY, NULL); + if (ret == 0 && WSTRCMP(decoded.countryOfCitizenship, "US") != 0) { + ret = -1; + } + wc_FreeDecodedCert(&decoded); + if (ret != 0 || !certmanIsSelfIssued(leaf, leafSz)) { + printf("CertMan: forged profile leaf is not as intended\n"); + result = -977; goto done; + } + + cm = wolfSSH_CERTMAN_new(NULL); + if (cm == NULL || + wolfSSH_CERTMAN_LoadRootCA_buffer(cm, root, rootSz) != WS_SUCCESS) { + result = -978; goto done; + } + chainSz = certmanAppendCert(chain, (word32)sizeof(chain), 0, + leaf, leafSz); + ret = wolfSSH_CERTMAN_VerifyCerts_buffer(cm, chain, chainSz, 1); + if (ret != WS_CERT_PROFILE_E) { + printf("CertMan: self-issued profile leaf got %d\n", ret); + result = -979; goto done; + } + +done: + if (cm != NULL) + wolfSSH_CERTMAN_free(cm); + if (haveRootKey) + wc_ecc_free(&rootKey); + if (haveLeafKey) + wc_ecc_free(&leafKey); + if (haveRng) + wc_FreeRng(&rng); + free(root); + free(rootKeyBuf); + return result; +} + +#endif /* !WOLFSSH_NO_FPKI && WOLFSSL_SUBJ_DIR_ATTR */ + #endif /* WOLFSSL_CERT_EXT */ #endif /* WOLFSSH_TEST_CERTMAN_PROMOTE */ @@ -26423,11 +27025,32 @@ int wolfSSH_UnitTest(int argc, char** argv) (unitResult == 0 ? "SUCCESS" : "FAILED")); testResult = testResult || unitResult; +#ifdef WOLFSSL_CERT_EXT + unitResult = test_CertMan_IssuerWithoutSkid(); + printf("CertMan_IssuerWithoutSkid: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; +#endif + +#ifdef WOLFSSH_TEST_SET_CERTMAN + unitResult = test_CertMan_TrustedRootNotRetagged(); + printf("CertMan_TrustedRootNotRetagged: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; +#endif + #ifdef WOLFSSL_CERT_EXT unitResult = test_CertMan_LeafKeyUsage(); printf("CertMan_LeafKeyUsage: %s\n", (unitResult == 0 ? "SUCCESS" : "FAILED")); testResult = testResult || unitResult; + +#if !defined(WOLFSSH_NO_FPKI) && defined(WOLFSSL_SUBJ_DIR_ATTR) + unitResult = test_CertMan_ProfileLeafNoSigner(); + printf("CertMan_ProfileLeafNoSigner: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; +#endif #endif #endif diff --git a/wolfssh/certman.h b/wolfssh/certman.h index 53d036ee6..3b840bb2b 100644 --- a/wolfssh/certman.h +++ b/wolfssh/certman.h @@ -57,11 +57,14 @@ struct WOLFSSL_CERT_MANAGER; * note wolfSSH mutates the shared object: in an HAVE_OCSP build this * enables WOLFSSL_OCSP_CHECKALL on cm, so a caller that keeps using the * same manager for TLS will find every chain requiring an OCSP response, - * and during certificate authentication wolfSSH permanently adds verified - * peer intermediate CAs to the manager as trusted roots. Prefer a manager - * dedicated to wolfSSH over one shared with a live TLS stack. On failure - * (WS_FATAL_ERROR) nothing has changed: the CTX keeps its previous manager - * and no policy has been applied to cm. Returns + * and certificate authentication adds a peer's verified intermediate CAs + * to the manager, where they stay. As in TLS they are WOLFSSL_CHAIN_CA + * signers, which wolfSSL_CertManagerUnloadIntermediateCerts() removes; with + * wolfSSL older than 5.8.4 or built with NO_SKID they are trusted roots, as + * is one whose WOLFSSL_CHAIN_CA retag failed, which is logged as an error. + * Prefer a manager dedicated to wolfSSH over one shared with a live TLS + * stack. On failure (WS_FATAL_ERROR) nothing has changed: the CTX keeps its + * previous manager and no policy has been applied to cm. Returns * WS_NOT_COMPILED for any arguments when built against wolfSSL older than * 4.6.0 (wolfSSL_CertManager_up_ref() is unavailable there). */ WOLFSSH_API @@ -78,9 +81,14 @@ WOLFSSH_API int wolfSSH_CERTMAN_LoadRootCA_buffer(WOLFSSH_CERTMAN* cm, const unsigned char* rootCa, word32 rootCaSz); -/* Verifies a leaf-first chain against the loaded roots and then applies the - * RFC 6187 section 2.2 leaf checks in every build: a KeyUsage extension - * must assert digitalSignature, and an ExtendedKeyUsage extension must name +/* Verifies a leaf-first chain against the loaded roots, adding each peer + * intermediate a cert needs as its signer to the manager as described at + * wolfSSH_SetCertManager(). The chain must be strictly ordered: each cert's + * only candidate signer from the chain is the cert after it, so a cert whose + * issuer is not loaded and is not the next entry fails with + * WS_CERT_NO_SIGNER_E. The call then applies the RFC 6187 section 2.2 leaf + * checks in every build: a KeyUsage extension must assert + * digitalSignature, and an ExtendedKeyUsage extension must name * anyExtendedKeyUsage or a purpose usable for the SSH role being verified, * id-kp-secureShellClient or TLS clientAuth for a user certificate and * id-kp-secureShellServer or TLS serverAuth for a host certificate. Fails diff --git a/wolfssh/internal.h b/wolfssh/internal.h index 866b96d2e..841f651ac 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -124,6 +124,7 @@ extern "C" { #define WOLFSSL_V5_7_0 0x05007000 #define WOLFSSL_V5_7_2 0x05007002 #define WOLFSSL_V5_8_0 0x05008000 +#define WOLFSSL_V5_8_4 0x05008004 #define WOLFSSL_V5_9_2 0x05009002 /* wolfSSL 5.8.0 added the trusted-certificate PEM header, both the @@ -132,6 +133,14 @@ extern "C" { #define WOLFSSH_HAVE_TRUSTED_CERT_PEM #endif +/* wolfSSL 5.8.4 added typed CA loads, so a peer chain's intermediates can + * be tagged WOLFSSL_CHAIN_CA as TLS does. The load finds the signer by + * subject key ID, so NO_SKID builds keep them as trusted roots. */ +#if defined(WOLFSSH_CERTS) && !defined(NO_SKID) && \ + (LIBWOLFSSL_VERSION_HEX >= WOLFSSL_V5_8_4) + #define WOLFSSH_CERTMAN_CHAIN_CA +#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. */