diff --git a/CMakeLists.txt b/CMakeLists.txt index 67eea96..30c8e8e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -165,18 +165,20 @@ check_c_source_compiles(" extern char wc_SetDNSEntry(void); extern char wc_SetAltNamesFromList(void); extern char SetLength(void); + extern char GetASNTag(void); + extern char GetLength(void); int main(void) { return FreeAltNames() + wc_SetDNSEntry() + wc_SetAltNamesFromList() + - SetLength(); + SetLength() + GetASNTag() + GetLength(); } " _have_asn_helpers) unset(CMAKE_REQUIRED_LIBRARIES) if(NOT _have_asn_helpers) message(FATAL_ERROR "wolfSSL does not export its ASN helpers (wc_SetDNSEntry, " - "wc_SetAltNamesFromList, FreeAltNames, SetLength). Rebuild wolfSSL " - "with CPPFLAGS=\"-DWOLFSSL_PUBLIC_ASN\".") + "wc_SetAltNamesFromList, FreeAltNames, SetLength, GetASNTag, " + "GetLength). Rebuild wolfSSL with CPPFLAGS=\"-DWOLFSSL_PUBLIC_ASN\".") endif() # Features that default-on in wolfSSL and that wolfCert depends on diff --git a/cli/wolfcert_client.c b/cli/wolfcert_client.c index 21a3740..741b252 100644 --- a/cli/wolfcert_client.c +++ b/cli/wolfcert_client.c @@ -152,7 +152,10 @@ static void print_usage(FILE* out) "reenroll options:\n" " --cert FILE Current certificate (PEM)\n" " --key FILE Current private key (PEM)\n" - " plus the enroll options above to describe the renewed cert.\n" + " --out-cert FILE Write renewed certificate (PEM;\n" + " default stdout)\n" + " The renewed cert keeps --cert's subject and SAN (RFC 7030\n" + " section 4.2.2), so --subject and --san-* are rejected.\n" " --cert/--key also authenticate the TLS connection, so\n" " --client-cert/--client-key are rejected.\n" "\n" @@ -1455,7 +1458,8 @@ static int cmd_reenroll(int argc, char** argv) uint8_t* cert_pem = NULL; uint8_t* key_pem = NULL; WolfCertKey* current_key = NULL; - WolfCertBuffer csr = { 0 }; + WolfCertKey* new_key = NULL; + WolfCertCertMeta meta = { 0 }; WolfCertBuffer issued = { 0 }; WolfCertProtocol p = 0; size_t cert_len = 0, key_len = 0; @@ -1485,8 +1489,16 @@ static int cmd_reenroll(int argc, char** argv) } if (ret == 0 && - (opts.subject == NULL || opts.cert_file == NULL || opts.key_file == NULL)) { - fprintf(stderr, "reenroll: --subject, --cert, --key required\n"); + (opts.subject != NULL || opts.san_dns_len != 0 || + opts.san_ip_len != 0 || opts.san_uri_len != 0 || + opts.san_email_len != 0)) { + fprintf(stderr, "reenroll: --subject/--san-* are not used; the renewed " + "cert keeps --cert's subject and SAN\n"); + ret = 1; + } + + if (ret == 0 && (opts.cert_file == NULL || opts.key_file == NULL)) { + fprintf(stderr, "reenroll: --cert, --key required\n"); ret = 1; } @@ -1505,20 +1517,6 @@ static int cmd_reenroll(int argc, char** argv) ret = 2; } - if (ret == 0) { - WolfCertCertMeta meta = { .subject_dn = opts.subject, - .san_dns = opts.san_dns, .san_dns_len = opts.san_dns_len, - .san_ip = opts.san_ip, .san_ip_len = opts.san_ip_len, - .san_uri = opts.san_uri, .san_uri_len = opts.san_uri_len, - .san_email = opts.san_email, .san_email_len = opts.san_email_len, - .challenge_password = opts.challenge }; - rc = wolfcert_csr_build(current_key, &meta, &csr); - if (rc != WOLFCERT_OK) { - fprintf(stderr, "reenroll csr: %s\n", wolfcert_strerror(rc)); - ret = 2; - } - } - WolfCertServerCfg srv = { .protocol = p, .server_url = opts.url }; if (ret == 0) { @@ -1529,14 +1527,17 @@ static int cmd_reenroll(int argc, char** argv) } if (ret == 0) { -#ifdef WOLFCERT_HAVE_EST - rc = wolfcert_est_simple_reenroll(&srv, cert_pem, cert_len, current_key, - csr.data, csr.len, &issued); -#else - rc = WOLFCERT_ERR_UNSUPPORTED; -#endif + meta.challenge_password = opts.challenge; + rc = wolfcert_client_reenroll(NULL, &srv, cert_pem, cert_len, + current_key, NULL, &meta, &new_key, + &issued); if (rc != WOLFCERT_OK) { fprintf(stderr, "reenroll: %s\n", wolfcert_strerror(rc)); + const char* m = wolfcert_last_error_message(); + if (m && *m) { + fprintf(stderr, "reenroll: detail (wolfssl_err=%d): %s\n", + wolfcert_last_wolfssl_err(), m); + } ret = 2; } } @@ -1553,8 +1554,9 @@ static int cmd_reenroll(int argc, char** argv) } } - wolfcert_buffer_free(&csr); wolfcert_buffer_free(&issued); + if (new_key != NULL) + wolfcert_key_free(new_key); if (current_key != NULL) wolfcert_key_free(current_key); free(cert_pem); diff --git a/configure.ac b/configure.ac index 7f64b68..3a3aea3 100644 --- a/configure.ac +++ b/configure.ac @@ -233,14 +233,16 @@ AC_LINK_IFELSE( [[extern char FreeAltNames(void); extern char wc_SetDNSEntry(void); extern char wc_SetAltNamesFromList(void); -extern char SetLength(void);]], +extern char SetLength(void); +extern char GetASNTag(void); +extern char GetLength(void);]], [[return FreeAltNames() + wc_SetDNSEntry() + wc_SetAltNamesFromList() + - SetLength();]])], + SetLength() + GetASNTag() + GetLength();]])], [AC_MSG_RESULT([yes])], [AC_MSG_RESULT([no]) AC_MSG_ERROR([wolfSSL does not export its ASN helpers (wc_SetDNSEntry, - wc_SetAltNamesFromList, FreeAltNames, SetLength). Rebuild wolfSSL with - CPPFLAGS="-DWOLFSSL_PUBLIC_ASN".])]) + wc_SetAltNamesFromList, FreeAltNames, SetLength, GetASNTag, GetLength). + Rebuild wolfSSL with CPPFLAGS="-DWOLFSSL_PUBLIC_ASN".])]) LIBS="$save_LIBS" # wolfSSL default-on features that wolfCert requires unconditionally: AES, diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index b3e6a31..5e69c02 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -107,8 +107,11 @@ The layering rules that matter to an integrator: can include it without caring whether EST or SCEP is present. - **`wolfcert_client_*` is the high-level orchestrator.** `wolfcert_client_enroll` / `_reenroll` route to EST or SCEP based on - `WolfCertServerCfg.protocol`. Callers that want finer control reach directly - into the `wolfcert_est_*` / `wolfcert_scep_*` primitives. + `WolfCertServerCfg.protocol`. `_reenroll` copies the Subject and SAN of the + certificate being renewed into its CSR byte for byte (RFC 7030 section + 4.2.2) and refuses a `WolfCertCertMeta` that sets either. Callers that want + finer control reach directly into the `wolfcert_est_*` / `wolfcert_scep_*` + primitives. - **Protocol modules depend on subsystems, never the reverse**, and the test server lives below the public API — an embedder can hand it an already-accepted socket via `wolfcert_server_serve_fd()` instead of using @@ -614,7 +617,8 @@ time `#error`s. See [`EMBEDDED.md`](EMBEDDED.md#configuring-wolfcert-without-its `WOLFSSL_BASE64_ENCODE`, `WOLFSSL_ALT_NAMES`, `WOLFSSL_CERT_NAME_ALL`. A `NO_RSA` build hard-fails unless SCEP is disabled. CMake and autoconf also link-probe the `WOLFSSL_ASN_API` helpers wolfCert calls (`wc_SetDNSEntry`, -`wc_SetAltNamesFromList`, `FreeAltNames`, `SetLength`); a shared libwolfssl +`wc_SetAltNamesFromList`, `FreeAltNames`, `SetLength`, `GetASNTag`, +`GetLength`); a shared libwolfssl exports them only under one of `WOLFSSL_PUBLIC_ASN` / `OPENSSL_EXTRA` / `OPENSSL_EXTRA_X509_SMALL` / `WOLFSSL_TEST_CERT`, a static one always links them. `check_config.h` cannot test a link, so a header-only build that misses diff --git a/docs/EMBEDDED.md b/docs/EMBEDDED.md index 2594f79..00dd961 100644 --- a/docs/EMBEDDED.md +++ b/docs/EMBEDDED.md @@ -102,7 +102,7 @@ miscompile. | Consumer | Where it lives | Default size | Dominated by | |----------|----------------|--------------|--------------| | wolfSSL `Cert` (CSR / cert build) | **heap** (`wc_CertNew`) | ~20+ KB | `altNames[16384]` | -| wolfSSL `DecodedCert` (cert parse) | **stack**, transient | several KB | parse scratch | +| wolfSSL `DecodedCert` (cert parse) | **stack** or **heap**, transient | several KB | parse scratch | | HTTP request handling | **stack** (EST + client); **heap** (SCEP server) | 2-3 KB stack | request read buffer | The good news: wolfCert never stack-allocates a `Cert`. Every CSR/cert @@ -120,12 +120,12 @@ the bulk of the size (defaults from `wolfssl/wolfcrypt/asn_public.h`): | Macro | Default | Effect | |-------|---------|--------| | `WC_CTC_MAX_ALT_SIZE` | `16384` | size of `Cert.altNames[]` - the encoded SAN extension. **Single largest contributor.** | -| `WC_CTC_NAME_SIZE` | `64` | size of every `CertName` string field (CN, O, OU, ...). `CertName` carries ~19 such fields under the `WOLFSSL_CERT_NAME_ALL` + `WOLFSSL_CERT_EXT` config wolfCert requires, ×2 for issuer+subject, plus raw copies. | +| `WC_CTC_NAME_SIZE` | `64` | size of every `CertName` string field (CN, O, OU, ...). `CertName` carries ~23 such fields under the `WOLFSSL_CERT_NAME_ALL` + `WOLFSSL_CERT_EXT` config wolfCert requires, ×2 for issuer+subject, plus raw copies. | These are **wolfSSL** settings, not wolfCert ones - set them when you build wolfSSL (via `user_settings.h` or `CPPFLAGS`), and wolfCert picks up whatever wolfSSL provides. For example, to drop a `Cert` from ~20 KB to -~3 KB: +~5 KB: ```c /* user_settings.h, when building wolfSSL */ @@ -142,7 +142,10 @@ Trade-offs: `WOLFCERT_ERR_BAD_ARG` rather than truncating it - both when a client builds a CSR (`assign_rdn()` in `src/csr.c`) and when the test server issues from one (`wolfcert_copy_csr_subject()` in `src/ca_issue.c`) - so - shrinking this caps how long a CN you can request. + shrinking this caps how long a CN you can request. A renewal instead + copies the certificate's subject whole, so there the limit is the total + encoded name, `sizeof(CertName)` (about 800 bytes at 32), and a longer one + fails with `WOLFCERT_ERR_UNSUPPORTED`. - Disabling `WOLFSSL_CERT_NAME_ALL` and/or `WOLFSSL_CERT_EXT` in wolfSSL removes the less-common `CertName` fields entirely - but wolfCert's build requires both (see `CLAUDE.md` / `CMakeLists.txt`), so prefer diff --git a/src/client.c b/src/client.c index e054a86..a358a87 100644 --- a/src/client.c +++ b/src/client.c @@ -229,9 +229,6 @@ int wolfcert_client_reenroll(WolfCertClient* client, const WolfCertServerCfg* sr WolfCertKey** out_key, WolfCertBuffer* out_cert_pem) { (void)client; -#ifndef WOLFCERT_HAVE_EST - (void)current_cert_len; /* only the EST reenroll path reads it */ -#endif if (srv == NULL || current_cert == NULL || current_key == NULL || meta == NULL || out_key == NULL || out_cert_pem == NULL) return WOLFCERT_ERR_BAD_ARG; @@ -246,7 +243,8 @@ int wolfcert_client_reenroll(WolfCertClient* client, const WolfCertServerCfg* sr const WolfCertKey* signing_key = nk ? nk : current_key; WolfCertBuffer csr = { 0 }; - rc = wolfcert_csr_build(signing_key, meta, &csr); + rc = wolfcert_csr_build_ex(signing_key, meta, current_cert, + current_cert_len, &csr); if (rc != WOLFCERT_OK) { if (nk) wolfcert_key_free(nk); diff --git a/src/csr.c b/src/csr.c index a0b9132..c16cd95 100644 --- a/src/csr.c +++ b/src/csr.c @@ -271,12 +271,172 @@ static int choose_sig_type(const WolfCertKey* key, const WolfCertKeyAlg* alg, return alg->ctc_sig_default; } +/* Find the subjectAltName extension in a certificate's [3] Extensions and + * return its GeneralNames bytes; *san stays NULL when there is none. */ +static int find_san(const byte* ext, int ext_sz, const byte** san, + word32* san_len) +{ + static const byte san_oid[] = { ASN_OBJECT_ID, 0x03, 0x55, 0x1D, 0x11 }; + word32 idx = 0; + word32 end = 0; + word32 ext_end = 0; + int len = 0; + int rc = WOLFCERT_OK; + byte tag = 0; + + *san = NULL; + *san_len = 0; + if (ext == NULL || ext_sz <= 0) + return WOLFCERT_OK; + + if (GetASNTag(ext, &idx, &tag, (word32)ext_sz) < 0 || + tag != (ASN_CONTEXT_SPECIFIC | ASN_CONSTRUCTED | 3) || + GetLength(ext, &idx, &len, (word32)ext_sz) < 0 || + GetASNTag(ext, &idx, &tag, (word32)ext_sz) < 0 || + tag != (ASN_SEQUENCE | ASN_CONSTRUCTED) || + GetLength(ext, &idx, &len, (word32)ext_sz) < 0) + rc = WOLFCERT_ERR_PARSE; + else + end = idx + (word32)len; + + /* Extension ::= SEQUENCE { OID, critical BOOLEAN OPTIONAL, OCTET STRING } */ + while (rc == WOLFCERT_OK && *san == NULL && idx < end) { + if (GetASNTag(ext, &idx, &tag, end) < 0 || + tag != (ASN_SEQUENCE | ASN_CONSTRUCTED) || + GetLength(ext, &idx, &len, end) < 0) { + rc = WOLFCERT_ERR_PARSE; + } + else { + ext_end = idx + (word32)len; + if (ext_end - idx < sizeof(san_oid) || + memcmp(ext + idx, san_oid, sizeof(san_oid)) != 0) { + idx = ext_end; + } + else { + idx += (word32)sizeof(san_oid); + if (idx < ext_end && ext[idx] == ASN_BOOLEAN) { + if (GetASNTag(ext, &idx, &tag, ext_end) < 0 || + GetLength(ext, &idx, &len, ext_end) < 0) + rc = WOLFCERT_ERR_PARSE; + else + idx += (word32)len; + } + if (rc == WOLFCERT_OK && + (GetASNTag(ext, &idx, &tag, ext_end) < 0 || + tag != ASN_OCTET_STRING || + GetLength(ext, &idx, &len, ext_end) < 0)) + rc = WOLFCERT_ERR_PARSE; + if (rc == WOLFCERT_OK) { + *san = ext + idx; + *san_len = (word32)len; + } + } + } + } + + return rc; +} + +/* Copy renew_cert's Subject and SAN into cert */ +static int copy_cert_identity(Cert* cert, const uint8_t* renew_cert, + size_t renew_cert_len, void* heap) +{ + WolfCertBuffer pem_der = { 0 }; + const uint8_t* der = renew_cert; + size_t der_len = renew_cert_len; + DecodedCert* dc = NULL; + const byte* san = NULL; + word32 san_len = 0; + int rc = WOLFCERT_OK; + int wrc; + + if (!wolfcert_buffer_is_der(renew_cert, renew_cert_len)) { + rc = wolfcert_pem_cert_to_der(renew_cert, renew_cert_len, &pem_der, + heap); + if (rc == WOLFCERT_ERR_PARSE) + rc = WOLFCERT_ERR(WOLFCERT_ERR_PARSE, "csr", + "certificate being renewed is not PEM or DER"); + der = pem_der.data; + der_len = pem_der.len; + } + + if (rc == WOLFCERT_OK) { + dc = (DecodedCert*)WOLFCERT_XMALLOC(sizeof(*dc), heap); + if (dc == NULL) + rc = WOLFCERT_ERR_MEMORY; + } + if (rc == WOLFCERT_OK) { + wc_InitDecodedCert(dc, der, (word32)der_len, heap); + wrc = wc_ParseCert(dc, CERT_TYPE, NO_VERIFY, NULL); + /* Only the identity is read; an unknown critical extension is fine */ + if (wrc != 0 && wrc != ASN_CRIT_EXT_E) + rc = WOLFCERT_ERR(WOLFCERT_ERR_PARSE, "csr", + "certificate being renewed does not parse (%d)", + wrc); + } + + /* Leave an empty Name blank; copy any other into sbjRaw if NUL-free */ + if (rc == WOLFCERT_OK && dc->subjectRaw != NULL && + dc->subjectRawLen == 0) { + memset(&cert->subject, 0, sizeof(cert->subject)); + cert->sbjRaw[0] = '\0'; + } + else if (rc == WOLFCERT_OK && + (dc->subjectRaw == NULL || dc->subjectRawLen <= 0 || + dc->subjectRawLen >= (int)sizeof(cert->sbjRaw) || + memchr(dc->subjectRaw, 0x00, (size_t)dc->subjectRawLen) != NULL)) { + rc = WOLFCERT_ERR(WOLFCERT_ERR_UNSUPPORTED, "csr", + "certificate subject cannot be carried into a CSR"); + } + else if (rc == WOLFCERT_OK) { + memcpy(cert->sbjRaw, dc->subjectRaw, (size_t)dc->subjectRawLen); + cert->sbjRaw[dc->subjectRawLen] = '\0'; + } + + if (rc == WOLFCERT_OK && + find_san(dc->extensions, dc->extensionsSz, &san, &san_len) != + WOLFCERT_OK) + rc = WOLFCERT_ERR(WOLFCERT_ERR_PARSE, "csr", + "certificate extensions do not parse"); + if (rc == WOLFCERT_OK && san_len > sizeof(cert->altNames)) + rc = WOLFCERT_ERR(WOLFCERT_ERR_UNSUPPORTED, "csr", + "certificate SAN is %u bytes, limit %zu", + (unsigned)san_len, sizeof(cert->altNames)); + if (rc == WOLFCERT_OK) { + if (san != NULL) + memcpy(cert->altNames, san, san_len); + cert->altNamesSz = (int)san_len; + cert->altNamesCrit = dc->extSubjAltNameCrit; + } + + if (dc != NULL) { + wc_FreeDecodedCert(dc); + WOLFCERT_XFREE(dc, heap); + } + wolfcert_buffer_free(&pem_der); + return rc; +} + int wolfcert_csr_build(const WolfCertKey* key, const WolfCertCertMeta* meta, WolfCertBuffer* out_der) +{ + return wolfcert_csr_build_ex(key, meta, NULL, 0, out_der); +} + +int wolfcert_csr_build_ex(const WolfCertKey* key, const WolfCertCertMeta* meta, + const uint8_t* renew_cert, size_t renew_cert_len, + WolfCertBuffer* out_der) { if (key == NULL || meta == NULL || out_der == NULL) return WOLFCERT_ERR_BAD_ARG; + if (renew_cert != NULL && + (meta->subject_dn != NULL || meta->san_dns_len != 0 || + meta->san_ip_len != 0 || meta->san_uri_len != 0 || + meta->san_email_len != 0)) + return WOLFCERT_ERR(WOLFCERT_ERR_BAD_ARG, "csr", + "a renewal keeps the certificate's subject and SAN"); + void* heap = key->heap ? key->heap : wolfcert_default_heap(); Cert* cert = wc_CertNew(heap); @@ -332,6 +492,15 @@ int wolfcert_csr_build(const WolfCertKey* key, const WolfCertCertMeta* meta, } } + /* After customize, so the callback cannot change a renewal's identity */ + if (renew_cert != NULL) { + rc = copy_cert_identity(cert, renew_cert, renew_cert_len, heap); + if (rc != WOLFCERT_OK) { + wc_CertFree(cert); + return rc; + } + } + const WolfCertKeyAlg* alg = wolfcert_key_alg(key->type); if (alg == NULL) { wc_CertFree(cert); @@ -339,12 +508,14 @@ int wolfcert_csr_build(const WolfCertKey* key, const WolfCertCertMeta* meta, } cert->sigType = choose_sig_type(key, alg, meta); - /* Size the DER buffer: algorithm hint + RSA modulus head room. */ + /* Size the DER buffer: algorithm hint + RSA modulus head room + the + * subject and SAN carried. */ size_t der_cap = alg->der_cap_hint + 1024; if (key->type == WOLFCERT_KEY_RSA) { size_t bits = key->rsa_bits ? (size_t)key->rsa_bits : 4096; der_cap = bits + 2048; } + der_cap += (size_t)cert->altNamesSz + strlen((const char*)cert->sbjRaw); uint8_t* der = (uint8_t*)WOLFCERT_XMALLOC(der_cap, heap); if (der == NULL) { wc_CertFree(cert); diff --git a/src/internal.h b/src/internal.h index ca374de..e11fcbb 100644 --- a/src/internal.h +++ b/src/internal.h @@ -359,6 +359,14 @@ int wolfcert_transport_fd(const WolfCertTransport* t, void* conn); int wolfcert_pem_cert_to_der(const uint8_t* pem, size_t pem_len, WolfCertBuffer* out_der, void* heap); +/* wolfcert_csr_build() for a renewal: with renew_cert (PEM or DER) set, the + * CSR carries that certificate's Subject and SAN, and meta may not set them. */ +WOLFCERT_TEST_VIS int wolfcert_csr_build_ex(const WolfCertKey* key, + const WolfCertCertMeta* meta, + const uint8_t* renew_cert, + size_t renew_cert_len, + WolfCertBuffer* out_der); + /* Heuristically classify a buffer as DER vs PEM. DER (ASN.1) starts with a * SEQUENCE tag (0x30) once any leading whitespace is skipped; PEM starts with * the "-----BEGIN" armor. Returns 1 if the buffer looks like DER, else 0. */ diff --git a/tests/integration/cli_proto_scoping.sh b/tests/integration/cli_proto_scoping.sh index 3ccdae5..f3ac0c6 100755 --- a/tests/integration/cli_proto_scoping.sh +++ b/tests/integration/cli_proto_scoping.sh @@ -104,9 +104,19 @@ expect_reject "over-long --serial" "20 octets" \ # reenroll authenticates TLS with --cert/--key, so a separate identity is refused. expect_reject "reenroll with --client-cert" "--client-cert/--client-key are not used" \ - reenroll --proto est --url "$EST_URL" --subject CN=x --cert /dev/null \ + reenroll --proto est --url "$EST_URL" --cert /dev/null \ --key /dev/null --client-cert /dev/null --client-key /dev/null +# reenroll refuses --subject and --san-*. +expect_reject "reenroll with --subject" "--subject/--san-* are not used" \ + reenroll --proto est --url "$EST_URL" --subject CN=x --cert /dev/null \ + --key /dev/null +for san in --san-dns --san-ip --san-uri --san-email; do + expect_reject "reenroll with $san" "--subject/--san-* are not used" \ + reenroll --proto est --url "$EST_URL" "$san" x --cert /dev/null \ + --key /dev/null +done + # The accept side of the same boundary: 20 octets is the longest RFC 5280 # permits and must get past parse_serial, failing later on the unreachable # port instead. Without this a regression to `n > CLI_SERIAL_MAX` (a one-byte diff --git a/tests/integration/test_est_tls_roundtrip.c b/tests/integration/test_est_tls_roundtrip.c index 4f8999b..5771cb1 100644 --- a/tests/integration/test_est_tls_roundtrip.c +++ b/tests/integration/test_est_tls_roundtrip.c @@ -42,6 +42,7 @@ #include #include "tls_test_util.h" +#include "est_client_cases.h" #include #include @@ -60,6 +61,144 @@ static void* server_thread(void* arg) { wolfcert_server_run((WolfCertServer*)arg); return NULL; } +static int impostor_customize(void* wolfssl_cert, void* ctx) +{ + Cert* c = (Cert*)wolfssl_cert; + + *(int*)ctx = 1; + snprintf(c->subject.commonName, sizeof(c->subject.commonName), "%s", + "impostor"); + return WOLFCERT_OK; +} + +/* The issued cert must name CN=reenroll-device with iPAddress SAN 127.0.0.1. */ +static int check_renewed_identity(const WolfCertBuffer* issued) +{ + static const uint8_t ip[4] = { 127, 0, 0, 1 }; + uint8_t der[4096]; + DecodedCert dc; + int der_len; + int found; + + der_len = wc_CertPemToDer(issued->data, (int)issued->len, der, + (int)sizeof(der), CERT_TYPE); + REQUIRE(der_len > 0); + + wc_InitDecodedCert(&dc, der, (word32)der_len, NULL); + REQUIRE(wc_ParseCert(&dc, CERT_TYPE, NO_VERIFY, NULL) == 0); + REQUIRE(dc.subjectCN != NULL); + REQUIRE(dc.subjectCNLen == (int)strlen("reenroll-device")); + REQUIRE(memcmp(dc.subjectCN, "reenroll-device", + strlen("reenroll-device")) == 0); + found = has_alt(dc.altNames, ASN_IP_TYPE, (const char*)ip, + (int)sizeof(ip)); + wc_FreeDecodedCert(&dc); + REQUIRE(found); + return 0; +} + +/* 1 when both PEM certs carry the same public key, 0 when not, -1 on error. */ +static int same_public_key(const uint8_t* a, size_t a_len, const uint8_t* b, + size_t b_len) +{ + uint8_t der[2][4096]; + DecodedCert dc[2]; + int len[2]; + int ret = -1; + + len[0] = wc_CertPemToDer(a, (int)a_len, der[0], (int)sizeof(der[0]), + CERT_TYPE); + len[1] = wc_CertPemToDer(b, (int)b_len, der[1], (int)sizeof(der[1]), + CERT_TYPE); + if (len[0] <= 0 || len[1] <= 0) + return -1; + + wc_InitDecodedCert(&dc[0], der[0], (word32)len[0], NULL); + wc_InitDecodedCert(&dc[1], der[1], (word32)len[1], NULL); + if (wc_ParseCert(&dc[0], CERT_TYPE, NO_VERIFY, NULL) == 0 && + wc_ParseCert(&dc[1], CERT_TYPE, NO_VERIFY, NULL) == 0) + ret = dc[0].pubKeySize == dc[1].pubKeySize && + memcmp(dc[0].publicKey, dc[1].publicKey, + dc[0].pubKeySize) == 0; + wc_FreeDecodedCert(&dc[0]); + wc_FreeDecodedCert(&dc[1]); + return ret; +} + +/* Reenroll with a mismatched meta, with a renaming customize callback, then + * with a fresh key. */ +static int test_client_reenroll_keeps_identity(const char* url, + const uint8_t* tls_cert, + size_t tls_cert_len) +{ + static const char* const impostor_dns[] = { "impostor.example" }; + uint8_t* cur_cert = NULL; + size_t cur_cert_len = 0; + uint8_t* cur_key_pem = NULL; + size_t cur_key_len = 0; + WolfCertKey* cur_key = NULL; + WolfCertKey* out_key = NULL; + WolfCertBuffer issued = { 0 }; + WolfCertCertMeta meta; + WolfCertServerCfg cli = { + .protocol = WOLFCERT_PROTO_EST, + .server_url = url, + .trust_anchors = tls_cert, + .trust_anchors_len = tls_cert_len, + .verify_server = 1, + }; + WolfCertKeyCfg kcfg = { .type = TEST_ENROLL_KEY_TYPE, + .param = TEST_ENROLL_KEY_PARAM, + .dev_id = WOLFCERT_DEVID_SOFTWARE }; + int called = 0; + + REQUIRE(mint_self_id("reenroll-device", 0, &cur_cert, &cur_cert_len, + &cur_key_pem, &cur_key_len) == 0); + REQUIRE(wolfcert_key_from_pem(cur_key_pem, cur_key_len, NULL, &cur_key) + == WOLFCERT_OK); + + memset(&meta, 0, sizeof(meta)); + meta.subject_dn = "CN=impostor"; + REQUIRE(wolfcert_client_reenroll(NULL, &cli, cur_cert, cur_cert_len, + cur_key, NULL, &meta, &out_key, &issued) + == WOLFCERT_ERR_BAD_ARG); + + memset(&meta, 0, sizeof(meta)); + meta.san_dns = impostor_dns; + meta.san_dns_len = 1; + REQUIRE(wolfcert_client_reenroll(NULL, &cli, cur_cert, cur_cert_len, + cur_key, NULL, &meta, &out_key, &issued) + == WOLFCERT_ERR_BAD_ARG); + + /* The caller's callback still runs but cannot rename the cert. */ + memset(&meta, 0, sizeof(meta)); + meta.customize = impostor_customize; + meta.customize_ctx = &called; + REQUIRE(wolfcert_client_reenroll(NULL, &cli, cur_cert, cur_cert_len, + cur_key, NULL, &meta, &out_key, &issued) + == WOLFCERT_OK); + REQUIRE(called == 1); + REQUIRE(out_key == NULL); + REQUIRE(check_renewed_identity(&issued) == 0); + wolfcert_buffer_free(&issued); + + memset(&meta, 0, sizeof(meta)); + REQUIRE(wolfcert_client_reenroll(NULL, &cli, cur_cert, cur_cert_len, + cur_key, &kcfg, &meta, &out_key, &issued) + == WOLFCERT_OK); + REQUIRE(out_key != NULL); + REQUIRE(check_renewed_identity(&issued) == 0); + REQUIRE(same_public_key(issued.data, issued.len, cur_cert, + cur_cert_len) == 0); + + wolfcert_key_free(out_key); + wolfcert_buffer_free(&issued); + wolfcert_key_free(cur_key); + free(cur_cert); + free(cur_key_pem); + return 0; +} + int main(void) { REQUIRE(wolfcert_init(NULL) == WOLFCERT_OK); @@ -157,6 +296,9 @@ int main(void) wolfcert_buffer_free(&ca_pem_der); wc_FreeDer(&ta_der); + REQUIRE(test_client_reenroll_keeps_identity(url, tls_cert, tls_cert_len) + == 0); + wolfcert_server_stop(srv); pthread_join(tid, NULL); wolfcert_server_free(srv); diff --git a/tests/unit/test_csr.c b/tests/unit/test_csr.c index 78b83e9..01007a1 100644 --- a/tests/unit/test_csr.c +++ b/tests/unit/test_csr.c @@ -27,6 +27,8 @@ #include #include +#include + #include #include @@ -212,6 +214,426 @@ static int csr_build_rejects_oversized_rdn(void) } #endif +#ifdef WOLFCERT_HAVE_ECC +/* Self-sign the subject and SAN set in c with the test's ECC key, then free + * c; returns the DER length. */ +static int make_self_cert(Cert* c, const WolfCertKey* key, byte* out, int cap) +{ + WC_RNG rng; + int len = -1; + + if (wc_InitRng(&rng) == 0) { + c->sigType = CTC_SHA256wECDSA; + c->selfSigned = 1; + if (wc_MakeCert_ex(c, out, (word32)cap, ECC_TYPE, key->impl, + &rng) > 0) + len = wc_SignCert_ex(c->bodySz, c->sigType, out, (word32)cap, + ECC_TYPE, key->impl, &rng); + wc_FreeRng(&rng); + } + wc_CertFree(c); + return len; +} + +/* A caller callback that names the CSR itself: CN=cb, DNS:cb.example. */ +static int add_identity_customize(void* wolfssl_cert, void* ctx) +{ + static const byte san[] = { 0x30, 0x0c, 0x82, 0x0a, 'c', 'b', '.', 'e', + 'x', 'a', 'm', 'p', 'l', 'e' }; + Cert* c = (Cert*)wolfssl_cert; + + *(int*)ctx = 1; + snprintf(c->subject.commonName, sizeof(c->subject.commonName), "%s", "cb"); + memcpy(c->altNames, san, sizeof(san)); + c->altNamesSz = (int)sizeof(san); + return WOLFCERT_OK; +} + +/* Renew a cert that has an empty subject and a SAN, critical or not. */ +static int renewal_keeps_empty_subject(int san_crit) +{ + static const byte san[] = { 0x30, 0x0d, 0x82, 0x0b, 'd', 'e', 'v', '.', + 'e', 'x', 'a', 'm', 'p', 'l', 'e' }; + WolfCertKeyCfg cfg = { .type = WOLFCERT_KEY_ECC, .param = 256, + .dev_id = WOLFCERT_DEVID_SOFTWARE }; + WolfCertKey* key = NULL; + WolfCertCertMeta meta = { 0 }; + WolfCertBuffer csr = { 0 }; + byte cert[1024]; + int cert_len; + int called = 0; + Cert* c; + DecodedCert dc; + + REQUIRE(wolfcert_key_generate(&cfg, &key) == WOLFCERT_OK); + REQUIRE((c = wc_CertNew(NULL)) != NULL); + memcpy(c->altNames, san, sizeof(san)); + c->altNamesSz = (int)sizeof(san); + c->altNamesCrit = san_crit ? 1 : 0; + cert_len = make_self_cert(c, key, cert, (int)sizeof(cert)); + REQUIRE(cert_len > 0); + + meta.customize = add_identity_customize; + meta.customize_ctx = &called; + REQUIRE(wolfcert_csr_build_ex(key, &meta, cert, (size_t)cert_len, &csr) + == WOLFCERT_OK); + REQUIRE(called == 1); + + wc_InitDecodedCert(&dc, csr.data, (word32)csr.len, NULL); + REQUIRE(wc_ParseCert(&dc, CERTREQ_TYPE, NO_VERIFY, NULL) == 0); + REQUIRE(dc.subjectRaw != NULL && dc.subjectRawLen == 0); + REQUIRE(dc.extSubjAltNameCrit == (san_crit ? 1 : 0)); + REQUIRE(dc.altNames != NULL && dc.altNames->next == NULL); + REQUIRE(dc.altNames->type == ASN_DNS_TYPE && dc.altNames->len == 11); + REQUIRE(memcmp(dc.altNames->name, "dev.example", 11) == 0); + wc_FreeDecodedCert(&dc); + + wolfcert_buffer_free(&csr); + wolfcert_key_free(key); + return 0; +} + +/* CN=crit,OU=Devices,O=Acme,C=US in that order (C as a PrintableString), + * then SKID, an unknown critical extension and DNS:dev.example. */ +static const byte crit_ext_cert[] = { + 0x30, 0x82, 0x01, 0xcb, 0x30, 0x82, 0x01, 0x71, 0xa0, 0x03, 0x02, 0x01, + 0x02, 0x02, 0x14, 0x1f, 0xb1, 0xc9, 0xdd, 0x7e, 0x9c, 0xbe, 0x2b, 0xf9, + 0xea, 0x9c, 0x44, 0x25, 0x5f, 0x82, 0x21, 0x86, 0xb2, 0xe3, 0xbd, 0x30, + 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x30, + 0x3d, 0x31, 0x0d, 0x30, 0x0b, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, 0x04, + 0x63, 0x72, 0x69, 0x74, 0x31, 0x10, 0x30, 0x0e, 0x06, 0x03, 0x55, 0x04, + 0x0b, 0x0c, 0x07, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x31, 0x0d, + 0x30, 0x0b, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x04, 0x41, 0x63, 0x6d, + 0x65, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, + 0x55, 0x53, 0x30, 0x20, 0x17, 0x0d, 0x32, 0x36, 0x30, 0x39, 0x32, 0x39, + 0x31, 0x32, 0x30, 0x35, 0x31, 0x30, 0x5a, 0x18, 0x0f, 0x32, 0x31, 0x32, + 0x36, 0x30, 0x39, 0x30, 0x35, 0x31, 0x32, 0x30, 0x35, 0x31, 0x30, 0x5a, + 0x30, 0x3d, 0x31, 0x0d, 0x30, 0x0b, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, + 0x04, 0x63, 0x72, 0x69, 0x74, 0x31, 0x10, 0x30, 0x0e, 0x06, 0x03, 0x55, + 0x04, 0x0b, 0x0c, 0x07, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x31, + 0x0d, 0x30, 0x0b, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x04, 0x41, 0x63, + 0x6d, 0x65, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, + 0x02, 0x55, 0x53, 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, + 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, + 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0x22, 0x45, 0x76, 0xfe, 0xee, 0x7c, + 0xef, 0xec, 0xaa, 0x4e, 0x47, 0x76, 0x00, 0xe0, 0x93, 0x85, 0x3d, 0x3d, + 0x71, 0x25, 0xaf, 0x25, 0x46, 0x31, 0x97, 0x9c, 0x83, 0xf8, 0x9e, 0xc9, + 0x3b, 0x2f, 0x7c, 0xe4, 0xa1, 0x01, 0x5a, 0x80, 0x44, 0x19, 0x29, 0xc2, + 0xef, 0x37, 0xce, 0x72, 0x43, 0x97, 0x02, 0x57, 0xc9, 0x56, 0xb8, 0x95, + 0x0d, 0x76, 0x98, 0xf8, 0xba, 0x43, 0x0d, 0xad, 0x80, 0x25, 0xa3, 0x4d, + 0x30, 0x4b, 0x30, 0x1d, 0x06, 0x03, 0x55, 0x1d, 0x0e, 0x04, 0x16, 0x04, + 0x14, 0x5f, 0xd9, 0xac, 0x6d, 0x30, 0x06, 0x20, 0x97, 0xb3, 0x21, 0x64, + 0x88, 0x96, 0xb8, 0x92, 0x11, 0xa2, 0x08, 0xba, 0xe5, 0x30, 0x12, 0x06, + 0x09, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x86, 0x8d, 0x1f, 0x01, 0x01, 0x01, + 0xff, 0x04, 0x02, 0x05, 0x00, 0x30, 0x16, 0x06, 0x03, 0x55, 0x1d, 0x11, + 0x04, 0x0f, 0x30, 0x0d, 0x82, 0x0b, 0x64, 0x65, 0x76, 0x2e, 0x65, 0x78, + 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, + 0xce, 0x3d, 0x04, 0x03, 0x02, 0x03, 0x48, 0x00, 0x30, 0x45, 0x02, 0x20, + 0x2a, 0xb4, 0xe2, 0x45, 0xf6, 0x83, 0xf1, 0x6c, 0x16, 0x30, 0x57, 0xf5, + 0x84, 0x7e, 0x56, 0x21, 0xcd, 0x1c, 0x50, 0xb3, 0x2f, 0xe2, 0x6a, 0xe1, + 0x6f, 0x18, 0xbe, 0x11, 0xee, 0x22, 0x54, 0xf9, 0x02, 0x21, 0x00, 0x98, + 0x92, 0xf7, 0xed, 0x15, 0x7c, 0x0a, 0xf6, 0xfc, 0x53, 0xd6, 0x61, 0x11, + 0xa5, 0x63, 0x73, 0x92, 0xe6, 0x66, 0xfa, 0x06, 0x93, 0x19, 0x2d, 0x90, + 0xeb, 0xea, 0x57, 0x1f, 0x00, 0x06, 0x00 +}; + +/* Renew a CA-shaped cert: reverse-order RDNs, other extensions before the + * SAN, one of them critical and unknown. */ +static int renewal_reads_past_unknown_critical_ext(void) +{ + WolfCertKeyCfg cfg = { .type = WOLFCERT_KEY_ECC, .param = 256, + .dev_id = WOLFCERT_DEVID_SOFTWARE }; + WolfCertKey* key = NULL; + WolfCertCertMeta meta = { 0 }; + WolfCertBuffer csr = { 0 }; + DecodedCert cc; + DecodedCert dc; + + wc_InitDecodedCert(&cc, crit_ext_cert, sizeof(crit_ext_cert), NULL); + REQUIRE(wc_ParseCert(&cc, CERT_TYPE, NO_VERIFY, NULL) == ASN_CRIT_EXT_E); + + REQUIRE(wolfcert_key_generate(&cfg, &key) == WOLFCERT_OK); + REQUIRE(wolfcert_csr_build_ex(key, &meta, crit_ext_cert, + sizeof(crit_ext_cert), &csr) == WOLFCERT_OK); + + wc_InitDecodedCert(&dc, csr.data, (word32)csr.len, NULL); + REQUIRE(wc_ParseCert(&dc, CERTREQ_TYPE, NO_VERIFY, NULL) == 0); + REQUIRE(dc.subjectRawLen == cc.subjectRawLen); + REQUIRE(memcmp(dc.subjectRaw, cc.subjectRaw, + (size_t)cc.subjectRawLen) == 0); + REQUIRE(dc.extSubjAltNameCrit == 0); + REQUIRE(dc.altNames != NULL && dc.altNames->next == NULL); + REQUIRE(dc.altNames->type == ASN_DNS_TYPE && dc.altNames->len == 11); + REQUIRE(memcmp(dc.altNames->name, "dev.example", 11) == 0); + wc_FreeDecodedCert(&dc); + wc_FreeDecodedCert(&cc); + + wolfcert_buffer_free(&csr); + wolfcert_key_free(key); + return 0; +} + +/* CN=bmp as a BMPString, which puts 0x00 bytes in the Name. */ +static const byte bmp_subject_cert[] = { + 0x30, 0x82, 0x01, 0x5e, 0x30, 0x82, 0x01, 0x05, 0xa0, 0x03, 0x02, 0x01, + 0x02, 0x02, 0x14, 0x43, 0x68, 0xe0, 0xfb, 0x08, 0x34, 0x31, 0x5d, 0xca, + 0xdf, 0x05, 0x66, 0xdb, 0xb3, 0x73, 0x4b, 0x98, 0x20, 0xb9, 0xda, 0x30, + 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x30, + 0x11, 0x31, 0x0f, 0x30, 0x0d, 0x06, 0x03, 0x55, 0x04, 0x03, 0x1e, 0x06, + 0x00, 0x62, 0x00, 0x6d, 0x00, 0x70, 0x30, 0x20, 0x17, 0x0d, 0x32, 0x36, + 0x30, 0x39, 0x32, 0x39, 0x31, 0x32, 0x30, 0x35, 0x31, 0x31, 0x5a, 0x18, + 0x0f, 0x32, 0x31, 0x32, 0x36, 0x30, 0x39, 0x30, 0x35, 0x31, 0x32, 0x30, + 0x35, 0x31, 0x31, 0x5a, 0x30, 0x11, 0x31, 0x0f, 0x30, 0x0d, 0x06, 0x03, + 0x55, 0x04, 0x03, 0x1e, 0x06, 0x00, 0x62, 0x00, 0x6d, 0x00, 0x70, 0x30, + 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, + 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, + 0x00, 0x04, 0x33, 0x7c, 0xb9, 0xd0, 0x1f, 0xec, 0xec, 0x91, 0x82, 0x33, + 0x14, 0x87, 0xea, 0xd3, 0x5f, 0x63, 0xd4, 0x0b, 0xc8, 0xd3, 0x65, 0x21, + 0x5d, 0x27, 0x95, 0xcd, 0xc4, 0xd9, 0x04, 0x97, 0x17, 0x2e, 0x6f, 0xcf, + 0x6c, 0x02, 0x40, 0x1b, 0x92, 0xdc, 0x52, 0xdd, 0xe8, 0x05, 0x7d, 0x41, + 0x20, 0x00, 0xcd, 0x5c, 0x33, 0x16, 0x27, 0xf6, 0xab, 0x6b, 0x56, 0x7a, + 0x16, 0x3e, 0x13, 0x9e, 0x3f, 0x34, 0xa3, 0x39, 0x30, 0x37, 0x30, 0x16, + 0x06, 0x03, 0x55, 0x1d, 0x11, 0x04, 0x0f, 0x30, 0x0d, 0x82, 0x0b, 0x64, + 0x65, 0x76, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x30, 0x1d, + 0x06, 0x03, 0x55, 0x1d, 0x0e, 0x04, 0x16, 0x04, 0x14, 0xab, 0x01, 0x31, + 0xe8, 0x49, 0x21, 0xdf, 0x6d, 0x69, 0x97, 0x4d, 0x8e, 0x38, 0xa3, 0xbe, + 0xbc, 0x30, 0xdd, 0x89, 0xc6, 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, + 0xce, 0x3d, 0x04, 0x03, 0x02, 0x03, 0x47, 0x00, 0x30, 0x44, 0x02, 0x20, + 0x43, 0x42, 0x4c, 0xb0, 0x4c, 0x42, 0xc1, 0xb2, 0xed, 0xfd, 0xb0, 0x0a, + 0x56, 0xc7, 0xd5, 0x38, 0xc1, 0x1f, 0x46, 0x56, 0x55, 0xa8, 0x05, 0x8d, + 0xa2, 0xfd, 0xea, 0x80, 0xdb, 0x98, 0x14, 0xc9, 0x02, 0x20, 0x38, 0x11, + 0x0d, 0xac, 0x84, 0xef, 0x6e, 0x93, 0x0d, 0xed, 0x51, 0x86, 0xb7, 0x54, + 0xac, 0x00, 0x27, 0x8f, 0xa7, 0x84, 0xf6, 0xa4, 0xd5, 0x97, 0x6b, 0x4b, + 0x4f, 0xac, 0x26, 0xd0, 0xc7, 0x4e +}; + +/* A subject the CSR cannot carry is refused rather than truncated. */ +static int renewal_refuses_bmpstring_subject(void) +{ + WolfCertKeyCfg cfg = { .type = WOLFCERT_KEY_ECC, .param = 256, + .dev_id = WOLFCERT_DEVID_SOFTWARE }; + WolfCertKey* key = NULL; + WolfCertCertMeta meta = { 0 }; + WolfCertBuffer csr = { 0 }; + + REQUIRE(wolfcert_key_generate(&cfg, &key) == WOLFCERT_OK); + REQUIRE(wolfcert_csr_build_ex(key, &meta, bmp_subject_cert, + sizeof(bmp_subject_cert), &csr) + == WOLFCERT_ERR_UNSUPPORTED); + REQUIRE(csr.data == NULL); + + wolfcert_key_free(key); + return 0; +} + +/* Renew a cert with no SAN while the caller's callback adds one. */ +static int renewal_drops_callback_san(void) +{ + WolfCertKeyCfg cfg = { .type = WOLFCERT_KEY_ECC, .param = 256, + .dev_id = WOLFCERT_DEVID_SOFTWARE }; + WolfCertKey* key = NULL; + WolfCertCertMeta meta = { 0 }; + WolfCertBuffer csr = { 0 }; + byte cert[1024]; + int cert_len; + int called = 0; + Cert* c; + DecodedCert dc; + + REQUIRE(wolfcert_key_generate(&cfg, &key) == WOLFCERT_OK); + REQUIRE((c = wc_CertNew(NULL)) != NULL); + strcpy(c->subject.commonName, "plain"); + cert_len = make_self_cert(c, key, cert, (int)sizeof(cert)); + REQUIRE(cert_len > 0); + + meta.customize = add_identity_customize; + meta.customize_ctx = &called; + REQUIRE(wolfcert_csr_build_ex(key, &meta, cert, (size_t)cert_len, &csr) + == WOLFCERT_OK); + REQUIRE(called == 1); + + wc_InitDecodedCert(&dc, csr.data, (word32)csr.len, NULL); + REQUIRE(wc_ParseCert(&dc, CERTREQ_TYPE, NO_VERIFY, NULL) == 0); + REQUIRE(dc.subjectCN != NULL && dc.subjectCNLen == 5); + REQUIRE(memcmp(dc.subjectCN, "plain", 5) == 0); + REQUIRE(dc.altNames == NULL && dc.extSubjAltNameSet == 0); + wc_FreeDecodedCert(&dc); + + wolfcert_buffer_free(&csr); + wolfcert_key_free(key); + return 0; +} + +/* Renew a CA-shaped cert whose extensions carry no SAN. */ +static int renewal_walks_extensions_without_san(void) +{ + WolfCertKeyCfg cfg = { .type = WOLFCERT_KEY_ECC, .param = 256, + .dev_id = WOLFCERT_DEVID_SOFTWARE }; + WolfCertKey* key = NULL; + WolfCertCertMeta meta = { 0 }; + WolfCertBuffer csr = { 0 }; + byte cert[1024]; + int cert_len; + Cert* c; + DecodedCert dc; + + REQUIRE(wolfcert_key_generate(&cfg, &key) == WOLFCERT_OK); + REQUIRE((c = wc_CertNew(NULL)) != NULL); + strcpy(c->subject.commonName, "nosan"); + c->isCA = 1; + REQUIRE(wc_SetKeyUsage(c, "digitalSignature,keyCertSign") == 0); + cert_len = make_self_cert(c, key, cert, (int)sizeof(cert)); + REQUIRE(cert_len > 0); + + REQUIRE(wolfcert_csr_build_ex(key, &meta, cert, (size_t)cert_len, &csr) + == WOLFCERT_OK); + + wc_InitDecodedCert(&dc, csr.data, (word32)csr.len, NULL); + REQUIRE(wc_ParseCert(&dc, CERTREQ_TYPE, NO_VERIFY, NULL) == 0); + REQUIRE(dc.subjectCN != NULL && dc.subjectCNLen == 5); + REQUIRE(memcmp(dc.subjectCN, "nosan", 5) == 0); + REQUIRE(dc.altNames == NULL && dc.extSubjAltNameSet == 0); + wc_FreeDecodedCert(&dc); + + wolfcert_buffer_free(&csr); + wolfcert_key_free(key); + return 0; +} + +/* Pass a private key PEM where the certificate being renewed belongs. */ +static int renewal_rejects_non_certificate(void) +{ + WolfCertKeyCfg cfg = { .type = WOLFCERT_KEY_ECC, .param = 256, + .dev_id = WOLFCERT_DEVID_SOFTWARE }; + WolfCertKey* key = NULL; + WolfCertCertMeta meta = { 0 }; + WolfCertBuffer key_pem = { 0 }; + WolfCertBuffer csr = { 0 }; + + REQUIRE(wolfcert_key_generate(&cfg, &key) == WOLFCERT_OK); + REQUIRE(wolfcert_key_to_pem(key, &key_pem) == WOLFCERT_OK); + REQUIRE(wolfcert_csr_build_ex(key, &meta, key_pem.data, key_pem.len, + &csr) == WOLFCERT_ERR_PARSE); + REQUIRE(csr.data == NULL); + REQUIRE(strstr(wolfcert_last_error_message(), "not PEM or DER") != NULL); + + wolfcert_buffer_free(&key_pem); + wolfcert_key_free(key); + return 0; +} + +/* An enroll meta reused for a renewal is refused for each identity field. */ +static int renewal_rejects_meta_identity(void) +{ + static const char* const one[] = { "x" }; + WolfCertKeyCfg cfg = { .type = WOLFCERT_KEY_ECC, .param = 256, + .dev_id = WOLFCERT_DEVID_SOFTWARE }; + WolfCertKey* key = NULL; + WolfCertCertMeta meta; + WolfCertBuffer csr = { 0 }; + int i; + + REQUIRE(wolfcert_key_generate(&cfg, &key) == WOLFCERT_OK); + for (i = 0; i < 5; i++) { + memset(&meta, 0, sizeof(meta)); + switch (i) { + case 0: + meta.subject_dn = "CN=x"; + break; + case 1: + meta.san_dns = one; + meta.san_dns_len = 1; + break; + case 2: + meta.san_ip = one; + meta.san_ip_len = 1; + break; + case 3: + meta.san_uri = one; + meta.san_uri_len = 1; + break; + default: + meta.san_email = one; + meta.san_email_len = 1; + break; + } + REQUIRE(wolfcert_csr_build_ex(key, &meta, crit_ext_cert, + sizeof(crit_ext_cert), &csr) + == WOLFCERT_ERR_BAD_ARG); + REQUIRE(csr.data == NULL); + } + + wolfcert_key_free(key); + return 0; +} + +#define LARGE_SAN_COUNT 60 + +static int count_csr_sans(const WolfCertBuffer* csr) +{ + DecodedCert dc; + DNS_entry* e; + int n = 0; + + wc_InitDecodedCert(&dc, csr->data, (word32)csr->len, NULL); + if (wc_ParseCert(&dc, CERTREQ_TYPE, NO_VERIFY, NULL) == 0) { + for (e = dc.altNames; e != NULL; e = e->next) + n++; + } + wc_FreeDecodedCert(&dc); + return n; +} + +/* 60 dNSNames, about 2 KB of SAN, from meta and from a renewed cert. */ +static int csr_carries_large_san(void) +{ + WolfCertKeyCfg cfg = { .type = WOLFCERT_KEY_ECC, .param = 256, + .dev_id = WOLFCERT_DEVID_SOFTWARE }; + char names[LARGE_SAN_COUNT][40]; + const char* dns[LARGE_SAN_COUNT]; + WolfCertKey* key = NULL; + WolfCertCertMeta meta = { 0 }; + WolfCertBuffer csr = { 0 }; + DNS_entry* list = NULL; + byte cert[4096]; + int cert_len; + int i; + Cert* c; + + REQUIRE(wolfcert_key_generate(&cfg, &key) == WOLFCERT_OK); + REQUIRE((c = wc_CertNew(NULL)) != NULL); + for (i = 0; i < LARGE_SAN_COUNT; i++) { + snprintf(names[i], sizeof(names[i]), "device-%02d.fleet.example.com", + i); + dns[i] = names[i]; + REQUIRE(wc_SetDNSEntry(NULL, names[i], (int)strlen(names[i]), + ASN_DNS_TYPE, &list) == 0); + } + REQUIRE(wc_SetAltNamesFromList(c, list) == 0); + FreeAltNames(list, NULL); + strcpy(c->subject.commonName, "big"); + cert_len = make_self_cert(c, key, cert, (int)sizeof(cert)); + REQUIRE(cert_len > 0); + + + meta.subject_dn = "CN=big"; + meta.san_dns = dns; + meta.san_dns_len = LARGE_SAN_COUNT; + REQUIRE(wolfcert_csr_build(key, &meta, &csr) == WOLFCERT_OK); + REQUIRE(count_csr_sans(&csr) == LARGE_SAN_COUNT); + wolfcert_buffer_free(&csr); + + memset(&meta, 0, sizeof(meta)); + REQUIRE(wolfcert_csr_build_ex(key, &meta, cert, (size_t)cert_len, &csr) + == WOLFCERT_OK); + REQUIRE(count_csr_sans(&csr) == LARGE_SAN_COUNT); + + wolfcert_buffer_free(&csr); + wolfcert_key_free(key); + return 0; +} +#endif + int main(void) { REQUIRE(test_static_mem_init() == 0); @@ -224,6 +646,22 @@ int main(void) #ifdef WOLFCERT_HAVE_ECC if (csr_build_rejects_oversized_rdn()) return 1; + if (renewal_keeps_empty_subject(1) || renewal_keeps_empty_subject(0)) + return 1; + if (renewal_reads_past_unknown_critical_ext()) + return 1; + if (renewal_refuses_bmpstring_subject()) + return 1; + if (renewal_drops_callback_san()) + return 1; + if (renewal_rejects_meta_identity()) + return 1; + if (renewal_walks_extensions_without_san()) + return 1; + if (renewal_rejects_non_certificate()) + return 1; + if (csr_carries_large_san()) + return 1; if (build_and_reparse(WOLFCERT_KEY_ECC, 256)) return 1; #endif diff --git a/wolfcert/client.h b/wolfcert/client.h index 675471c..9cf3b95 100644 --- a/wolfcert/client.h +++ b/wolfcert/client.h @@ -67,8 +67,9 @@ WOLFCERT_API int wolfcert_client_enroll(WolfCertClient* client, WolfCertKey** out_key, WolfCertBuffer* out_cert_pem); -/* Re-enroll using an existing cert/key. If new_key_cfg is NULL the existing - * key is reused; otherwise a fresh key is generated. */ +/* Re-enroll using an existing cert/key; a NULL new_key_cfg reuses the key. + * The CSR copies current_cert's Subject and SAN: WOLFCERT_ERR_BAD_ARG if meta + * sets them, _PARSE for a bad cert, _UNSUPPORTED if one cannot be carried. */ WOLFCERT_API int wolfcert_client_reenroll(WolfCertClient* client, const WolfCertServerCfg* srv, const uint8_t* current_cert, size_t current_cert_len, diff --git a/wolfcert/types.h b/wolfcert/types.h index 06023e9..2bcada6 100644 --- a/wolfcert/types.h +++ b/wolfcert/types.h @@ -105,11 +105,9 @@ typedef struct { void* heap; /* optional heap hint; NULL = default */ } WolfCertKeyCfg; -/* Callback invoked by wolfcert_csr_build() after the standard fields are - * populated. `cert` is a wolfSSL `Cert*` cast to `void*` so that the - * caller can reach into fields like custom extension OIDs or SAN types - * that wolfCert doesn't expose directly. Returns WOLFCERT_OK on success - * (build continues), any error (build aborts). */ +/* Called by wolfcert_csr_build() once the standard fields are set, with the + * wolfSSL Cert* to add what wolfCert does not expose; any error aborts. + * A renewal then overwrites Cert's subject and altNames from current_cert. */ typedef int (*WolfCertCsrCustomizeCb)(void* wolfssl_cert, void* ctx); typedef struct {