From 1a4d473c81f780e7c233e3fb9e2a780a5602148f Mon Sep 17 00:00:00 2001 From: Aidan Keefe Date: Mon, 10 Aug 2026 16:23:01 -0600 Subject: [PATCH 1/3] CSR/CA/X509 refactor and extension support Rework the req, x509 and ca commands and their extension handling, and extend the x509 request test suite to cover them. - req: rework option parsing and the CSR/self-sign/CA-sign paths, add -serial and -CAkey - config: parse subjectAltName, authorityKeyIdentifier, subjectKeyIdentifier, keyUsage, extendedKeyUsage and basicConstraints, from both -addext and config sections wolfCLU_GetOpt is rewritten as a left-to-right argv walk rather than a scan of the option table, which the reworked x509 parsing depends on. That is cross-cutting, so the commands taking positional arguments (enc, dsaparam, dhparam, genkey, rand, sign/verify) are adapted to it and wolfCLU_checkForArg now takes a struct option. Commands that only take flags are untouched. -addext remains single-use, as on main. fixed issues from skoll review scope reduction --- src/clu_main.c | 5 +- src/x509/clu_ca_setup.c | 45 +- src/x509/clu_cert_setup.c | 86 +- src/x509/clu_config.c | 1098 +++++++++++++++++---- src/x509/clu_request_setup.c | 1794 +++++++++++++++++++++++++--------- src/x509/clu_x509_sign.c | 31 +- tests/x509/x509-ca-test.py | 22 + tests/x509/x509-req-test.py | 1656 ++++++++++++++++++++++++++++++- wolfclu/clu_header_main.h | 7 +- wolfclu/clu_optargs.h | 2 + wolfclu/x509/clu_cert.h | 5 + wolfclu/x509/clu_x509_sign.h | 2 + 12 files changed, 4073 insertions(+), 680 deletions(-) diff --git a/src/clu_main.c b/src/clu_main.c index 9847bf72..4dcce6ef 100644 --- a/src/clu_main.c +++ b/src/clu_main.c @@ -358,10 +358,7 @@ int main(int argc, char** argv) ret = WOLFCLU_FATAL_ERROR; } wolfSSL_Cleanup(); - - /* main function we want to return 0 on success so that the executable - * returns the expected 0 on success */ - return (ret == WOLFCLU_SUCCESS)? 0 : ret; + return ret == WOLFCLU_FATAL_ERROR ? 1 : 0; } #ifdef FREERTOS diff --git a/src/x509/clu_ca_setup.c b/src/x509/clu_ca_setup.c index 566cc3ae..f74f046d 100644 --- a/src/x509/clu_ca_setup.c +++ b/src/x509/clu_ca_setup.c @@ -37,9 +37,6 @@ static const struct option ca_options[] = { {"-in", required_argument, 0, WOLFCLU_INFILE }, {"-out", required_argument, 0, WOLFCLU_OUTFILE }, {"-keyfile", required_argument, 0, WOLFCLU_KEY }, - {"-subjkey", required_argument, 0, WOLFCLU_SUBJKEY }, - {"-altkey", required_argument, 0, WOLFCLU_ALTKEY }, - {"-altpub", required_argument, 0, WOLFCLU_ALTPUB }, {"-cert", required_argument, 0, WOLFCLU_CAFILE }, {"-extensions",required_argument, 0, WOLFCLU_EXTENSIONS}, {"-md", required_argument, 0, WOLFCLU_MD }, @@ -48,7 +45,12 @@ static const struct option ca_options[] = { {"-config", required_argument, 0, WOLFCLU_CONFIG }, {"-days", required_argument, 0, WOLFCLU_DAYS }, {"-selfsign", no_argument, 0, WOLFCLU_SELFSIGN }, +#if defined(WOLFSSL_DUAL_ALG_CERTS) && defined(HAVE_DILITHIUM) {"-altextend", no_argument, 0, WOLFCLU_ALTEXTEND }, + {"-subjkey", required_argument, 0, WOLFCLU_SUBJKEY }, + {"-altkey", required_argument, 0, WOLFCLU_ALTKEY }, + {"-altpub", required_argument, 0, WOLFCLU_ALTPUB }, +#endif /* WOLFSSL_DUAL_ALG_CERTS && HAVE_DILITHIUM */ {"-h", no_argument, 0, WOLFCLU_HELP }, {"-help", no_argument, 0, WOLFCLU_HELP }, @@ -120,7 +122,6 @@ int wolfCLU_CASetup(int argc, char** argv) ret = WOLFCLU_FATAL_ERROR; } break; - case WOLFCLU_SELFSIGN: selfSigned = 1; break; @@ -155,16 +156,16 @@ int wolfCLU_CASetup(int argc, char** argv) case WOLFCLU_ALTPUB: altKeyPub = wolfSSL_BIO_new_file(optarg, "rb"); if (altKeyPub == NULL) { - wolfCLU_LogError("Unable to open \ - alternate public key file %s", optarg); + wolfCLU_LogError("Unable to open alternate public key " + "file %s", optarg); ret = WOLFCLU_FATAL_ERROR; } break; -#endif /* WOLFSSL_DUAL_ALG_CERTS && HAVE_DILITHIUM */ case WOLFCLU_ALTEXTEND: altSign = 1; break; +#endif /* WOLFSSL_DUAL_ALG_CERTS && HAVE_DILITHIUM */ case WOLFCLU_CAFILE: ca = wolfSSL_X509_load_certificate_file(optarg, @@ -205,7 +206,18 @@ int wolfCLU_CASetup(int argc, char** argv) break; case WOLFCLU_DAYS: - days = XATOI(optarg); + { + long d = 0; + + if (optarg == NULL || wolfCLU_parseDecimalBounded(optarg, 1, + WOLFCLU_MAX_VALIDITY, &d) != WOLFCLU_SUCCESS) { + wolfCLU_LogError("-days expects a positive integer, got %s", + optarg != NULL ? optarg : "(nothing)"); + ret = WOLFCLU_FATAL_ERROR; + break; + } + days = (int)d; + } break; case WOLFCLU_EXTENSIONS: @@ -234,7 +246,7 @@ int wolfCLU_CASetup(int argc, char** argv) } } - if (reqIn == NULL && !altSign) { + if (ret == WOLFCLU_SUCCESS && reqIn == NULL && !altSign) { wolfCLU_LogError("Expecting CSR input"); ret = WOLFCLU_FATAL_ERROR; } @@ -242,10 +254,10 @@ int wolfCLU_CASetup(int argc, char** argv) if (ret == WOLFCLU_SUCCESS && config != NULL) { signer = wolfCLU_readSignConfig(config, (char*)"ca"); } - else { + else if (ret == WOLFCLU_SUCCESS) { signer = wolfCLU_CertSignNew(); } - if (signer == NULL) { + if (ret == WOLFCLU_SUCCESS && signer == NULL) { wolfCLU_LogError("Unable to create a signer struct"); ret = WOLFCLU_FATAL_ERROR; } @@ -310,6 +322,16 @@ int wolfCLU_CASetup(int argc, char** argv) wolfCLU_GetTypeFromPKEY(pkey)); } } + else { + if (ca != NULL) { + wolfSSL_X509_free(ca); + ca = NULL; + } + if (pkey != NULL) { + wolfSSL_EVP_PKEY_free(pkey); + pkey = NULL; + } + } /* default to version 3 which supports extensions */ if (ret == WOLFCLU_SUCCESS && @@ -357,3 +379,4 @@ int wolfCLU_CASetup(int argc, char** argv) return WOLFCLU_FATAL_ERROR; #endif } + diff --git a/src/x509/clu_cert_setup.c b/src/x509/clu_cert_setup.c index a7cda803..ee568f76 100644 --- a/src/x509/clu_cert_setup.c +++ b/src/x509/clu_cert_setup.c @@ -25,6 +25,7 @@ #include #include #include +#include /* for wolfCLU_CertSetDate() */ #define PEM_BEGIN_CERT "-----BEGIN CERTIFICATE-----" #define BEGIN_CERT_REQ "-----BEGIN CERTIFICATE REQUEST-----" @@ -38,6 +39,9 @@ void wolfCLU_certHelp(void) WOLFCLU_LOG(WOLFCLU_L0, "-out output file to write to"); WOLFCLU_LOG(WOLFCLU_L0, "-req input file is a CSR file"); WOLFCLU_LOG(WOLFCLU_L0, "-signkey a key for signing"); + WOLFCLU_LOG(WOLFCLU_L0, + "-days number of days the re-signed cert is valid for " + "(requires -req)"); WOLFCLU_LOG(WOLFCLU_L0, "-* supported digests for signing"); WOLFCLU_LOG(WOLFCLU_L0, "-extfile config file"); WOLFCLU_LOG(WOLFCLU_L0, "-extensions section of the config file to use"); @@ -88,6 +92,7 @@ static const struct option cert_options[] = { { "-signkey", required_argument, 0, WOLFCLU_SIGNKEY }, { "-extfile", required_argument, 0, WOLFCLU_EXTFILE }, { "-extensions", required_argument, 0, WOLFCLU_EXTENSIONS }, + { "-days", required_argument, 0, WOLFCLU_DAYS }, { "-req", no_argument, 0, WOLFCLU_REQ }, { "-noout", no_argument, 0, WOLFCLU_NOOUT }, @@ -123,6 +128,7 @@ int wolfCLU_certSetup(int argc, char **argv) int reqFlag = 0; /* set to read csr file */ int silentFlag = 0; /* set to disable echo to command line */ int modulus = 0; /* set to view modulus of cert */ + int days = 0; /* how long the cert is valid for */ char *inFile = NULL; /* pointer to the inFile name */ char *outFile = NULL; /* pointer to the outFile name */ @@ -298,6 +304,22 @@ int wolfCLU_certSetup(int argc, char **argv) printSubjHash = 1; break; + case WOLFCLU_DAYS: + { + long d = 0; + + if (optarg == NULL || wolfCLU_parseDecimalBounded(optarg, 1, + WOLFCLU_MAX_VALIDITY, &d) != WOLFCLU_SUCCESS) { + wolfCLU_LogError("-days expects a positive integer, got %s", + optarg != NULL ? optarg : "(nothing)"); + ret = WOLFCLU_FATAL_ERROR; + break; + } + days = (int)d; + break; + } + + case ARG_FOUND_TWICE: wolfCLU_LogError("Found duplicate argument"); ret = WOLFCLU_FATAL_ERROR; @@ -437,6 +459,16 @@ int wolfCLU_certSetup(int argc, char **argv) keyIn = NULL; } + /* Both -extfile and -days mutate the certificate, and a mutated + * certificate is only written out when -req makes it re-signed. Checked + * once for both so the two options do not disagree: without this, + * -extfile exited 0 having quietly dropped every extension asked for. */ + if (ret == WOLFCLU_SUCCESS && !reqFlag && (extFile != NULL || days > 0)) { + wolfCLU_LogError("Altering a Cert requires a resign and -req was " + "not set"); + ret = WOLFCLU_FATAL_ERROR; + } + if (ret == WOLFCLU_SUCCESS && extFile != NULL) { WOLFSSL_CONF *conf = NULL; long line = 0; @@ -450,11 +482,17 @@ int wolfCLU_certSetup(int argc, char **argv) ret = WOLFCLU_FATAL_ERROR; } else { - ret = wolfCLU_setExtensions(x509, conf, ext); + /* this command re-signs a certificate with its own key (-signkey), + * so there is no separate issuing certificate to name */ + ret = wolfCLU_setExtensions(x509, conf, ext, NULL); } wolfSSL_NCONF_free(conf); } + if (ret == WOLFCLU_SUCCESS && days > 0) { + ret = wolfCLU_CertSetDate(x509, days); + } + /*default to version 3 which supports extensions */ if (ret == WOLFCLU_SUCCESS && wolfSSL_X509_set_version(x509, WOLFSSL_X509_V3) != WOLFSSL_SUCCESS && @@ -769,16 +807,46 @@ int wolfCLU_certSetup(int argc, char **argv) /* write out certificate */ if (ret == WOLFCLU_SUCCESS && !nooutFlag) { - byte *derBuf = inBuf; byte *pt; /* use pt with i2d to handle potential pointer increment */ + /* DER input is already the encoding to write out, PEM input is + * converted below */ + byte *derBuf = inBuf; int derBufSz = inBufSz; + byte derBufAllocated = 0; /* if inform is PEM we convert to DER for excluding input that is not * part of the certificate */ if (inForm == PEM_FORM) { if (reqFlag) { - pt = derBuf; - derBufSz = wolfSSL_i2d_X509(x509, &pt); + /* the re-encoded cert is written back over the input buffer, + * so check it fits before writing rather than after */ + int needed = wolfSSL_i2d_X509(x509, NULL); + + if (needed <= 0) { + wolfCLU_LogError("Unable to get re-encoded certificate " + "size"); + ret = WOLFCLU_FATAL_ERROR; + } + else if (needed > inBufSz) { + derBuf = (byte *)XMALLOC(needed, HEAP_HINT, + DYNAMIC_TYPE_TMP_BUFFER); + if (derBuf == NULL) { + wolfCLU_LogError("Could not allocate space for " + "reencoded certificate"); + ret = WOLFCLU_FATAL_ERROR; + } + else { + derBufAllocated = 1; + } + } + if (ret == WOLFCLU_SUCCESS) { + pt = derBuf; + derBufSz = wolfSSL_i2d_X509(x509, &pt); + if (derBufSz <= 0) { + wolfCLU_LogError("Unable to re-encode the certificate"); + ret = WOLFCLU_FATAL_ERROR; + } + } } else { derBuf = derObj->buffer; @@ -786,14 +854,15 @@ int wolfCLU_certSetup(int argc, char **argv) } } - /* PEM/DER -> DER */ - if (outForm == DER_FORM) { + /* PEM/DER -> DER. Guarded on 'ret' so a failed re-encode above leaves + * nothing to write. */ + if (ret == WOLFCLU_SUCCESS && outForm == DER_FORM) { if (wolfSSL_BIO_write(out, derBuf, derBufSz) <= 0) { ret = WOLFCLU_FATAL_ERROR; } } /* PEM/DER -> PEM */ - else if (outForm == PEM_FORM) { + else if (ret == WOLFCLU_SUCCESS && outForm == PEM_FORM) { tmpOutBufSz = wc_DerToPem(derBuf, derBufSz, NULL, 0, CERT_TYPE); if (tmpOutBufSz <= 0) { wolfCLU_LogError("wc_DerToPem to get necessary length failed"); @@ -820,6 +889,9 @@ int wolfCLU_certSetup(int argc, char **argv) } } } + if (derBufAllocated) { + XFREE(derBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + } } if (inBufRaw != NULL) { diff --git a/src/x509/clu_config.c b/src/x509/clu_config.c index 780e56db..300ae340 100644 --- a/src/x509/clu_config.c +++ b/src/x509/clu_config.c @@ -53,6 +53,17 @@ static int wolfCLU_setAttributes(WOLFSSL_X509* x509, WOLFSSL_CONF* conf, #ifdef WOLFSSL_CERT_EXT +/* defined further down, forward declared for the parsers below. Both the + * definition and every call site live inside this guard, so the declaration + * has to as well or a !WOLFSSL_CERT_EXT build carries a static function that + * is declared and never defined. */ +static char* wolfCLU_trimToken(char* word); + +#ifdef WOLFSSL_ALT_NAMES +/* defined further down, forward declared for wolfCLU_parseExtension */ +static int wolfCLU_setInlineSubjectAltNames(WOLFSSL_X509* x509, char* val); +#endif + WOLFSSL_ASN1_OBJECT* wolfCLU_extenstionGetObjectNID(WOLFSSL_X509_EXTENSION *ext, int nid, int crit) { WOLFSSL_ASN1_OBJECT *obj; if (ext == NULL) @@ -78,124 +89,622 @@ WOLFSSL_ASN1_OBJECT* wolfCLU_extenstionGetObjectNID(WOLFSSL_X509_EXTENSION *ext, static WOLFSSL_X509_EXTENSION* wolfCLU_parseBasicConstraint(char* in, int crit) { - int idx = 0; /* offset into string */ char* word, *end, *str = in; - char* deli = (char*)":"; WOLFSSL_X509_EXTENSION *ext; WOLFSSL_ASN1_OBJECT *obj; + /* an empty value would otherwise tokenize to nothing and hand back a + * CA:FALSE extension the user never asked for */ + int sawValue = 0; if (str == NULL) { return NULL; } - /* if critical key word was found, then advance string pointer past - * 'critical,' */ - if (crit) { - int inSz = (int)XSTRLEN(in); + ext = wolfSSL_X509_EXTENSION_new(); + if (ext == NULL) { + return NULL; + } - for (idx = 0; idx < inSz; idx++) { - if (str[idx] == ',') break; + /* on failure this takes 'ext' down with it, nothing to free here */ + obj = wolfCLU_extenstionGetObjectNID(ext, NID_basic_constraints, crit); + if (obj == NULL) { + return NULL; + } + + + /* Split the value on ',' first and only then split each token on ':'. + * Tokenizing the whole string on ':' while the keyword branches consumed + * their value with ',' let the two delimiters cross: for + * "CA:TRUE,critical,pathlen:0" the pass after CA scanned past the comma + * and yielded "critical,pathlen" as one token, which matched nothing and + * rejected a valid value. Splitting on ',' first also makes "critical" + * position independent, since it is then always a token of its own. */ + for (word = XSTRTOK(str, ",", &end); word != NULL; + word = XSTRTOK(NULL, ",", &end)) { + /* hold on to the keyword: 'val' is the part after the colon, and + * testing that against the next keyword would let "CA:pathlen" style + * nonsense through */ + char* tok = wolfCLU_trimToken(word); + char* val = XSTRSTR(tok, ":"); + + if (val != NULL) { + *val = '\0'; + val = wolfCLU_trimToken(val + 1); + tok = wolfCLU_trimToken(tok); } - if (idx + 1 >= inSz) { - WOLFCLU_LOG(WOLFCLU_E0, "bad basic constraint string in conf file"); + if (XSTRCMP(tok, "CA") == 0) { + int z, valSz; + + if (val == NULL) { + wolfCLU_LogError("basicConstraints CA is missing a value, " + "expected \"CA:TRUE\" or \"CA:FALSE\""); + wolfSSL_X509_EXTENSION_free(ext); + return NULL; + } + + valSz = (int)XSTRLEN(val); + for (z = 0; z < valSz; z++) + val[z] = XTOUPPER(val[z]); + + if (XSTRCMP(val, "TRUE") == 0) { + obj->ca = 1; + sawValue = 1; + continue; + } + /* CA:FALSE is the default and the usual spelling for a leaf + * certificate's conf file, not a bad token */ + if (XSTRCMP(val, "FALSE") == 0) { + obj->ca = 0; + sawValue = 1; + continue; + } + + wolfCLU_LogError("Unable to parse basic constraint CA value " + "%s, expected \"TRUE\" or \"FALSE\"", + valSz ? val : "\"\""); + wolfSSL_X509_EXTENSION_free(ext); return NULL; } - /* advance past any white spaces */ - for (idx = idx + 1; idx < inSz; idx++) { - if (str[idx] != ' ') break; + if (XSTRCMP(tok, "pathlen") == 0) { + long pathLen = 0; + + /* 0 is a valid path length: the CA may issue end entity + * certificates but no further CAs */ + if (val == NULL || + wolfCLU_parseDecimalBounded(val, 0, 127, &pathLen) != + WOLFCLU_SUCCESS) { + wolfCLU_LogError("Unable to parse basic constraint " + "pathlen value %s, it must be a number in the " + "range [0, 127]", + (val != NULL && XSTRLEN(val)) ? val : "\"\""); + wolfSSL_X509_EXTENSION_free(ext); + return NULL; + } + + /* the [0, 127] bound above is WOLFSSL_MAX_PATH_LEN, which + * CopyX509ToCert() rejects anything larger against */ + + if (obj->pathlen != NULL) + wolfSSL_ASN1_INTEGER_free(obj->pathlen); + obj->pathlen = wolfSSL_ASN1_INTEGER_new(); + if (obj->pathlen == NULL) { + wolfSSL_X509_EXTENSION_free(ext); + return NULL; + } + if (wolfSSL_ASN1_INTEGER_set(obj->pathlen, pathLen) + != WOLFSSL_SUCCESS) { + wolfCLU_LogError("Unable to set the basic constraint pathlen"); + wolfSSL_X509_EXTENSION_free(ext); + return NULL; + } + + /* NOTE: Not undoing the set above: wolfSSL_X509_add_ext() reads the + * pathlen *value* out of ->length, which otherwise holds the DER + * encoding length. This leaves the ASN1_INTEGER inconsistent, so + * it must not be dup'd, printed or re-encoded past this point -- + * ->data holds only 20 bytes. @TODO drop once wolfSSL has a real + * path length accessor. */ + obj->pathlen->length = pathLen; + sawValue = 1; + continue; } + + /* the caller flagged 'critical' by searching the whole value, so + * every occurrence of it is just a token to step over here */ + if (XSTRCMP(tok, "critical") == 0) { + continue; + } + + wolfCLU_LogError("Unknown token \"%s\" while parsing " + "basicConstraints", tok); + wolfSSL_X509_EXTENSION_free(ext); + return NULL; } - ext = wolfSSL_X509_EXTENSION_new(); - obj = wolfCLU_extenstionGetObjectNID(ext, NID_basic_constraints, crit); - if (obj == NULL) { + /* an empty value tokenizes to nothing, so without this it would add a + * CA:FALSE extension that was never asked for */ + if (!sawValue) { + wolfCLU_LogError("no basicConstraints value found, expected " + "\"CA:TRUE\" or \"CA:FALSE\""); + wolfSSL_X509_EXTENSION_free(ext); return NULL; } + return ext; +} + +/* Trim spaces and tabs from both ends of 'word', in place. Returns the new + * start. Config values are commonly written "a, b" or even "a , b", so a + * token has to survive whitespace on either side. */ +static char* wolfCLU_trimToken(char* word) +{ + int sz; + + while (*word == ' ' || *word == '\t' || *word == '\n' || *word == '\r') { + word++; + } + sz = (int)XSTRLEN(word); + while (sz > 0 && (word[sz-1] == ' ' || word[sz-1] == '\t' || + word[sz-1] == '\n' || word[sz-1] == '\r')) { + word[--sz] = '\0'; + } + + return word; +} + +/* Is "critical" one of the comma separated tokens of 'str'? + * + * Matched per token rather than with a plain substring search, which reported + * a critical extension for any value that merely contained the word -- i.e. + * "subjectAltName=DNS:critical.example.com". 'str' is not modified, this runs + * before the parsers tokenize it in place. + * returns 1 when the key word is present, 0 otherwise */ +static int wolfCLU_hasCriticalToken(const char* str) +{ + const char* tok = str; + + while (tok != NULL) { + const char* end = XSTRSTR(tok, ","); + int sz; + + while (*tok == ' ' || *tok == '\t') { + tok++; + } + + sz = (end != NULL) ? (int)(end - tok) : (int)XSTRLEN(tok); + while (sz > 0 && (tok[sz - 1] == ' ' || tok[sz - 1] == '\t')) { + sz--; + } + + if (sz == 8 && XSTRNCMP(tok, "critical", 8) == 0) { + return 1; + } + + tok = (end != NULL) ? end + 1 : NULL; + } + + return 0; +} + +/* Does 'word' name the key word 'kw'? A ':' qualifier is allowed after it, so + * that OpenSSL's "keyid:always" / "issuer:optional" spellings match, while a + * typo such as "keyidalways" does not. + * returns 1 on a match, 0 otherwise */ +static int wolfCLU_tokenIs(const char* word, const char* kw) +{ + int kwSz = (int)XSTRLEN(kw); + + if (XSTRNCMP(word, kw, kwSz) != 0) { + return 0; + } + + return word[kwSz] == '\0' || word[kwSz] == ':'; +} + +/* Get the wolfCrypt key out of the certificate's public key and translate the + * key type into the *_TYPE value the wc_Set*KeyIdFromPublicKey_ex() helpers + * expect. On success the caller owns '*pkey' and must wolfSSL_EVP_PKEY_free() + * it; '*key' points into it and must not outlive it. + * return WOLFCLU_SUCCESS on success */ +static int wolfCLU_getPubKeyForId(WOLFSSL_X509* x509, WOLFSSL_EVP_PKEY** pkey, + void** key, int* keyType) +{ + int type; + + *pkey = NULL; + *key = NULL; + + type = wolfSSL_X509_get_pubkey_type(x509); + + *pkey = wolfSSL_X509_get_pubkey(x509); + if (*pkey == NULL) { + wolfCLU_LogError("no public key set to hash for key id"); + return WOLFCLU_FATAL_ERROR; + } + + switch (type) { + case RSAk: + if ((*pkey)->rsa != NULL) { + *key = (*pkey)->rsa->internal; + } + *keyType = RSA_TYPE; + break; + + case ECDSAk: + if ((*pkey)->ecc != NULL) { + *key = (*pkey)->ecc->internal; + } + *keyType = ECC_TYPE; + break; + + default: + wolfCLU_LogError("key type not yet supported"); + wolfSSL_EVP_PKEY_free(*pkey); + *pkey = NULL; + return WOLFCLU_FATAL_ERROR; + } + + if (*key == NULL) { + wolfCLU_LogError("Could not get public key"); + wolfSSL_EVP_PKEY_free(*pkey); + *pkey = NULL; + return WOLFCLU_FATAL_ERROR; + } + + return WOLFCLU_SUCCESS; +} + +/* Only consulted when no issuing certificate is available, since this compares + * names and so cannot tell a self signed certificate from an RFC 5280 4.2.1.1 + * key rollover one. An issuer with no entries is the self signed case too. + * returns 1 when self issued, 0 otherwise */ +static int wolfCLU_isSelfIssued(WOLFSSL_X509* x509) +{ + WOLFSSL_X509_NAME* issuer = wolfSSL_X509_get_issuer_name(x509); + + if (issuer == NULL || wolfSSL_X509_NAME_entry_count(issuer) == 0) { + return 1; + } + return wolfSSL_X509_NAME_cmp(wolfSSL_X509_get_subject_name(x509), issuer) + == 0; +} - for (word = XSTRTOK(str + idx, deli, &end); word != NULL; +/* Create an authority key identifier extension from the config values + * "keyid[:always]" (or "hash"). With 'issuer' the key id names that + * certificate's key and wolfSSL applies it to 'x509' directly; without one it + * is derived by hashing the key in 'x509', which is only the authority's key + * for a self signed cert. The "issuer" DN/serial form is skipped, not an + * error, since the keyid alone is still a valid AKID. + * + * On success '*out' holds the new extension, or NULL when it was applied + * directly or every key word present was a skipped one. + * return WOLFCLU_SUCCESS on success */ +static int wolfCLU_parseAuthorityKeyId(char* str, int crit, + WOLFSSL_X509* x509, WOLFSSL_X509* issuer, + WOLFSSL_X509_EXTENSION** out) +{ + WOLFSSL_X509_EXTENSION* ext = NULL; + WOLFSSL_EVP_PKEY *pkey = NULL; + char* word, *end; + char* deli = (char*)","; + int ret = WOLFCLU_SUCCESS; + /* A value naming only the skipped "issuer" is a success with no + * extension; one naming nothing usable at all is an error. */ + int sawSkipped = 0; + int sawKeyId = 0; + + if (x509 == NULL || str == NULL || out == NULL) + return BAD_FUNC_ARG; + + *out = NULL; + + /* RFC 5280 4.2.1.1 says the AKID MUST be non-critical, so the key word is + * accepted and reported rather than honoured. */ + if (crit) { + WOLFCLU_LOG(WOLFCLU_L0, "Ignoring \"critical\" on " + "authorityKeyIdentifier, RFC 5280 requires it be " + "non-critical"); + } + + for (word = XSTRTOK(str, deli, &end); + word != NULL && ret == WOLFCLU_SUCCESS; word = XSTRTOK(NULL, deli, &end)) { - if (word != NULL && XSTRCMP(word, "CA") == 0) { - word = XSTRTOK(NULL, deli, &end); - if (word != NULL) { - int z, wordSz; - - wordSz = (int)XSTRLEN(word); - for (z = 0; z < wordSz; z++) - word[z] = toupper(word[z]); - if (XSTRCMP(word, "TRUE") == 0) { - obj->ca = 1; + word = wolfCLU_trimToken(word); + + /* the critical key word was already handled by the caller */ + if (XSTRCMP(word, "critical") == 0) { + continue; + } + + /* "keyid" may carry a ":always" or ":optional" qualifier, both are + * treated the same here since the key id can always be derived */ + if (wolfCLU_tokenIs(word, "keyid") || wolfCLU_tokenIs(word, "hash")) { + WOLFSSL_ASN1_STRING *data; + void *key = NULL; + int keyType; + + sawKeyId = 1; + + /* Take the key id from the issuing certificate rather than + * hashing the key being certified; wolfSSL sets it on 'x509' + * itself, so there is no extension to hand back. The API hashes + * with SHA-1, so it is guarded the same way clu_request_setup.c + * guards its own call. */ + if (issuer != NULL) { +#ifndef NO_SHA + if (wolfSSL_X509_set_authority_key_id_ex(x509, issuer) + != WOLFSSL_SUCCESS) { + wolfCLU_LogError("error setting the authority key id from " + "the issuing certificate"); + ret = WOLFCLU_FATAL_ERROR; + } +#else + wolfCLU_LogError("cannot derive an authority key id from the " + "issuing certificate, wolfSSL was built with NO_SHA"); + ret = NOT_COMPILED_IN; +#endif + continue; + } + + /* a value may name the key id more than once, i.e. "keyid,hash", + * only the first one builds the extension */ + if (ext != NULL) { + continue; + } + + if (wolfCLU_getPubKeyForId(x509, &pkey, &key, &keyType) + != WOLFCLU_SUCCESS) { + ret = WOLFCLU_FATAL_ERROR; + break; + } + + /* Cert is several kilobytes, so it is scoped to the one branch + * that needs it rather than sitting on the frame throughout. */ + { + Cert cert; /* temporary to use existing auth key id api */ + + XMEMSET(&cert, 0, sizeof(Cert)); + if (wc_SetAuthKeyIdFromPublicKey_ex(&cert, keyType, key) < 0) { + wolfCLU_LogError("error hashing public key"); + ret = WOLFCLU_FATAL_ERROR; + } + else { + data = wolfSSL_ASN1_STRING_new(); + if (data == NULL) { + ret = MEMORY_E; + } + else { + if (wolfSSL_ASN1_STRING_set(data, cert.akid, cert.akidSz) + != WOLFSSL_SUCCESS) { + wolfCLU_LogError("error setting the akid"); + ret = WOLFCLU_FATAL_ERROR; + } + else { + /* RFC 5280 4.2.1.1 requires a non-critical AKID, so + * the extension is built that way on both paths even + * when the config asked for critical. */ + ext = wolfSSL_X509_EXTENSION_new(); + if (ext != NULL && + wolfCLU_extenstionGetObjectNID(ext, + NID_authority_key_identifier, 0) + == NULL) { + /* extension was free'd on failure */ + ext = NULL; + } + if (ext == NULL) { + ret = WOLFCLU_FATAL_ERROR; + } + else if (wolfSSL_X509_EXTENSION_set_data(ext, data) + != WOLFSSL_SUCCESS) { + wolfCLU_LogError("error setting the akid data"); + ret = WOLFCLU_FATAL_ERROR; + } + } + wolfSSL_ASN1_STRING_free(data); } } + } + wolfSSL_EVP_PKEY_free(pkey); + pkey = NULL; + } + else if (wolfCLU_tokenIs(word, "issuer")) { + /* the issuer name / serial form can not be created without the + * issuing certificate, the keyid alone is still a valid AKID */ + sawSkipped = 1; + WOLFCLU_LOG(WOLFCLU_L0, "Skipping authority key identifier " + "\"issuer\", only \"keyid\" is supported"); + } + else { + wolfCLU_LogError("unsupported authority key identifier \"%s\"", + word); + ret = WOLFCLU_FATAL_ERROR; + } + } + + if (ret != WOLFCLU_SUCCESS) { + if (ext != NULL) { + wolfSSL_X509_EXTENSION_free(ext); + } + return ret; + } + + /* The value was empty or held only "critical". Reported rather than + * dropped, matching every sibling parser in this file. */ + if (ext == NULL && !sawKeyId && !sawSkipped) { + wolfCLU_LogError("no authority key identifier value found, " + "expected \"keyid\" or \"issuer\""); + return WOLFCLU_FATAL_ERROR; + } + + *out = ext; + + return WOLFCLU_SUCCESS; +} + +/* The extended key usages wolfSSL can express, by both the OpenSSL key word + * and the dotted OID that conf files commonly use for the same purpose. */ +typedef struct WOLFCLU_EKU_MAP { + const char* name; + const char* oid; + byte flag; +} WOLFCLU_EKU_MAP; + +static const WOLFCLU_EKU_MAP wolfCLU_ekuMap[] = { + {"anyExtendedKeyUsage", "2.5.29.37.0", EXTKEYUSE_ANY}, + {"any", NULL, EXTKEYUSE_ANY}, + {"serverAuth", "1.3.6.1.5.5.7.3.1", EXTKEYUSE_SERVER_AUTH}, + {"clientAuth", "1.3.6.1.5.5.7.3.2", EXTKEYUSE_CLIENT_AUTH}, + {"codeSigning", "1.3.6.1.5.5.7.3.3", EXTKEYUSE_CODESIGN}, + {"emailProtection", "1.3.6.1.5.5.7.3.4", EXTKEYUSE_EMAILPROT}, + {"timeStamping", "1.3.6.1.5.5.7.3.8", EXTKEYUSE_TIMESTAMP}, + {"OCSPSigning", "1.3.6.1.5.5.7.3.9", EXTKEYUSE_OCSP_SIGN} +}; + +/* Create an extended key usage extension from a comma separated list of the + * key words wolfSSL supports, i.e. "critical,serverAuth,clientAuth". The + * dotted OID spelling of each of those purposes is accepted too, since conf + * files written for OpenSSL commonly use it. + * + * returns the new extension on success, NULL on failure */ +static WOLFSSL_X509_EXTENSION* wolfCLU_parseExtKeyUsage(char* str, int crit) +{ + WOLFSSL_ASN1_STRING *data; + WOLFSSL_X509_EXTENSION *ext = NULL; + char* word, *end; + char* deli = (char*)","; + byte extKeyUseFlag = 0; + size_t i; + + if (str == NULL) + return NULL; + + for (word = XSTRTOK(str, deli, &end); word != NULL; + word = XSTRTOK(NULL, deli, &end)) { + int found = 0; + + word = wolfCLU_trimToken(word); + + /* the critical key word was already handled by the caller */ + if (XSTRCMP(word, "critical") == 0) { + continue; } - if (word != NULL && XSTRCMP(word, "pathlen") == 0) { - word = XSTRTOK(NULL, deli, &end); - if (word != NULL) { - if (obj->pathlen != NULL) - wolfSSL_ASN1_INTEGER_free(obj->pathlen); - obj->pathlen = wolfSSL_ASN1_INTEGER_new(); - wolfSSL_ASN1_INTEGER_set(obj->pathlen, XATOI(word)); + for (i = 0; i < sizeof(wolfCLU_ekuMap) / sizeof(wolfCLU_ekuMap[0]); + i++) { + if (XSTRCMP(word, wolfCLU_ekuMap[i].name) == 0 || + (wolfCLU_ekuMap[i].oid != NULL && + XSTRCMP(word, wolfCLU_ekuMap[i].oid) == 0)) { + extKeyUseFlag |= wolfCLU_ekuMap[i].flag; + found = 1; + break; } } + + if (!found) { + wolfCLU_LogError("unsupported extended key usage \"%s\"", word); + wolfCLU_LogError("supported: any, serverAuth, clientAuth, " + "codeSigning, emailProtection, timeStamping, OCSPSigning"); + return NULL; + } } + if (extKeyUseFlag == 0) { + wolfCLU_LogError("no extended key usage values found"); + return NULL; + } + + ext = wolfSSL_X509_EXTENSION_new(); + if (ext == NULL) { + return NULL; + } + + if (wolfCLU_extenstionGetObjectNID(ext, NID_ext_key_usage, crit) == NULL) { + /* extension was free'd on failure */ + wolfCLU_LogError("Could not add ExtKeyUsage extension"); + return NULL; + } + + data = wolfSSL_ASN1_STRING_new(); + if (data == NULL) { + wolfSSL_X509_EXTENSION_free(ext); + return NULL; + } + + /* a single byte of flags is what wolfSSL_X509_add_ext() expects */ + if (wolfSSL_ASN1_STRING_set(data, &extKeyUseFlag, (int)sizeof(byte)) + != WOLFSSL_SUCCESS || + wolfSSL_X509_EXTENSION_set_data(ext, data) != WOLFSSL_SUCCESS) { + wolfCLU_LogError("error setting the extended key use"); + wolfSSL_X509_EXTENSION_free(ext); + ext = NULL; + } + wolfSSL_ASN1_STRING_free(data); + return ext; } - +/* Create a subject key identifier extension from the config value "hash", + * derived by hashing the public key held in 'x509'. + * + * returns the new extension on success, NULL on failure */ static WOLFSSL_X509_EXTENSION* wolfCLU_parseSubjectKeyID(char* str, int crit, WOLFSSL_X509* x509) { - Cert cert; /* temporary to use existing subject key id api */ WOLFSSL_X509_EXTENSION *ext = NULL; WOLFSSL_EVP_PKEY *pkey = NULL; char* word, *end; char* deli = (char*)","; + /* separates "the config named no key id" from "deriving one failed", which + * both left ext NULL and reported the former */ + int sawHash = 0; if (x509 == NULL || str == NULL) return NULL; for (word = XSTRTOK(str, deli, &end); word != NULL; word = XSTRTOK(NULL, deli, &end)) { + word = wolfCLU_trimToken(word); - if (XSTRNCMP(word, "hash", XSTRLEN(word)) == 0) { + /* the critical key word was already handled by the caller */ + if (XSTRCMP(word, "critical") == 0) { + continue; + } + + if (XSTRCMP(word, "hash") == 0) { WOLFSSL_ASN1_STRING *data; int keyType; void *key = NULL; + /* Cert is several kilobytes, so it is scoped to this branch */ + Cert cert; /* temporary to use existing subject key id api */ - XMEMSET(&cert, 0, sizeof(Cert)); - keyType = wolfSSL_X509_get_pubkey_type(x509); + sawHash = 1; - pkey = wolfSSL_X509_get_pubkey(x509); - if (pkey == NULL) { - wolfCLU_LogError("no public key set to hash for subject key id"); - return NULL; + /* only the first "hash" builds the extension */ + if (ext != NULL) { + continue; } - switch (keyType) { - case RSAk: - key = pkey->rsa->internal; - keyType = RSA_TYPE; - break; - - case ECDSAk: - key = pkey->ecc->internal; - keyType = ECC_TYPE; - break; - - default: - wolfCLU_LogError("key type not yet supported"); + if (wolfCLU_getPubKeyForId(x509, &pkey, &key, &keyType) + != WOLFCLU_SUCCESS) { + return NULL; } + XMEMSET(&cert, 0, sizeof(Cert)); if (wc_SetSubjectKeyIdFromPublicKey_ex(&cert, keyType, key) < 0) { wolfCLU_LogError("error hashing public key"); + /* this function owns pkey from here on, and returning skips + * the free below */ + wolfSSL_EVP_PKEY_free(pkey); + return NULL; } else { data = wolfSSL_ASN1_STRING_new(); - if (data != NULL) { + if (data == NULL) { + wolfCLU_LogError("out of memory building the skid"); + } + else { if (wolfSSL_ASN1_STRING_set(data, cert.skid, cert.skidSz) != WOLFSSL_SUCCESS) { wolfCLU_LogError("error setting the skid"); @@ -203,20 +712,44 @@ static WOLFSSL_X509_EXTENSION* wolfCLU_parseSubjectKeyID(char* str, int crit, else { ext = wolfSSL_X509V3_EXT_i2d(NID_subject_key_identifier, crit, data); + if (ext == NULL) { + wolfCLU_LogError("error encoding the skid " + "extension"); + } } wolfSSL_ASN1_STRING_free(data); } } - wolfSSL_EVP_PKEY_free(pkey); + wolfSSL_EVP_PKEY_free(pkey); + pkey = NULL; } + else { + wolfCLU_LogError("unsupported subject key identifier \"%s\"", + word); + if (ext != NULL) { + wolfSSL_X509_EXTENSION_free(ext); + } + return NULL; + } + } + + /* only report a missing value when the config really named none; a failure + * while deriving the key id has already logged its own cause */ + if (ext == NULL && !sawHash) { + wolfCLU_LogError("no subject key identifier value found, " + "expected \"hash\""); } return ext; } - -static WOLFSSL_X509_EXTENSION* wolfCLU_parseKeyUsage(char* str, int crit, - WOLFSSL_X509* x509) +/* Create a key usage extension from a comma separated list of the key words + * wolfSSL supports, i.e. "critical,digitalSignature,keyEncipherment". + * An unrecognized key word is an error rather than a silently dropped bit, + * matching wolfCLU_parseExtKeyUsage(). + * + * returns the new extension on success, NULL on failure */ +static WOLFSSL_X509_EXTENSION* wolfCLU_parseKeyUsage(char* str, int crit) { WOLFSSL_ASN1_STRING *data; WOLFSSL_X509_EXTENSION *ext = NULL; @@ -224,55 +757,55 @@ static WOLFSSL_X509_EXTENSION* wolfCLU_parseKeyUsage(char* str, int crit, char* deli = (char*)","; word16 keyUseFlag = 0; - if (x509 == NULL || str == NULL) + if (str == NULL) return NULL; for (word = XSTRTOK(str, deli, &end); word != NULL; word = XSTRTOK(NULL, deli, &end)) { + word = wolfCLU_trimToken(word); - /* remove empty spaces at beginning of word */ - int mxSz = (int)XSTRLEN(word); - while (word[0] == ' ' && mxSz > 0) { - word++; - mxSz--; + /* the critical key word was already handled by the caller */ + if (XSTRCMP(word, "critical") == 0) { + continue; } - - if (XSTRCMP(word, "digitalSignature") == 0) { + else if (XSTRCMP(word, "digitalSignature") == 0) { keyUseFlag |= KEYUSE_DIGITAL_SIG; } - - if (XSTRCMP(word, "nonRepudiation") == 0 || + else if (XSTRCMP(word, "nonRepudiation") == 0 || XSTRCMP(word, "contentCommitment") == 0) { keyUseFlag |= KEYUSE_CONTENT_COMMIT; } - - if (XSTRCMP(word, "keyEncipherment") == 0) { + else if (XSTRCMP(word, "keyEncipherment") == 0) { keyUseFlag |= KEYUSE_KEY_ENCIPHER; } - - if (XSTRCMP(word, "dataEncipherment") == 0) { + else if (XSTRCMP(word, "dataEncipherment") == 0) { keyUseFlag |= KEYUSE_DATA_ENCIPHER; } - - if (XSTRCMP(word, "keyAgreement") == 0) { + else if (XSTRCMP(word, "keyAgreement") == 0) { keyUseFlag |= KEYUSE_KEY_AGREE; } - - if (XSTRCMP(word, "keyCertSign") == 0) { + else if (XSTRCMP(word, "keyCertSign") == 0) { keyUseFlag |= KEYUSE_KEY_CERT_SIGN; } - - if (XSTRCMP(word, "cRLSign") == 0) { + else if (XSTRCMP(word, "cRLSign") == 0) { keyUseFlag |= KEYUSE_CRL_SIGN; } - - if (XSTRCMP(word, "encipherOnly") == 0) { + else if (XSTRCMP(word, "encipherOnly") == 0) { keyUseFlag |= KEYUSE_ENCIPHER_ONLY; } - - if (XSTRCMP(word, "decipherOnly") == 0) { + else if (XSTRCMP(word, "decipherOnly") == 0) { keyUseFlag |= KEYUSE_DECIPHER_ONLY; } + else { + wolfCLU_LogError("unsupported key usage \"%s\"", XSTRLEN(word) ? + word : ""); + return NULL; + } + } + + if (keyUseFlag == 0) { + wolfCLU_LogError("no key usage values found"); + return NULL; } data = wolfSSL_ASN1_STRING_new(); @@ -289,47 +822,182 @@ static WOLFSSL_X509_EXTENSION* wolfCLU_parseKeyUsage(char* str, int crit, return ext; } +/* Apply an inline "[critical,]TYPE:value[,TYPE:value...]" subject alt name + * list. wolfSSL keeps alt names on the WOLFSSL_X509 struct rather than as a + * generic extension, so nothing is handed back to the caller. 'str' is + * tokenized in place, callers pass a writable copy. + * return WOLFCLU_SUCCESS on success */ +static int wolfCLU_parseSubjectAltNames(WOLFSSL_X509* x509, char* str, int crit) +{ +#ifndef WOLFSSL_ALT_NAMES + (void)x509; + (void)str; + (void)crit; -/* return WOLFCLU_SUCCESS on success */ + /* alt names were explicitly requested, so fail rather than silently + * emitting a cert without them */ + wolfCLU_LogError("wolfSSL not compiled with alt name support " + "(WOLFSSL_ALT_NAMES); cannot apply requested subjectAltName"); + return NOT_COMPILED_IN; +#else + /* wolfSSL has no way to mark alt names critical. Said out loud because + * RFC 5280 4.2.1.6 requires a critical SAN when the subject is empty, so + * dropping it silently can emit a non-conformant certificate. The token + * itself is skipped wherever it appears, by the tokenizer below. */ + if (crit) { + WOLFCLU_LOG(WOLFCLU_L0, "Warning: wolfSSL cannot mark subjectAltName " + "critical, emitting it as non-critical"); + } + + return wolfCLU_setInlineSubjectAltNames(x509, str); +#endif +} + +/* Name an extension nid for a diagnostic, writing into 'buf' when it has to + * build the text. For error messages only. + * + * The NID_* macros for certificate extensions do not expand to the small + * OpenSSL nids: wolfSSL defines them as its internal OID sums, so + * NID_issuer_alt_name is 0x7fed1daa rather than 86. Printing the raw value + * hands the operator a ten digit number that matches nothing they can look + * up, and it reads like memory corruption. Prefer the long name, fall back to + * the dotted OID, and only use the number when neither is available. */ +static const char* wolfCLU_extNidName(int nid, char* buf, int bufSz) +{ + const char* ln = wolfSSL_OBJ_nid2ln(nid); + WOLFSSL_ASN1_OBJECT* obj; + + if (ln != NULL) { + return ln; + } + + /* nid2ln has no entry for several of the extensions handled here, but the + * object table still knows the OID itself */ + obj = wolfSSL_OBJ_nid2obj(nid); + if (obj != NULL) { + int sz = wolfSSL_OBJ_obj2txt(buf, bufSz, obj, 1); + + wolfSSL_ASN1_OBJECT_free(obj); + if (sz > 0) { + return buf; + } + } + + XSNPRINTF(buf, bufSz, "%d", nid); + return buf; +} + +static int wolfCLU_extNotSupported(const char* name) +{ + wolfCLU_LogError("extension %s is not supported by wolfSSL when creating " + "a certificate", name); + return WOLFCLU_FATAL_ERROR; +} + +/* Apply the extension 'nid' with value 'str' to 'x509'. 'issuer' is the + * certificate that will sign it, or NULL when it signs itself or the signer is + * not known here; only the authority key identifier uses it. + * return WOLFCLU_SUCCESS on success */ static int wolfCLU_parseExtension(WOLFSSL_X509* x509, char* str, int nid, - int* idx) + WOLFSSL_X509* issuer) { + char nameBuf[80]; + WOLFSSL_X509_EXTENSION *ext = NULL; - int ret, crit = 0; + int ret = WOLFCLU_SUCCESS; + int crit = 0; - if (XSTRSTR(str, "critical") != NULL) { + if (x509 == NULL || str == NULL) { + return BAD_FUNC_ARG; + } + + if (wolfCLU_hasCriticalToken(str)) { crit = 1; } + switch (nid) { case NID_basic_constraints: ext = wolfCLU_parseBasicConstraint(str, crit); break; case NID_subject_key_identifier: + /* every failure path returns NULL, so it must fall through to the + * error below rather than dropping the skid at exit 0 */ ext = wolfCLU_parseSubjectKeyID(str, crit, x509); break; case NID_authority_key_identifier: - /* @TODO */ + /* With an issuing certificate the key id comes from it and is + * right however the names compare; without one it can only be + * derived from the subject's own key, which needs a self signed + * cert. (The -CA path in clu_request_setup.c never lands here.) */ + if (issuer == NULL && !wolfCLU_isSelfIssued(x509)) { + WOLFCLU_LOG(WOLFCLU_L0, "Skipping authority key identifier, " + "deriving it for a certificate that is not self " + "issued needs the issuing certificate"); + return WOLFCLU_SUCCESS; + } + ret = wolfCLU_parseAuthorityKeyId(str, crit, x509, issuer, &ext); + if (ret != WOLFCLU_SUCCESS) { + return ret; + } + if (ext == NULL) { + /* either wolfSSL applied the key id to 'x509' directly from + * the issuing certificate, or every key word present was one + * that is deliberately skipped, i.e. "issuer" on its own */ + return WOLFCLU_SUCCESS; + } break; case NID_key_usage: - ext = wolfCLU_parseKeyUsage(str, crit, x509); + ext = wolfCLU_parseKeyUsage(str, crit); + break; + case NID_ext_key_usage: + ext = wolfCLU_parseExtKeyUsage(str, crit); break; + /* alt names are stored on the x509 struct instead of being added as + * an extension, so this case is done once it returns */ + case NID_subject_alt_name: + return wolfCLU_parseSubjectAltNames(x509, str, crit); + + /* wolfSSL_X509_add_ext() has nowhere to keep these on the x509 struct, + * so report them rather than silently dropping what was asked for. + * Named literally with the spelling -addext and the conf files use: + * three of the five are absent from wolfSSL's object table, so neither + * nid2ln nor nid2obj can name them and the message would otherwise + * degrade to a bare OID sum. */ + case NID_issuer_alt_name: + return wolfCLU_extNotSupported("issuerAltName"); + case NID_name_constraints: + return wolfCLU_extNotSupported("nameConstraints"); + case NID_policy_constraints: + return wolfCLU_extNotSupported("policyConstraints"); + case NID_policy_mappings: + return wolfCLU_extNotSupported("policyMappings"); + case NID_inhibit_any_policy: + return wolfCLU_extNotSupported("inhibitAnyPolicy"); default: - WOLFCLU_LOG(WOLFCLU_L0, "unknown / supported nid %d value for extension", - nid); + wolfCLU_LogError("unknown / unsupported extension %s", + wolfCLU_extNidName(nid, nameBuf, (int)sizeof(nameBuf))); + return WOLFCLU_FATAL_ERROR; } - if (ext != NULL) { - ret = wolfSSL_X509_add_ext(x509, ext, -1); - if (ret != WOLFSSL_SUCCESS) { - wolfCLU_LogError("error %d adding extension", ret); - } - *idx = *idx + 1; - wolfSSL_X509_EXTENSION_free(ext); + /* note that 'str' has been tokenized in place by now, so it is not worth + * echoing back in the error */ + if (ext == NULL) { + wolfCLU_LogError("unable to create extension %s", + wolfCLU_extNidName(nid, nameBuf, (int)sizeof(nameBuf))); + return WOLFCLU_FATAL_ERROR; } - return WOLFCLU_SUCCESS; -} + /* wolfSSL only supports appending, loc must be negative */ + if (wolfSSL_X509_add_ext(x509, ext, -1) != WOLFSSL_SUCCESS) { + wolfCLU_LogError("error adding extension %s", + wolfCLU_extNidName(nid, nameBuf, (int)sizeof(nameBuf))); + ret = WOLFCLU_FATAL_ERROR; + } + wolfSSL_X509_EXTENSION_free(ext); + + return ret; +} /* add a single alt name of type 'name' ("IP", "DNS", "URI", "RID" or "email") * with value 'value' to x509, shared by the config and -addext paths. @@ -497,7 +1165,6 @@ static int wolfCLU_addAltName(WOLFSSL_X509* x509, const char* name, } #endif /* WOLFSSL_ALT_NAMES */ - /* return WOLFCLU_SUCCESS on success, searches for IP's and DNS's */ static int wolfCLU_setAltNames(WOLFSSL_X509* x509, WOLFSSL_CONF* conf, char* sect) @@ -511,9 +1178,16 @@ static int wolfCLU_setAltNames(WOLFSSL_X509* x509, WOLFSSL_CONF* conf, #ifndef WOLFSSL_ALT_NAMES (void)x509; + (void)conf; (void)altNames; (void)i; - WOLFCLU_LOG(WOLFCLU_L0, "Skipping alt names, recompile wolfSSL with WOLFSSL_ALT_NAMES..."); + + /* the config named an alt name section, so fail rather than silently + * emitting a cert without those names */ + wolfCLU_LogError("wolfSSL not compiled with alt name support " + "(WOLFSSL_ALT_NAMES); cannot apply requested subjectAltName " + "section \"%s\"", sect); + ret = NOT_COMPILED_IN; #else altNames = wolfSSL_NCONF_get_section(conf, sect); if (altNames != NULL) { @@ -537,19 +1211,22 @@ static int wolfCLU_setAltNames(WOLFSSL_X509* x509, WOLFSSL_CONF* conf, } } } + else { + wolfCLU_LogError("Section %s was not found", sect); + ret = WOLFCLU_FATAL_ERROR; + } #endif return ret; } - #ifdef WOLFSSL_ALT_NAMES /* Apply an inline subjectAltName list to x509, e.g. * "DNS:example.com,IP:10.0.0.1". Leading whitespace per entry is skipped. * Buffer is tokenized in place, so callers pass a writable string. Returns * WOLFCLU_SUCCESS, or WOLFCLU_FATAL_ERROR on a malformed entry so a bad SAN is * never silently ignored. */ -static int wolfCLU_setInlineAltNames(WOLFSSL_X509* x509, char* val) +static int wolfCLU_setInlineSubjectAltNames(WOLFSSL_X509* x509, char* val) { int ret = WOLFCLU_SUCCESS; char* token; @@ -574,6 +1251,14 @@ static int wolfCLU_setInlineAltNames(WOLFSSL_X509* x509, char* val) token[len - 1] == '\r' || token[len - 1] == '\n')) { token[--len] = '\0'; } + /* "critical" is a flag, not a name, and the caller has already acted + * on it. Skipped here so it is position independent, matching the + * other extension parsers. */ + if (XSTRCMP(token, "critical") == 0) { + token = XSTRTOK(NULL, ",", &ptr); + continue; + } + colon = XSTRSTR(token, ":"); if (colon == NULL) { wolfCLU_LogError("bad subjectAltName entry \"%s\", expected " @@ -612,94 +1297,135 @@ static int wolfCLU_setInlineAltNames(WOLFSSL_X509* x509, char* val) } #endif /* WOLFSSL_ALT_NAMES */ +/* Look 'key' up in the config section and hand its value to + * wolfCLU_parseExtension() as the extension 'nid'. The value is copied first: + * the extension parsers tokenize (and upper case) in place, and the string + * returned by wolfSSL_NCONF_get_string() belongs to the WOLFSSL_CONF. + * A key that is not present in the section is not an error. + * return WOLFCLU_SUCCESS on success */ +static int wolfCLU_setExtensionFromConf(WOLFSSL_X509* x509, WOLFSSL_CONF* conf, + char* sect, const char* key, int nid, WOLFSSL_X509* issuer) +{ + char* current; + char* dup; + int len; + int ret; + + current = wolfSSL_NCONF_get_string(conf, sect, key); + if (current == NULL) { + return WOLFCLU_SUCCESS; /* not set in this section */ + } + + len = (int)XSTRLEN(current); + dup = (char*)XMALLOC(len + 1, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (dup == NULL) { + wolfCLU_LogError("out of memory duplicating %s value", key); + return MEMORY_E; + } + XMEMCPY(dup, current, len + 1); + + ret = wolfCLU_parseExtension(x509, dup, nid, issuer); + XFREE(dup, NULL, DYNAMIC_TYPE_TMP_BUFFER); + + return ret; +} /* return WOLFCLU_SUCCESS on success */ -int wolfCLU_setExtensions(WOLFSSL_X509* x509, WOLFSSL_CONF* conf, char* sect) +int wolfCLU_setExtensions(WOLFSSL_X509* x509, WOLFSSL_CONF* conf, char* sect, + WOLFSSL_X509* issuer) { char *current; - int idx = 1; int ret = WOLFCLU_SUCCESS; if (sect == NULL) { return WOLFCLU_SUCCESS; /* none set */ } - current = wolfSSL_NCONF_get_string(conf, sect, "basicConstraints"); - if (current != NULL) { - wolfCLU_parseExtension(x509, current, NID_basic_constraints, &idx); + ret = wolfCLU_setExtensionFromConf(x509, conf, sect, + "basicConstraints", NID_basic_constraints, issuer); + + if (ret == WOLFCLU_SUCCESS) { + ret = wolfCLU_setExtensionFromConf(x509, conf, sect, + "subjectKeyIdentifier", NID_subject_key_identifier, issuer); } - current = wolfSSL_NCONF_get_string(conf, sect, "subjectKeyIdentifier"); - if (current != NULL) { - wolfCLU_parseExtension(x509, current, NID_subject_key_identifier, &idx); + if (ret == WOLFCLU_SUCCESS) { + ret = wolfCLU_setExtensionFromConf(x509, conf, sect, + "authorityKeyIdentifier", NID_authority_key_identifier, issuer); } - current = wolfSSL_NCONF_get_string(conf, sect, "authorityKeyIdentifier"); - if (current != NULL) { - wolfCLU_parseExtension(x509, current, NID_authority_key_identifier, - &idx); + if (ret == WOLFCLU_SUCCESS) { + ret = wolfCLU_setExtensionFromConf(x509, conf, sect, + "keyUsage", NID_key_usage, issuer); } - current = wolfSSL_NCONF_get_string(conf, sect, "keyUsage"); - if (current != NULL) { - wolfCLU_parseExtension(x509, current, NID_key_usage, &idx); + if (ret == WOLFCLU_SUCCESS) { + ret = wolfCLU_setExtensionFromConf(x509, conf, sect, + "extendedKeyUsage", NID_ext_key_usage, issuer); } - current = wolfSSL_NCONF_get_string(conf, sect, "subjectAltName"); - if (current != NULL) { - if (current[0] == '@') { + if (ret == WOLFCLU_SUCCESS) { + current = wolfSSL_NCONF_get_string(conf, sect, "subjectAltName"); + if (current != NULL && current[0] == '@') { + /* the "@section" form needs the conf handle to look the section + * up, which wolfCLU_parseExtension() does not have */ ret = wolfCLU_setAltNames(x509, conf, current + 1); } - else { - /* Accept inline form for config compatibility. */ -#ifndef WOLFSSL_ALT_NAMES - /* Intentional: mirror the pre-existing silent-skip behaviour of - * the @section form (wolfCLU_setAltNames is also a no-op when - * WOLFSSL_ALT_NAMES is not defined). We log the skip but do NOT - * promote ret to WOLFCLU_FATAL_ERROR so that a config containing - * a subjectAltName line is still usable in builds where alt-name - * support was compiled out. */ - WOLFCLU_LOG(WOLFCLU_L0, "Skipping alt names, recompile wolfSSL " - "with WOLFSSL_ALT_NAMES..."); -#else - { - int len = (int)XSTRLEN(current); - char* dup = (char*)XMALLOC(len + 1, NULL, - DYNAMIC_TYPE_TMP_BUFFER); - - if (dup == NULL) { - wolfCLU_LogError("out of memory duplicating " - "subjectAltName value"); - ret = WOLFCLU_FATAL_ERROR; - } - else { - XMEMCPY(dup, current, len + 1); - ret = wolfCLU_setInlineAltNames(x509, dup); - XFREE(dup, NULL, DYNAMIC_TYPE_TMP_BUFFER); - } - } -#endif + else if (current != NULL) { + ret = wolfCLU_setExtensionFromConf(x509, conf, sect, + "subjectAltName", NID_subject_alt_name, issuer); } } + return ret; } - -/* parse a command line "-addext name=value" and apply it to x509. Currently - * supports "subjectAltName=TYPE:val[,TYPE:val...]". +/* the extension names -addext accepts, and the nid each one routes to */ +typedef struct WOLFCLU_ADDEXT_MAP { + const char* name; + int nid; +} WOLFCLU_ADDEXT_MAP; + +static const WOLFCLU_ADDEXT_MAP wolfCLU_addExtMap[] = { + {"basicConstraints", NID_basic_constraints}, + {"subjectKeyIdentifier", NID_subject_key_identifier}, + {"authorityKeyIdentifier", NID_authority_key_identifier}, + {"subjectAltName", NID_subject_alt_name}, + {"issuerAltName", NID_issuer_alt_name}, + {"keyUsage", NID_key_usage}, + {"extendedKeyUsage", NID_ext_key_usage}, + {"nameConstraints", NID_name_constraints}, + {"policyConstraints", NID_policy_constraints}, + {"policyMappings", NID_policy_mappings}, + {"inhibitAnyPolicy", NID_inhibit_any_policy} +}; + +/* parse a command line "-addext name=value" and apply it to x509, i.e. + * "subjectAltName=DNS:example.com,IP:10.0.0.1". * return WOLFCLU_SUCCESS on success */ int wolfCLU_parseAddExt(WOLFSSL_X509* x509, char* addExt) { - char* dup; - char* val; - int len; - int ret = WOLFCLU_SUCCESS; + int ret; + int len; + int nameSz; + int nid = 0; + size_t i; + char* dup; + char* name; + char* value; if (x509 == NULL || addExt == NULL) { return BAD_FUNC_ARG; } - /* work on a writable copy so the original argv string is untouched */ + value = XSTRSTR(addExt, "="); /* find value */ + if (value == NULL) { + wolfCLU_LogError("-addext expects \"name=value\", got %s", addExt); + return WOLFCLU_FATAL_ERROR; + } + + /* work on a writable copy, the extension parsers tokenize in place and + * the original argv string should be left alone */ len = (int)XSTRLEN(addExt); dup = (char*)XMALLOC(len + 1, NULL, DYNAMIC_TYPE_TMP_BUFFER); if (dup == NULL) { @@ -707,38 +1433,43 @@ int wolfCLU_parseAddExt(WOLFSSL_X509* x509, char* addExt) } XMEMCPY(dup, addExt, len + 1); - /* split "name=value" on the first '=' */ - val = XSTRSTR(dup, "="); - if (val == NULL) { - wolfCLU_LogError("-addext expects \"name=value\", got %s", addExt); - XFREE(dup, NULL, DYNAMIC_TYPE_TMP_BUFFER); - return WOLFCLU_FATAL_ERROR; + /* Split the copy at the '=' and trim the name, so that the spacing + * OpenSSL's conf parser tolerates -- "keyUsage = digitalSignature" -- is + * accepted here too. Comparing whole names also keeps a longer name that + * starts with a shorter one, i.e. "keyUsagePeriod", from matching. */ + nameSz = (int)(value - addExt); + dup[nameSz] = '\0'; + name = wolfCLU_trimToken(dup); + value = dup + nameSz + 1; + + for (i = 0; i < sizeof(wolfCLU_addExtMap) / sizeof(wolfCLU_addExtMap[0]); + i++) { + if (XSTRCMP(name, wolfCLU_addExtMap[i].name) == 0) { + nid = wolfCLU_addExtMap[i].nid; + break; + } } - *val = '\0'; - val++; - if (XSTRCMP(dup, "subjectAltName") == 0) { -#ifndef WOLFSSL_ALT_NAMES - (void)val; - WOLFCLU_LOG(WOLFCLU_L0, "Skipping alt names, recompile wolfSSL with WOLFSSL_ALT_NAMES..."); -#else - /* value is a comma separated list of TYPE:value pairs */ - ret = wolfCLU_setInlineAltNames(x509, val); -#endif + if (nid == 0) { + wolfCLU_LogError("unsupported -addext extension \"%s\"", name); + ret = WOLFCLU_FATAL_ERROR; } else { - wolfCLU_LogError("unsupported -addext extension \"%s\"", dup); - ret = WOLFCLU_FATAL_ERROR; + ret = wolfCLU_parseExtension(x509, value, nid, NULL); } XFREE(dup, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return ret; } + #else -int wolfCLU_setExtensions(WOLFSSL_X509* x509, WOLFSSL_CONF* conf, char* sect) +int wolfCLU_setExtensions(WOLFSSL_X509* x509, WOLFSSL_CONF* conf, char* sect, + WOLFSSL_X509* issuer) { (void)x509; (void)conf; + (void)issuer; /* No extension section requested, so not having WOLFSSL_CERT_EXT * can be ignored. (Coupled with `ret = ` in wolfCLU_readConfig) */ @@ -764,7 +1495,6 @@ int wolfCLU_parseAddExt(WOLFSSL_X509* x509, char* addExt) } #endif /* WOLFSSL_CERT_EXT */ - #define MAX_DIST_NAME 80 #define DEFAULT_STR_SZ 9 #define MIN_MAX_STR_SZ 5 @@ -983,7 +1713,6 @@ static int wolfCLU_setDisNames(WOLFSSL_X509* x509, WOLFSSL_CONF* conf, return ret; } - /* Make a new WOLFSSL_X509 based off of the config file read */ int wolfCLU_readConfig(WOLFSSL_X509* x509, char* config, char* sect, char* ext) { @@ -1015,7 +1744,7 @@ int wolfCLU_readConfig(WOLFSSL_X509* x509, char* config, char* sect, char* ext) * is NULL, but fails loudly if an extension section IS requested and * WOLFSSL_CERT_EXT is disabled. These two behaviors are coupled. */ ret = wolfCLU_setExtensions(x509, conf, - wolfSSL_NCONF_get_string(conf, sect, "x509_extensions")); + wolfSSL_NCONF_get_string(conf, sect, "x509_extensions"), NULL); } else { /* extension was specifically set, error out if not found */ @@ -1025,7 +1754,7 @@ int wolfCLU_readConfig(WOLFSSL_X509* x509, char* config, char* sect, char* ext) ret = WOLFCLU_FATAL_ERROR; } else { - ret = wolfCLU_setExtensions(x509, conf, ext); + ret = wolfCLU_setExtensions(x509, conf, ext, NULL); } } @@ -1040,7 +1769,6 @@ int wolfCLU_readConfig(WOLFSSL_X509* x509, char* config, char* sect, char* ext) return ret; } - int wolfCLU_GetTypeFromPKEY(WOLFSSL_EVP_PKEY* key) { int keyType = 0; diff --git a/src/x509/clu_request_setup.c b/src/x509/clu_request_setup.c index 6cea40c0..8246a622 100644 --- a/src/x509/clu_request_setup.c +++ b/src/x509/clu_request_setup.c @@ -18,7 +18,6 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA */ - #include #include #include @@ -27,30 +26,77 @@ #include #include #include -#include #include #include #include +#include /* for time() / time_t, not pulled in by a wolfSSL header */ +#include /* for LONG_MAX */ + +/* Accepted "-newkey rsa:" sizes: only the three standard steps, since + * anything below 2048 is no longer an acceptable strength. A list rather than + * a range so a typo such as "rsa:20488" never reaches keygen. */ +#define WOLFCLU_RSA_BITS_2048 2048 +#define WOLFCLU_RSA_BITS_3072 3072 +#define WOLFCLU_RSA_BITS_4096 4096 #if defined(WOLFSSL_CERT_REQ) && !defined(WOLFCLU_NO_FILESYSTEM) + +#ifndef _WIN32 + #include /* for the -keyout / -out same file check */ +#endif + +/* Do 'a' and 'b' name the same file? A plain string compare misses the same + * file spelled two ways ("out.pem" and "./out.pem"), so where stat() is + * available the device and inode decide it. + * returns 1 when both name one file, 0 otherwise */ +static int wolfCLU_isSameFile(const char* a, const char* b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + if (XSTRCMP(a, b) == 0) { + return 1; + } + +#ifndef _WIN32 + { + struct stat sa, sb; + + /* only meaningful once both exist; the caller has already created the + * -keyout file by this point */ + if (stat(a, &sa) == 0 && stat(b, &sb) == 0) { + return sa.st_dev == sb.st_dev && sa.st_ino == sb.st_ino; + } + } +#endif + + return 0; +} + + static void wolfCLU_certgenHelp(void) { WOLFCLU_LOG(WOLFCLU_L0, "Arguments:"); WOLFCLU_LOG(WOLFCLU_L0, "\t-in input file to read from"); WOLFCLU_LOG(WOLFCLU_L0, "\t-out file to write to (default stdout)"); - WOLFCLU_LOG(WOLFCLU_L0, "\t-key public key to put into certificate request"); WOLFCLU_LOG(WOLFCLU_L0, "\t-inform der or pem format for '-in'"); WOLFCLU_LOG(WOLFCLU_L0, "\t-outform der or pem format for '-out'"); WOLFCLU_LOG(WOLFCLU_L0, "\t-config file to parse for certificate configuration"); - WOLFCLU_LOG(WOLFCLU_L0, "\t-days number of days should be valid for"); + WOLFCLU_LOG(WOLFCLU_L0, "\t-days number of days should be valid for (default: 20 days)"); WOLFCLU_LOG(WOLFCLU_L0, "\t-x509 generate self signed certificate"); + WOLFCLU_LOG(WOLFCLU_L0, "\t-CA Parent ca of new cert"); + WOLFCLU_LOG(WOLFCLU_L0, "\t-CAkey Ca key for signing new cert"); + WOLFCLU_LOG(WOLFCLU_L0, "\t-set_serial Input a serial number for the cert to use if not set one will be generated at random"); WOLFCLU_LOG(WOLFCLU_L0, "\t-extensions overwrite the section to get extensions from"); WOLFCLU_LOG(WOLFCLU_L0, "\t-addext add an extension, ie \"subjectAltName=IP:192.168.1.2,DNS:example.com\""); WOLFCLU_LOG(WOLFCLU_L0, "\t-nodes no DES encryption on private key output"); - WOLFCLU_LOG(WOLFCLU_L0, "\t-newkey generate the private key to use with req"); + WOLFCLU_LOG(WOLFCLU_L0, "\t-newkey generate the private key to use with " + "req, as : i.e. rsa:2048 (rsa 2048/3072/4096 only)"); WOLFCLU_LOG(WOLFCLU_L0, "\t-inkey private key to use with req"); WOLFCLU_LOG(WOLFCLU_L0, "\t-keyout file to output key to"); WOLFCLU_LOG(WOLFCLU_L0, "\t-subj use a specified subject name, ie O=wolfSSL/C=US/ST=WA/L=Seattle/CN=wolfSSL/OU=org-unit"); - WOLFCLU_LOG(WOLFCLU_L0, "\t-verify check the signature on the req"); + WOLFCLU_LOG(WOLFCLU_L0, "\t-verify verify the signature of a req"); + WOLFCLU_LOG(WOLFCLU_L0, "\t-key public key to put into certificate request"); WOLFCLU_LOG(WOLFCLU_L0, "\t-text output human readable text of req"); WOLFCLU_LOG(WOLFCLU_L0, "\t-noout do not print out the generated results"); } @@ -58,25 +104,32 @@ static void wolfCLU_certgenHelp(void) { static const struct option req_options[] = { {"-sha", no_argument, 0, WOLFCLU_CERT_SHA }, + {"-sha1", no_argument, 0, WOLFCLU_CERT_SHA }, {"-sha224", no_argument, 0, WOLFCLU_CERT_SHA224}, {"-sha256", no_argument, 0, WOLFCLU_CERT_SHA256}, {"-sha384", no_argument, 0, WOLFCLU_CERT_SHA384}, {"-sha512", no_argument, 0, WOLFCLU_CERT_SHA512}, + + /* key gen algorithms */ {"-rsa", no_argument, 0, WOLFCLU_RSA }, + {"-ecc", no_argument, 0, WOLFCLU_ECC }, {"-ed25519", no_argument, 0, WOLFCLU_ED25519 }, {"-in", required_argument, 0, WOLFCLU_INFILE }, {"-out", required_argument, 0, WOLFCLU_OUTFILE }, {"-key", required_argument, 0, WOLFCLU_KEY }, - {"-new", no_argument, 0, WOLFCLU_NEW }, + {"-CA", required_argument, 0, WOLFCLU_CA }, + {"-CAkey", required_argument, 0, WOLFCLU_CAKEY }, {"-newkey", required_argument, 0, WOLFCLU_NEWKEY }, {"-inkey", required_argument, 0, WOLFCLU_INKEY }, + {"-set_serial",required_argument, 0, WOLFCLU_SERIAL}, {"-keyout", required_argument, 0, WOLFCLU_OUTKEY }, {"-inform", required_argument, 0, WOLFCLU_INFORM }, {"-outform", required_argument, 0, WOLFCLU_OUTFORM }, {"-config", required_argument, 0, WOLFCLU_CONFIG }, {"-days", required_argument, 0, WOLFCLU_DAYS }, {"-x509", no_argument, 0, WOLFCLU_X509 }, + {"-new", no_argument, 0, WOLFCLU_NEW }, {"-subj", required_argument, 0, WOLFCLU_SUBJECT }, {"-verify", no_argument, 0, WOLFCLU_VERIFY }, {"-text", no_argument, 0, WOLFCLU_TEXT_OUT }, @@ -274,10 +327,13 @@ static int _wolfSSL_X509_extensions_print(WOLFSSL_BIO* bio, WOLFSSL_X509* x509, break; #endif default: - /* extension nid not yet supported */ + /* extension nid not yet supported. 'buf' holds the + * name obj2txt just produced; the nid itself is + * wolfSSL's internal OID sum, a ten digit number that + * means nothing to the reader */ XSNPRINTF(scratch, MAX_WIDTH, - "%*sNID %d print not yet supported\n", - indent + 8, "", nid); + "%*s%s print not yet supported\n", + indent + 8, "", buf); wolfSSL_BIO_write(bio, scratch, (int)XSTRLEN(scratch)); } } @@ -551,622 +607,1450 @@ static int wolfSSL_X509_REQ_print(WOLFSSL_BIO* bio, WOLFSSL_X509* x509, return WOLFSSL_SUCCESS; } #endif /* NO_WOLFSSL_REQ_PRINT */ -#endif -/* return WOLFCLU_SUCCESS on success */ -int wolfCLU_requestSetup(int argc, char** argv) +/* crash on null args because they are statically allocated by the calling + * func make it easy to debug. */ +static void mapOptionToMd(int option, const WOLFSSL_EVP_MD** md) { -#ifndef WOLFSSL_CERT_REQ - wolfCLU_LogError("wolfSSL not compiled with --enable-certreq"); - /* silence unused variable warnings */ - (void) argc; - (void) argv; - return NOT_COMPILED_IN; -#elif defined(WOLFCLU_NO_FILESYSTEM) - WOLFCLU_LOG(WOLFCLU_E0, "No Filesystem Support."); - /* silence unused variable warnings */ - (void) argc; - (void) argv; - return NOT_COMPILED_IN; -#else - WOLFSSL_BIO *bioOut = NULL; - WOLFSSL_BIO *keyIn = NULL; - WOLFSSL_BIO *reqIn = NULL; - WOLFSSL_X509 *x509 = NULL; - const WOLFSSL_EVP_MD *md = NULL; - WOLFSSL_EVP_PKEY *pkey = NULL; + switch (option) { + case WOLFCLU_CERT_SHA: + *md = wolfSSL_EVP_sha1(); + break; + case WOLFCLU_CERT_SHA224: + *md = wolfSSL_EVP_sha224(); + break; + case WOLFCLU_CERT_SHA384: + *md = wolfSSL_EVP_sha384(); + break; + case WOLFCLU_CERT_SHA512: + *md = wolfSSL_EVP_sha512(); + break; + case WOLFCLU_CERT_SHA256: + /* sha256 is the default and fallthrough is intentional */ + default: + *md = wolfSSL_EVP_sha256(); + break; + } +} - int ret = WOLFCLU_SUCCESS; - char* in = NULL; - char* out = NULL; - char* config = NULL; - char* subj = NULL; - char* ext = NULL; - char* addExt = NULL; - char* keyType = NULL; - char* keyInfo = NULL; - char* keyOut = NULL; +static int verifyX509(WOLFSSL_BIO* keyBio, WOLFSSL_X509* x509, int isCSR) +{ + int ret = WOLFCLU_SUCCESS; + WOLFSSL_EVP_PKEY* pkey = NULL; - int algCheck = 0; /* algorithm type */ - int oid = 0; - int outForm = PEM_FORM; /* default to PEM format */ - int inForm = PEM_FORM; - int option; - int longIndex = 1; - int days = 0; - int genX509 = 0; - int passout = 0; + /* A request is self-signed, so the key that verifies it is the public key + * it carries. Prefer that over -key: it is the correct key by definition, + * and it is a public key, which is what REQ_verify wants. -key is only a + * fallback for a request that carries no usable public key. */ + pkey = wolfSSL_X509_get_pubkey(x509); - char password[MAX_PASSWORD_SIZE]; - int passwordSz = MAX_PASSWORD_SIZE; + if (pkey == NULL && keyBio != NULL) { + /* the key may already have been read once to sign with, rewind so + * this read starts at the beginning of the file again */ + wolfSSL_BIO_reset(keyBio); - byte doVerify = 0; - byte doTextOut = 0; - byte reSign = 0; /* flag for if resigning req is needed */ - byte noOut = 0; - byte useDes = 1; -#ifdef NO_WOLFSSL_REQ_PRINT - byte isCSR = 1; -#endif - /* Multiple -addext is not yet supported. Detect it up front and fail - * instead of silently dropping the extension and exiting success. */ - { - int i, addExtCount = 0; - for (i = 1; i < argc; i++) { - if (argv[i] != NULL && XSTRCMP(argv[i], "-addext") == 0) { - addExtCount++; - } - } - if (addExtCount > 1) { - wolfCLU_LogError("only one -addext arg is currently supported"); - return USER_INPUT_ERROR; + pkey = wolfSSL_PEM_read_bio_PrivateKey(keyBio, NULL, NULL, NULL); + if (pkey == NULL) { + wolfCLU_LogError("Unable to read the key to verify with from the " + "file passed to -key"); + ret = WOLFCLU_FATAL_ERROR; } } + else if (pkey == NULL) { + wolfCLU_LogError("Unable to get public key to verify with from " + "req that was passed in"); + ret = WOLFCLU_FATAL_ERROR; + } - opterr = 0; /* do not display unrecognized options */ - optind = 0; /* start at indent 0 */ - while (ret == WOLFCLU_SUCCESS && (option = wolfCLU_GetOpt(argc, argv, "", - req_options, &longIndex )) != END_OF_ARGS) { - - switch (option) { - case WOLFCLU_EXTENSIONS: - ext = optarg; - break; + if (ret == WOLFCLU_SUCCESS && isCSR) { + if (wolfSSL_X509_REQ_verify(x509, pkey) != 1) { + wolfCLU_LogError("verify failed"); + ret = WOLFCLU_FATAL_ERROR; + } + else { + WOLFCLU_LOG(WOLFCLU_L0, "verify OK"); + } + } + else if (ret == WOLFCLU_SUCCESS) { + if (wolfSSL_X509_verify(x509, pkey) != 1) { + wolfCLU_LogError("verify failed"); + ret = WOLFCLU_FATAL_ERROR; + } + else { + WOLFCLU_LOG(WOLFCLU_L0, "verify OK"); + } + } - case WOLFCLU_ADDEXT: - addExt = optarg; - break; + /* prepare BIO for future use */ + if (keyBio != NULL) { + wolfSSL_BIO_reset(keyBio); + } + wolfSSL_EVP_PKEY_free(pkey); + return ret; +} - case WOLFCLU_NODES: - useDes = 0; - break; +/* 'passwordCap' is the buffer capacity the stdin prompt needs, not the current + * password length; conflating the two gave "-passout pass:" a capacity of 0. + * 'havePassword' keeps an explicitly empty -passout from becoming a prompt. */ +static int writeOutPkey(WOLFSSL_BIO* keyOutBio, WOLFSSL_EVP_PKEY* pkey, + int useDes, char* password, word32 passwordCap, int havePassword) +{ + int ret = WOLFCLU_SUCCESS; + WOLFSSL_BIO* localBio = NULL; + + /* only fall back to stdout when the caller had no -keyout; the caller + * retains ownership of any BIO it passed in */ + if (keyOutBio == NULL) { + localBio = wolfSSL_BIO_new(wolfSSL_BIO_s_file()); + keyOutBio = localBio; + if (keyOutBio != NULL) { + if (wolfSSL_BIO_set_fp(keyOutBio, stdout, BIO_NOCLOSE) + != WOLFSSL_SUCCESS) { + ret = WOLFCLU_FATAL_ERROR; + } + } + else { + wolfCLU_LogError("Could not open file object for stdout"); + ret = WOLFCLU_FATAL_ERROR; + } + } - case WOLFCLU_OUTKEY: - keyOut = optarg; - break; + if (ret == WOLFCLU_SUCCESS) { + if (useDes) { + if (!havePassword) { + word32 promptSz = passwordCap; - case WOLFCLU_NEWKEY: - if (optarg == NULL) { - wolfCLU_LogError("no key string"); + if (wolfCLU_GetStdinPassword((byte*)password, &promptSz) + != WOLFCLU_SUCCESS) { + wolfCLU_LogError("Unable to read a password from stdin"); ret = WOLFCLU_FATAL_ERROR; } + } - if (ret == WOLFCLU_SUCCESS) { - if (XSTRSTR(optarg, ":") == NULL) { - wolfCLU_LogError("key string does not have ':'"); - ret = WOLFCLU_FATAL_ERROR; - } - } + /* an empty password encrypts nothing, whether it came from the + * prompt or from "-passout pass:" */ + if (ret == WOLFCLU_SUCCESS && password[0] == '\0') { + wolfCLU_LogError("Please enter a password"); + ret = WOLFCLU_FATAL_ERROR; + } - if (ret == WOLFCLU_SUCCESS) { - int idx; - idx = (int)strcspn(optarg, ":"); - keyType = (char*)XMALLOC(idx + 1, HEAP_HINT, - DYNAMIC_TYPE_TMP_BUFFER); - if (keyType == NULL) { - ret = WOLFCLU_FATAL_ERROR; - } - else { - XMEMCPY(keyType, optarg, idx); - keyType[idx] = '\0'; - } + if (ret == WOLFCLU_SUCCESS) { + ret = wolfCLU_pKeyPEMtoPriKeyEnc(keyOutBio, pkey, DES3b, + (byte*)password, (int)XSTRLEN(password)); + } + } + else { + ret = wolfCLU_pKeyPEMtoPriKey(keyOutBio, pkey); + } + } - if (ret == WOLFCLU_SUCCESS) { - keyInfo = optarg + idx + 1; - } - } - break; + wolfSSL_BIO_free(localBio); + return ret; +} - case WOLFCLU_INFILE: - reqIn = wolfSSL_BIO_new_file(optarg, "rb"); - if (reqIn == NULL) { - wolfCLU_LogError("Unable to open input file %s", - optarg); - ret = WOLFCLU_FATAL_ERROR; - } - break; + /* return WOLFCLU_SUCCESS on success */ +/* Write the signed request or certificate out to 'outBio'. + * + * Kept out of makeReq()/selfSignCert()/caSignCert() so the caller can emit + * -text (and run -verify) after signing but before the encoded body, which is + * the order OpenSSL uses. + * return WOLFCLU_SUCCESS on success */ +static int writeOutX509(WOLFSSL_BIO* outBio, WOLFSSL_X509* x509, int outForm, + int isCSR) +{ + int ret; - case WOLFCLU_KEY: - in = optarg; - keyIn = wolfSSL_BIO_new_file(optarg, "rb"); - if (keyIn == NULL) { - wolfCLU_LogError("Unable to open public key file %s", - optarg); - ret = WOLFCLU_FATAL_ERROR; - } - break; + if (outBio == NULL || x509 == NULL) { + return WOLFCLU_FATAL_ERROR; + } - case WOLFCLU_OUTFILE: - out = optarg; - break; + if (isCSR) { + ret = (outForm == DER_FORM) ? wolfSSL_i2d_X509_REQ_bio(outBio, x509) + : wolfSSL_PEM_write_bio_X509_REQ(outBio, + x509); + } + else { + ret = (outForm == DER_FORM) ? wolfSSL_i2d_X509_bio(outBio, x509) + : wolfSSL_PEM_write_bio_X509(outBio, x509); + } - case WOLFCLU_INFORM: - inForm = wolfCLU_checkInform(optarg); - break; + if (ret != WOLFSSL_SUCCESS) { + wolfCLU_LogError("Error %d writing out %s", ret, + isCSR ? "cert req" : "x509 cert"); + return WOLFCLU_FATAL_ERROR; + } - case WOLFCLU_OUTFORM: - outForm = wolfCLU_checkOutform(optarg); - break; + return WOLFCLU_SUCCESS; +} - case WOLFCLU_SUBJECT: - subj = optarg; - break; - case WOLFCLU_HELP: - wolfCLU_certgenHelp(); - return WOLFCLU_SUCCESS; +static int makeReq(WOLFSSL_X509* x509, WOLFSSL_EVP_PKEY* pkey, + const WOLFSSL_EVP_MD* md, byte reSign) +{ + int ret = WOLFCLU_SUCCESS; - case WOLFCLU_RSA: - algCheck = 1; - break; + if (reSign && pkey == NULL) { + wolfCLU_LogError("The request has been altered and requires a resign. " + "But no key was passed to sign with"); + ret = WOLFCLU_FATAL_ERROR; + } - case WOLFCLU_ED25519: - algCheck = 2; - break; + if (ret == WOLFCLU_SUCCESS) { + if (wolfSSL_X509_REQ_set_version(x509, WOLFSSL_X509_V1) != + WOLFSSL_SUCCESS) { + wolfCLU_LogError("Error setting CSR version"); + ret = WOLFCLU_FATAL_ERROR; + } + } - case WOLFCLU_CONFIG: - config = optarg; - break; + if (ret == WOLFCLU_SUCCESS && pkey != NULL) { + if (wolfSSL_X509_REQ_sign(x509, pkey, md) != WOLFSSL_SUCCESS) { + wolfCLU_LogError("Unable to sign request"); + ret = WOLFCLU_FATAL_ERROR; + } + } - case WOLFCLU_DAYS: - days = XATOI(optarg); - break; + /* the caller writes the encoded form out, after -text / -verify */ - case WOLFCLU_CERT_SHA: - md = wolfSSL_EVP_sha1(); - oid = SHA_HASH; - break; + return ret; +} - case WOLFCLU_CERT_SHA224: - md = wolfSSL_EVP_sha224(); - oid = SHA_HASH224; - break; + /* return WOLFCLU_SUCCESS on success */ +static int selfSignCert(WOLFSSL_X509* x509, WOLFSSL_EVP_PKEY* pkey, + const WOLFSSL_EVP_MD* md, long days, long serial) +{ + int ret = WOLFCLU_SUCCESS; - case WOLFCLU_CERT_SHA256: - md = wolfSSL_EVP_sha256(); - oid = SHA_HASH256; - break; + if (pkey == NULL) { + wolfCLU_LogError("A key for signing is required to create a selfsigned " + "cert"); + return WOLFCLU_FATAL_ERROR; + } - case WOLFCLU_CERT_SHA384: - md = wolfSSL_EVP_sha384(); - oid = SHA_HASH384; - break; + /* Bump to v3 so extensions are honored: */ + if (ret == WOLFCLU_SUCCESS) { + if (wolfSSL_X509_set_version(x509, WOLFSSL_X509_V3) != + WOLFSSL_SUCCESS) { + wolfCLU_LogError("Error setting CSR version"); + ret = WOLFCLU_FATAL_ERROR; + } + } - case WOLFCLU_CERT_SHA512: - md = wolfSSL_EVP_sha512(); - oid = SHA_HASH512; - break; + /* Issuer == subject (self-signed) */ + if (ret == WOLFCLU_SUCCESS) { + if (wolfSSL_X509_set_issuer_name(x509, + wolfSSL_X509_get_subject_name(x509)) != WOLFSSL_SUCCESS) { + wolfCLU_LogError("Error setting issuer name"); + ret = WOLFCLU_FATAL_ERROR; + } + } - case WOLFCLU_X509: - genX509 = 1; - break; + /* Set validity window from days */ + if (ret == WOLFCLU_SUCCESS && days > 0) { + WOLFSSL_ASN1_TIME *notBefore, *notAfter; + time_t t; - case WOLFCLU_VERIFY: - doVerify = 1; - break; + if ((t = time(NULL)) == (time_t)-1) { + wolfCLU_LogError("Error fetching time"); + ret = WOLFCLU_FATAL_ERROR; + } + else { + notBefore = wolfSSL_ASN1_TIME_adj(NULL, t, 0, 0); + notAfter = wolfSSL_ASN1_TIME_adj(NULL, t, days, 0); + if (notBefore == NULL || notAfter == NULL) { + wolfCLU_LogError("Error creating not before/after dates"); + ret = WOLFCLU_FATAL_ERROR; + } + else { + wolfSSL_X509_set_notBefore(x509, notBefore); + wolfSSL_X509_set_notAfter(x509, notAfter); + } - case WOLFCLU_TEXT_OUT: - doTextOut = 1; - break; + wolfSSL_ASN1_TIME_free(notBefore); + wolfSSL_ASN1_TIME_free(notAfter); + } + } - case WOLFCLU_PASSWORD_OUT: - passout = 1; - ret = wolfCLU_GetPassword(password, &passwordSz, optarg); - break; + /* Set the serial number. */ + if (ret == WOLFCLU_SUCCESS && serial > 0) { + WOLFSSL_ASN1_INTEGER* asn1SerialNum = wolfSSL_ASN1_INTEGER_new(); + if (asn1SerialNum != NULL) { + /* wolfSSL statuses stay out of 'ret', which carries the WOLFCLU + * status; the two only happen to agree on success */ + if (wolfSSL_ASN1_INTEGER_set(asn1SerialNum, serial) + != WOLFSSL_SUCCESS || + wolfSSL_X509_set_serialNumber(x509, asn1SerialNum) + != WOLFSSL_SUCCESS) { + wolfCLU_LogError("Unable to set serial number"); + ret = WOLFCLU_FATAL_ERROR; + } + } + else { + wolfCLU_LogError("Unable to set serial number"); + ret = WOLFCLU_FATAL_ERROR; + } + wolfSSL_ASN1_INTEGER_free(asn1SerialNum); + } - case WOLFCLU_NOOUT: - noOut = 1; - break; +#if defined(WOLFSSL_CERT_EXT) && !defined(NO_SHA) - case WOLFCLU_NEW: - break; + /* Default Basic Constraints to CA:TRUE when not already set */ + if (ret == WOLFCLU_SUCCESS && + !wolfSSL_X509_ext_isSet_by_NID(x509, NID_basic_constraints)) { + WOLFSSL_X509_EXTENSION *newExt; + WOLFSSL_ASN1_OBJECT *obj; - case ARG_FOUND_TWICE: - if (keyType != NULL) { - XFREE(keyType, NULL, DYNAMIC_TYPE_TMP_BUFFER); - } - wolfSSL_BIO_free(reqIn); - wolfSSL_BIO_free(keyIn); - wolfSSL_BIO_free(bioOut); - wolfSSL_X509_free(x509); - wolfSSL_EVP_PKEY_free(pkey); - return WOLFCLU_FATAL_ERROR; + newExt = wolfSSL_X509_EXTENSION_new(); + obj = wolfCLU_extenstionGetObjectNID(newExt, NID_basic_constraints, 1); - case ':': - case '?': - wolfCLU_LogError("Unexpected argument"); - ret = WOLFCLU_FATAL_ERROR; - wolfCLU_certgenHelp(); - break; - - default: - wolfCLU_LogError("Unsupported argument"); + if (obj == NULL || newExt == NULL) { + ret = WOLFCLU_FATAL_ERROR; + } + else { + obj->ca = 1; /* CA:TRUE -- req -x509 makes a self-signed root */ + if (wolfSSL_X509_add_ext(x509, newExt, -1) != WOLFSSL_SUCCESS) { + WOLFCLU_LOG(WOLFCLU_E0, + "error adding Basic Constraints extension"); ret = WOLFCLU_FATAL_ERROR; - wolfCLU_certgenHelp(); + } + wolfSSL_X509_EXTENSION_free(newExt); } } - /* default to sha256 if not set */ - if (ret == WOLFCLU_SUCCESS && md == NULL) { - md = wolfSSL_EVP_sha256(); - oid = SHA_HASH256; +#else + if (ret == WOLFCLU_SUCCESS) { + WOLFCLU_LOG(WOLFCLU_L0, "Skipping basicConstaints " + "WOLFSSL_CERT_EXT or SHA-1 disabled"); } +#endif + /* NOTE: wolfSSL_X509_sign() returns the cert LENGTH on success, not + * WOLFSSL_SUCCESS, so it is checked for > 0 and kept in a local. */ if (ret == WOLFCLU_SUCCESS) { - if (reqIn == NULL) { - x509 = wolfSSL_X509_new(); - if (x509 == NULL) { - wolfCLU_LogError("Issue creating structure to use"); - ret = MEMORY_E; - } - } - else { - if (inForm == PEM_FORM) { - wolfSSL_PEM_read_bio_X509_REQ(reqIn, &x509, NULL, NULL); - } - else { - wolfSSL_d2i_X509_REQ_bio(reqIn, &x509); - } - if (x509 == NULL) { - wolfCLU_LogError("Issue creating structure to use"); - ret = WOLFCLU_FATAL_ERROR; - } + int signSz = wolfSSL_X509_sign(x509, pkey, md); + + if (signSz <= 0) { + wolfCLU_LogError("Error signing certificate"); + ret = WOLFCLU_FATAL_ERROR; } } - if (ret == WOLFCLU_SUCCESS && days > 0) { - WOLFSSL_ASN1_TIME *notBefore, *notAfter; - time_t t; + /* the caller writes the encoded form out, after -text / -verify */ - t = time(NULL); - notBefore = wolfSSL_ASN1_TIME_adj(NULL, t, 0, 0); - notAfter = wolfSSL_ASN1_TIME_adj(NULL, t, days, 0); - if (notBefore == NULL || notAfter == NULL) { - wolfCLU_LogError("Error creating not before/after dates"); - ret = WOLFCLU_FATAL_ERROR; - } - else { - wolfSSL_X509_set_notBefore(x509, notBefore); - wolfSSL_X509_set_notAfter(x509, notAfter); - } + return ret; +} - wolfSSL_ASN1_TIME_free(notBefore); - wolfSSL_ASN1_TIME_free(notAfter); + /* return WOLFCLU_SUCCESS on success */ +static int caSignCert(WOLFSSL_X509* x509, WOLFSSL_BIO* caBio, + WOLFSSL_BIO* caKeyBio, const WOLFSSL_EVP_MD* md, long days, + long serial, int doVerify) +{ + int ret = WOLFCLU_SUCCESS; + WOLFSSL_EVP_PKEY* caKey = NULL; + WOLFSSL_X509* caCert = NULL; - reSign = 1; /* re-sign after date change */ - } + /* Load the CA material */ + if (caBio == NULL || caKeyBio == NULL) + return WOLFCLU_FATAL_ERROR; - if (ret == WOLFCLU_SUCCESS && keyIn != NULL) { - pkey = wolfSSL_PEM_read_bio_PrivateKey(keyIn, NULL, NULL, NULL); - if (pkey == NULL) { - wolfCLU_LogError("Error reading key from file"); - ret = USER_INPUT_ERROR; - } + caCert = wolfSSL_PEM_read_bio_X509(caBio, NULL, NULL, NULL); + if (caCert == NULL) { + wolfCLU_LogError("Unable to read ca cert passed to -CA"); + ret = WOLFCLU_FATAL_ERROR; + } - if (ret == WOLFCLU_SUCCESS && - wolfSSL_X509_set_pubkey(x509, pkey) != WOLFSSL_SUCCESS) { - ret = WOLFCLU_FATAL_ERROR; + if (ret == WOLFCLU_SUCCESS) { + caKey = wolfSSL_PEM_read_bio_PrivateKey(caKeyBio, NULL, NULL, NULL); + if (caKey == NULL) { + wolfCLU_LogError("Unable to read ca key passed to -CAkey"); + ret = WOLFCLU_FATAL_ERROR; } } - /* generate key for -newkey */ - if (ret == WOLFCLU_SUCCESS && keyType != NULL && keyInfo != NULL && - pkey == NULL) { - WOLFSSL_EVP_PKEY_CTX* ctx = NULL; - - if (XSTRNCMP("ec", keyType, 2) == 0) { - wolfCLU_LogError("No supporting ecc gen with -newkey yet, " - "use ecparam command instead"); + /* Confirm the CA cert can issue. wolfSSL_X509_check_ca() answers 1 for + * CA:TRUE but also 4 for a leaf that merely carries a critical + * extendedKeyUsage, so only the CA bit may be accepted here. */ + if (ret == WOLFCLU_SUCCESS) { + if (wolfSSL_X509_check_ca(caCert) != 1) { + wolfCLU_LogError("The certificate passed to -CA is not a CA " + "(basicConstraints CA:TRUE) and cannot issue"); ret = WOLFCLU_FATAL_ERROR; } + } - if (XSTRNCMP("rsa", keyType, 3) == 0) { - ctx = wolfSSL_EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL); - ret = wolfSSL_EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, - (int)XATOI(keyInfo)); + /* A published keyUsage has to include keyCertSign (RFC 5280 4.2.1.3). + * wolfSSL_X509_get_key_usage() returns all bits set when the extension is + * absent, so an unrestricted CA needs no special case. */ + if (ret == WOLFCLU_SUCCESS) { + if ((wolfSSL_X509_get_key_usage(caCert) & KEYUSE_KEY_CERT_SIGN) == 0) { + wolfCLU_LogError("The certificate passed to -CA has a keyUsage " + "extension without keyCertSign and cannot issue"); + ret = WOLFCLU_FATAL_ERROR; } + } - if (ret == WOLFCLU_SUCCESS && ctx == NULL) { - wolfCLU_LogError("Unknown/unsupported algo name"); + /* -CAkey has to be the key -CA was issued under, otherwise the + * certificate would carry the CA's issuer name over a signature that + * does not chain to it. Caught here rather than after signing so the + * failure names the option the operator got wrong. */ + if (ret == WOLFCLU_SUCCESS) { + if (wolfSSL_X509_check_private_key(caCert, caKey) != WOLFSSL_SUCCESS) { + wolfCLU_LogError("The key passed to -CAkey does not match the " + "certificate passed to -CA"); ret = WOLFCLU_FATAL_ERROR; } + } - if (ret == WOLFCLU_SUCCESS) { - if (wolfSSL_EVP_PKEY_keygen(ctx, &pkey) != WOLFSSL_SUCCESS) { - wolfCLU_LogError("Error with keygen"); + /* Verify the incoming request before certifying it */ + if (ret == WOLFCLU_SUCCESS) { + WOLFSSL_EVP_PKEY* reqPub = wolfSSL_X509_get_pubkey(x509); + if (reqPub == NULL) { + wolfCLU_LogError("Req did not have a public key to verify it with"); + ret = WOLFCLU_FATAL_ERROR; + } + else { + if (wolfSSL_X509_REQ_verify(x509, reqPub) < 1) { + wolfCLU_LogError("Req Failed verification"); ret = WOLFCLU_FATAL_ERROR; } } - wolfSSL_EVP_PKEY_CTX_free(ctx); + wolfSSL_EVP_PKEY_free(reqPub); + } - if (ret == WOLFCLU_SUCCESS && - wolfSSL_X509_set_pubkey(x509, pkey) != WOLFSSL_SUCCESS) { + + /* Bump to v3 */ + if (ret == WOLFCLU_SUCCESS) { + if (wolfSSL_X509_set_version(x509, WOLFSSL_X509_V3) != + WOLFSSL_SUCCESS) { + wolfCLU_LogError("Error setting CSR version"); ret = WOLFCLU_FATAL_ERROR; } } - if (ret == WOLFCLU_SUCCESS && reqIn == NULL && pkey == NULL) { - wolfCLU_LogError("Please specify a -key option when " - "generating a certificate."); - wolfCLU_certgenHelp(); - ret = USER_INPUT_ERROR; + /* Issuer == the CA's subject */ + if (ret == WOLFCLU_SUCCESS) { + if (wolfSSL_X509_set_issuer_name(x509, + wolfSSL_X509_get_subject_name(caCert)) != WOLFSSL_SUCCESS) { + wolfCLU_LogError("Error setting issuer name"); + ret = WOLFCLU_FATAL_ERROR; + } } - if (ret == WOLFCLU_SUCCESS && config != NULL) { - ret = wolfCLU_readConfig(x509, config, (char*)"req", ext); - reSign = 1; /* re-sign after config changes */ - } + /* Set validity from days */ + if (ret == WOLFCLU_SUCCESS && days > 0) { + WOLFSSL_ASN1_TIME *notBefore, *notAfter; + time_t t; - if (ret == WOLFCLU_SUCCESS && subj != NULL) { - WOLFSSL_X509_NAME *name; - name = wolfCLU_ParseX509NameString(subj, (int)XSTRLEN(subj)); - if (name != NULL) { - wolfSSL_X509_REQ_set_subject_name(x509, name); - wolfSSL_X509_NAME_free(name); - reSign = 1; /* re-sign after subject change */ + if ((t = time(NULL)) == (time_t)-1) { + wolfCLU_LogError("Error fetching time"); + ret = WOLFCLU_FATAL_ERROR; } else { - wolfCLU_LogError("Failed to parse -subj string"); - wolfCLU_certgenHelp(); - ret = USER_INPUT_ERROR; - } - } + notBefore = wolfSSL_ASN1_TIME_adj(NULL, t, 0, 0); + notAfter = wolfSSL_ASN1_TIME_adj(NULL, t, days, 0); + if (notBefore == NULL || notAfter == NULL) { + wolfCLU_LogError("Error creating not before/after dates"); + ret = WOLFCLU_FATAL_ERROR; + } + else { + wolfSSL_X509_set_notBefore(x509, notBefore); + wolfSSL_X509_set_notAfter(x509, notAfter); + } - /* apply the -addext extension, if present */ - if (ret == WOLFCLU_SUCCESS && addExt != NULL) { - ret = wolfCLU_parseAddExt(x509, addExt); - reSign = 1; /* re-sign after extension change */ + wolfSSL_ASN1_TIME_free(notBefore); + wolfSSL_ASN1_TIME_free(notAfter); + } } - /* if no configure is passed in then get input from command line */ - if (ret == WOLFCLU_SUCCESS && subj == NULL && config == NULL && - reqIn == NULL) { - WOLFSSL_X509_NAME *name; - - name = wolfSSL_X509_NAME_new(); - if (name == NULL) { - ret = MEMORY_E; + /* Assign the CA-chosen serial */ + if (ret == WOLFCLU_SUCCESS && serial > 0) { + WOLFSSL_ASN1_INTEGER* asn1SerialNum = wolfSSL_ASN1_INTEGER_new(); + if (asn1SerialNum != NULL) { + /* wolfSSL statuses are kept out of 'ret', which carries the + * WOLFCLU status; the two only happen to agree on success */ + if (wolfSSL_ASN1_INTEGER_set(asn1SerialNum, serial) + != WOLFSSL_SUCCESS || + wolfSSL_X509_set_serialNumber(x509, asn1SerialNum) + != WOLFSSL_SUCCESS) { + wolfCLU_LogError("Unable to set serial number"); + ret = WOLFCLU_FATAL_ERROR; + } } else { - ret = wolfCLU_CreateX509Name(name); - if (ret == WOLFCLU_SUCCESS) { - wolfSSL_X509_REQ_set_subject_name(x509, name); - } - wolfSSL_X509_NAME_free(name); + wolfCLU_LogError("Unable to set serial number"); + ret = WOLFCLU_FATAL_ERROR; } + wolfSSL_ASN1_INTEGER_free(asn1SerialNum); } - /* default to CA:TRUE for req -x509 command (self signed certificates) when - * a basic constraint is not already set */ - if (genX509 && ret == WOLFCLU_SUCCESS && - !wolfSSL_X509_ext_isSet_by_NID(x509, NID_basic_constraints)) { - WOLFSSL_X509_EXTENSION *newExt; - WOLFSSL_ASN1_OBJECT *obj; - newExt = wolfSSL_X509_EXTENSION_new(); - obj = wolfCLU_extenstionGetObjectNID(newExt, NID_basic_constraints, 1); + /* Chaining extensions for a leaf cert: + * - Basic Constraints CA:FALSE, overriding whatever the req carried + * - Subject Key Id derived from this cert's own public key + * - Authority Key Id copied from the CA cert's Subject Key Id */ + if (ret == WOLFCLU_SUCCESS && + wolfSSL_X509_ext_isSet_by_NID(x509, NID_basic_constraints) && + wolfSSL_X509_check_ca(x509) == 1) { + WOLFCLU_LOG(WOLFCLU_L0, "Warning: request asked for Basic Constraints " + "CA:TRUE; issuing a leaf with CA:FALSE"); + } - if (obj == NULL || newExt == NULL) { +#if defined(WOLFSSL_CERT_EXT) && !defined(NO_SHA) + + if (ret == WOLFCLU_SUCCESS) { + WOLFSSL_X509_EXTENSION* ext = wolfSSL_X509_EXTENSION_new(); + WOLFSSL_ASN1_OBJECT* obj = wolfCLU_extenstionGetObjectNID(ext, + NID_basic_constraints, 1); + + if (obj == NULL) { + /* wolfCLU_extenstionGetObjectNID() frees ext on failure, so it + * must not be freed again here */ ret = WOLFCLU_FATAL_ERROR; } else { - obj->ca = 1; - - ret = wolfSSL_X509_add_ext(x509, newExt, -1); - if (ret != WOLFSSL_SUCCESS) { + obj->ca = 0; /* CA:FALSE -- this is a leaf, not a CA */ + if (wolfSSL_X509_add_ext(x509, ext, -1) != WOLFSSL_SUCCESS) { WOLFCLU_LOG(WOLFCLU_E0, - "error %d adding Basic Constraints extension", ret); + "error adding Basic Constraints extension"); + ret = WOLFCLU_FATAL_ERROR; } - wolfSSL_X509_EXTENSION_free(newExt); + wolfSSL_X509_EXTENSION_free(ext); } } - /* default to version 1 when generating CSR */ + + /* Subject Key Id from this cert's own public key */ if (ret == WOLFCLU_SUCCESS) { - if (wolfSSL_X509_REQ_set_version(x509, WOLFSSL_X509_V1) != - WOLFSSL_SUCCESS) { - wolfCLU_LogError("Error setting CSR version"); + if (wolfSSL_X509_set_subject_key_id_ex(x509) != WOLFSSL_SUCCESS) { + wolfCLU_LogError("Error setting Subject Key Identifier"); ret = WOLFCLU_FATAL_ERROR; } } - /* check that we have the key if re-signing */ - if (ret == WOLFCLU_SUCCESS && - (reqIn == NULL || reSign) && pkey == NULL) { - wolfCLU_LogError("No key loaded to sign with"); - ret = WOLFCLU_FATAL_ERROR; + /* Authority Key Id = the CA cert's Subject Key Id (links the chain) */ + if (ret == WOLFCLU_SUCCESS) { + if (wolfSSL_X509_set_authority_key_id_ex(x509, caCert) != + WOLFSSL_SUCCESS) { + wolfCLU_LogError("Error setting Authority Key Identifier"); + ret = WOLFCLU_FATAL_ERROR; + } + } +#else + if (ret == WOLFCLU_SUCCESS) { + WOLFCLU_LOG(WOLFCLU_L0, "Skipping basicConstaints AKI and SKI " + "WOLFSSL_CERT_EXT or SHA-1 disabled"); } +#endif + + /* Sign with the CA key. wolfSSL_X509_sign() hands back the cert length + * on success rather than WOLFSSL_SUCCESS, so it is kept in a local. */ + if (ret == WOLFCLU_SUCCESS) { + int signSz = wolfSSL_X509_sign(x509, caKey, md); - if (ret == WOLFCLU_SUCCESS && bioOut == NULL && out != NULL) { - bioOut = wolfSSL_BIO_new_file(out, "wb"); - if (bioOut == NULL) { - wolfCLU_LogError("Unable to open output file %s", out); + if (signSz <= 0) { + wolfCLU_LogError("Error signing certificate"); ret = WOLFCLU_FATAL_ERROR; } } - if (ret == WOLFCLU_SUCCESS && bioOut == NULL) { - /* output to stdout if no output is provided */ - bioOut = wolfSSL_BIO_new(wolfSSL_BIO_s_file()); - if (bioOut != NULL) { - if (wolfSSL_BIO_set_fp(bioOut, stdout, BIO_NOCLOSE) - != WOLFSSL_SUCCESS) { + /* Check the certificate just signed against the CA's public key. The + * caller's -verify path can not do this: it only holds the subject's key, + * which is the wrong key once the cert is issued by someone else. */ + if (ret == WOLFCLU_SUCCESS) { + WOLFSSL_EVP_PKEY* pubKey = wolfSSL_X509_get_pubkey(caCert); + + if (pubKey == NULL) { + wolfCLU_LogError("Could not get the public key out of the " + "certificate passed to -CA"); + ret = WOLFCLU_FATAL_ERROR; + } + else { + if (wolfSSL_X509_verify(x509, pubKey) != 1) { + wolfCLU_LogError("New x509 ca signed cert could not be " + "verified"); ret = WOLFCLU_FATAL_ERROR; } + else if (doVerify) { + WOLFCLU_LOG(WOLFCLU_L0, "verify OK"); + } + wolfSSL_EVP_PKEY_free(pubKey); } } + /* the caller writes the encoded form out, after -text / -verify */ - /* sign the req/cert */ - if (ret == WOLFCLU_SUCCESS && (reqIn == NULL || reSign)) { - if (genX509) { -#ifdef NO_WOLFSSL_REQ_PRINT - isCSR = 0; -#endif - /* default to version 3 which supports extensions */ - if (wolfSSL_X509_set_version(x509, WOLFSSL_X509_V3) != - WOLFSSL_SUCCESS) { - wolfCLU_LogError("Unable to set version 3 for cert"); - ret = WOLFSSL_FAILURE; - } + wolfSSL_X509_free(caCert); + wolfSSL_EVP_PKEY_free(caKey); - if (ret == WOLFCLU_SUCCESS) { - ret = wolfSSL_X509_sign(x509, pkey, md); - if (ret > 0) - ret = WOLFSSL_SUCCESS; - } + return ret; +} + + +/* A WOLFSSL_X509 read in from a request carries an internal "is a CSR" flag, + * and wolfSSL uses that flag to pick the type it re-parses the DER as when + * walking extensions. Once -CA/-x509 has signed the request into a + * certificate the flag is stale, and it makes wolfSSL_X509_print() decode the + * new certificate as a request, fail, and silently drop the whole extension + * section. Round tripping the signed DER back through d2i hands back a plain + * certificate object with the flag clear. + * + * return WOLFCLU_SUCCESS on success */ +static int reloadAsCert(WOLFSSL_X509** x509) +{ + int ret = WOLFCLU_SUCCESS; + int derSz; + byte* der = NULL; + byte* pt; /* use pt with i2d/d2i to handle the pointer increment */ + WOLFSSL_X509* cert = NULL; + + if (x509 == NULL || *x509 == NULL) { + return WOLFCLU_FATAL_ERROR; + } + + derSz = wolfSSL_i2d_X509(*x509, NULL); + if (derSz <= 0) { + wolfCLU_LogError("Unable to get size of the signed certificate"); + ret = WOLFCLU_FATAL_ERROR; + } + + if (ret == WOLFCLU_SUCCESS) { + der = (byte*)XMALLOC(derSz, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + if (der == NULL) { + wolfCLU_LogError("Could not allocate space for the signed " + "certificate"); + ret = WOLFCLU_FATAL_ERROR; } - else { - ret = wolfSSL_X509_REQ_sign(x509, pkey, md); + } + + if (ret == WOLFCLU_SUCCESS) { + pt = der; + if (wolfSSL_i2d_X509(*x509, &pt) != derSz) { + wolfCLU_LogError("Unable to encode the signed certificate"); + ret = WOLFCLU_FATAL_ERROR; } + } - if (ret != WOLFSSL_SUCCESS) { - wolfCLU_LogError("Error %d signing", ret); + if (ret == WOLFCLU_SUCCESS) { + pt = der; + cert = wolfSSL_d2i_X509(NULL, (const unsigned char**)&pt, derSz); + if (cert == NULL) { + wolfCLU_LogError("Unable to parse the signed certificate"); ret = WOLFCLU_FATAL_ERROR; } else { - ret = WOLFCLU_SUCCESS; + wolfSSL_X509_free(*x509); + *x509 = cert; } } - if (ret == WOLFCLU_SUCCESS && doVerify) { + XFREE(der, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - /* get public key from req if not passed in */ - if (pkey == NULL) { - pkey = wolfSSL_X509_get_pubkey(x509); - } + return ret; +} - if (pkey == NULL) { - wolfCLU_LogError("Error getting the public key to verify"); - ret = WOLFCLU_FATAL_ERROR; +#endif + +/* return WOLFCLU_SUCCESS on success */ +int wolfCLU_requestSetup(int argc, char** argv) +{ +#ifndef WOLFSSL_CERT_REQ + wolfCLU_LogError("wolfSSL not compiled with --enable-certreq"); + /* silence unused variable warnings */ + (void) argc; + (void) argv; + return NOT_COMPILED_IN; +#elif defined(WOLFCLU_NO_FILESYSTEM) + WOLFCLU_LOG(WOLFCLU_E0, "No Filesystem Support."); + /* silence unused variable warnings */ + (void) argc; + (void) argv; + return NOT_COMPILED_IN; +#else + + char* caFile = NULL; + char* caKeyFile = NULL; + char* outFile = NULL; + char* keyFile = NULL; + char* reqFile = NULL; + char* configFile = NULL; + char* outKeyFile = NULL; + + const WOLFSSL_EVP_MD *md = wolfSSL_EVP_sha256(); + + long serialNumber = -1; + + int ret = WOLFCLU_SUCCESS; + char* subj = NULL; + char* ext = NULL; + char* addExt = NULL; + int keyType = 0; + int keyInfo = 0; + + int algCheck = 0; /* algorithm type */ + int outForm = PEM_FORM; /* default to PEM format */ + int inForm = PEM_FORM; + int option; + int longIndex = 1; + int days = 0; + int genX509 = 0; + int mdSet = 0; + + char password[MAX_PASSWORD_SIZE] = {0}; + /* the length wolfCLU_GetPassword() parsed, not the buffer capacity; + * writeOutPkey() is handed sizeof(password) for that */ + int passwordLen = MAX_PASSWORD_SIZE; + int passoutSet = 0; + + byte doVerify = 0; + byte doTextOut = 0; + byte noOut = 0; + byte useDes = 1; + /* -help prints and stops; tracked so the password wipe still runs */ + byte helpOnly = 0; + /* cleared once the run has produced a certificate rather than a request, + * so -text prints the right object regardless of which printer is used */ + byte isCSR = 1; + + /* Multiple -addext is not yet supported. Detect it up front and fail + * instead of silently dropping the extension and exiting success. */ + { + int i, addExtCount = 0; + for (i = 1; i < argc; i++) { + if (argv[i] != NULL && XSTRCMP(argv[i], "-addext") == 0) { + addExtCount++; + } } - else { - if (wolfSSL_X509_REQ_verify(x509, pkey) == 1) { - WOLFCLU_LOG(WOLFCLU_L0, "verify OK"); + if (addExtCount > 1) { + wolfCLU_LogError("only one -addext arg is currently supported"); + return USER_INPUT_ERROR; + } + } + + opterr = 0; /* do not display unrecognized options */ + optind = 0; /* start at indent 0 */ + while (ret == WOLFCLU_SUCCESS && !helpOnly && + (option = wolfCLU_GetOpt(argc, argv, "", + req_options, &longIndex )) != END_OF_ARGS) { + + switch (option) { + case WOLFCLU_EXTENSIONS: + ext = optarg; + break; + + case WOLFCLU_ADDEXT: + addExt = optarg; + break; + + case WOLFCLU_NODES: + useDes = 0; + break; + + + case WOLFCLU_NEWKEY: + if (keyFile != NULL) { + wolfCLU_LogError("-key/-inkey was set with -newkey " + "which is invalid"); + ret = WOLFCLU_FATAL_ERROR; + break; + } + { + char* split; + if (optarg == NULL) { + wolfCLU_LogError("-newkey needs an arg"); + ret = WOLFCLU_FATAL_ERROR; + break; + } + if ((split = XSTRSTR(optarg, ":")) == NULL) { + wolfCLU_LogError("-newkey needs form : " + "saw: %s", optarg); + ret = WOLFCLU_FATAL_ERROR; + break; + } + /* match the whole name before the ':', so "rsafoo:2048" + * is not accepted as rsa */ + keyType = ((split - optarg) == 3 && + XSTRNCMP("rsa", optarg, 3) == 0) ? + EVP_PKEY_RSA : 0; + if (keyType == 0) { + wolfCLU_LogError("-newkey only supports rsa generation " + "saw request for %.*s, " + "please provide pre-generated key via commandline", + (int)(split - optarg), optarg); + ret = WOLFCLU_FATAL_ERROR; + break; + } + /* the agreement check against -rsa/-ecc/-ed25519 runs + * after the loop, where both values are final */ + { + long bits = 0; + + /* parsed like -days and -set_serial below; a bare XATOI + * accepts "-5" and hands it to the keygen bit setter */ + if (wolfCLU_parseDecimalBounded(split+1, + WOLFCLU_RSA_BITS_2048, WOLFCLU_RSA_BITS_4096, + &bits) != WOLFCLU_SUCCESS || + (bits != WOLFCLU_RSA_BITS_2048 && + bits != WOLFCLU_RSA_BITS_3072 && + bits != WOLFCLU_RSA_BITS_4096)) { + wolfCLU_LogError("-newkey rsa expects a key size of " + "%d, %d or %d, i.e. rsa:2048, got %s", + WOLFCLU_RSA_BITS_2048, WOLFCLU_RSA_BITS_3072, + WOLFCLU_RSA_BITS_4096, split+1); + ret = WOLFCLU_FATAL_ERROR; + break; + } + keyInfo = (int)bits; + } + } + break; + + case WOLFCLU_INFILE: + reqFile = optarg; + break; + + case WOLFCLU_INKEY: /* alias for -key, both are advertised */ + case WOLFCLU_KEY: + if (keyFile != NULL) { + wolfCLU_LogError("-key/-inkey was already set"); + ret = WOLFCLU_FATAL_ERROR; + break; + } + if (keyInfo != 0 && keyType != 0) { + wolfCLU_LogError("-newkey was set with -key/-inkey " + "this is invalid"); + ret = WOLFCLU_FATAL_ERROR; + break; + } + keyFile = optarg; + break; + + case WOLFCLU_OUTFILE: + outFile = optarg; + break; + + case WOLFCLU_OUTKEY: + outKeyFile = optarg; + break; + + case WOLFCLU_CA: + caFile = optarg; + break; + + case WOLFCLU_CAKEY: + caKeyFile = optarg; + break; + + case WOLFCLU_INFORM: + inForm = wolfCLU_checkInform(optarg); + if (inForm == USER_INPUT_ERROR || inForm == RAW_FORM) { + wolfCLU_LogError("must pass pem or der to -inform"); + ret = WOLFCLU_FATAL_ERROR; + } + break; + + case WOLFCLU_OUTFORM: + outForm = wolfCLU_checkOutform(optarg); + if (outForm == USER_INPUT_ERROR || outForm == RAW_FORM) { + wolfCLU_LogError("must pass pem or der to -outform"); + ret = WOLFCLU_FATAL_ERROR; + } + break; + + case WOLFCLU_SUBJECT: + subj = optarg; + break; + + case WOLFCLU_HELP: + /* -passout is parsed before a later -help, so returning + * here would leave a password on the stack */ + wolfCLU_certgenHelp(); + helpOnly = 1; + break; + + case WOLFCLU_RSA: + if (algCheck != 0) { + wolfCLU_LogError("More than one key algorithm passed in"); + ret = WOLFCLU_FATAL_ERROR; + break; + } + algCheck = WC_EVP_PKEY_RSA; + break; + + /* Only keygen is unsupported for these. -key may still come + * later in argv, so the check waits until parsing is done. */ + case WOLFCLU_ECC: + if (algCheck != 0) { + wolfCLU_LogError("More than one key algorithm passed in"); + ret = WOLFCLU_FATAL_ERROR; + break; + } + algCheck = WC_EVP_PKEY_EC; + break; + + case WOLFCLU_ED25519: + if (algCheck != 0) { + wolfCLU_LogError("More than one key algorithm passed in"); + ret = WOLFCLU_FATAL_ERROR; + break; + } + algCheck = WC_EVP_PKEY_ED25519; + break; + + case WOLFCLU_CONFIG: + configFile = optarg; + break; + + case WOLFCLU_DAYS: + { + long d = 0; + + if (optarg == NULL || wolfCLU_parseDecimalBounded(optarg, 1, + WOLFCLU_MAX_VALIDITY, &d) != WOLFCLU_SUCCESS) { + wolfCLU_LogError("-days expects a positive integer, got %s", + optarg != NULL ? optarg : "(nothing)"); + ret = WOLFCLU_FATAL_ERROR; + break; + } + days = (int)d; + break; } - else { - wolfCLU_LogError("verify failed"); + + case WOLFCLU_CERT_SHA: + case WOLFCLU_CERT_SHA224: + case WOLFCLU_CERT_SHA256: + case WOLFCLU_CERT_SHA384: + case WOLFCLU_CERT_SHA512: + /* GetOpt only spots a repeat of the same option name, and + * -sha/-sha1 are two names for one digest, so the exclusion + * is tracked here the way the key algorithms track algCheck */ + if (mdSet) { + wolfCLU_LogError("More than one digest passed in"); + ret = WOLFCLU_FATAL_ERROR; + break; + } + mapOptionToMd(option, &md); + mdSet = 1; + break; + + case WOLFCLU_X509: + genX509 = 1; + break; + + case WOLFCLU_VERIFY: + doVerify = 1; + break; + + case WOLFCLU_TEXT_OUT: + doTextOut = 1; + break; + + case WOLFCLU_PASSWORD_OUT: + ret = wolfCLU_GetPassword(password, &passwordLen, optarg); + passoutSet = 1; + break; + + case WOLFCLU_NOOUT: + noOut = 1; + break; + + case WOLFCLU_SERIAL: + if (optarg == NULL) { + wolfCLU_LogError("-set_serial has no arg"); + ret = WOLFCLU_FATAL_ERROR; + break; + } + if (wolfCLU_parseDecimalBounded(optarg, 1, LONG_MAX, + &serialNumber) != WOLFCLU_SUCCESS) { + wolfCLU_LogError("-set_serial expects a positive integer, " + "got %s", optarg); + ret = WOLFCLU_FATAL_ERROR; + } + break; + + case WOLFCLU_NEW: + break; + + case ARG_FOUND_TWICE: ret = WOLFCLU_FATAL_ERROR; - } + break; + + case ':': + case '?': + wolfCLU_LogError("Unexpected argument"); + ret = WOLFCLU_FATAL_ERROR; + wolfCLU_certgenHelp(); + break; + + default: + wolfCLU_LogError("Unsupported argument"); + ret = WOLFCLU_FATAL_ERROR; + wolfCLU_certgenHelp(); } } - if (ret == WOLFCLU_SUCCESS && doTextOut) { -#ifdef NO_WOLFSSL_REQ_PRINT - wolfSSL_X509_REQ_print(bioOut, x509, isCSR); -#else - wolfSSL_X509_REQ_print(bioOut, x509); -#endif + /* usage has been printed; the password buffer still has to be wiped */ + if (helpOnly) { + wolfCLU_ForceZero(password, sizeof(password)); + return WOLFCLU_SUCCESS; } - if (ret == WOLFCLU_SUCCESS && !noOut) { - if (outForm == DER_FORM) { - if (genX509) { - ret = wolfSSL_i2d_X509_bio(bioOut, x509); - } - else { - ret = wolfSSL_i2d_X509_REQ_bio(bioOut, x509); - } + /* Checked after the loop rather than inside the -newkey case, so it holds + * however the two options were ordered on the command line. */ + if (ret == WOLFCLU_SUCCESS && keyType != 0 && algCheck != 0 && + algCheck != keyType) { + wolfCLU_LogError("-newkey asks to generate an rsa key but a different " + "algorithm flag was also given"); + ret = WOLFCLU_FATAL_ERROR; + } + + /* wolfCLU cannot generate an ECC or ED25519 key yet, but it can build a + * request around one given by -key or carried by -in, so this only fails + * when a key would actually have to be generated. */ + if (ret == WOLFCLU_SUCCESS && + (algCheck == WC_EVP_PKEY_EC || algCheck == WC_EVP_PKEY_ED25519) && + keyFile == NULL && reqFile == NULL) { + wolfCLU_LogError("%s key generation is not yet supported; pass a " + "pre-generated key with -key", + algCheck == WC_EVP_PKEY_EC ? "ECC" : "ED25519"); + ret = WOLFCLU_FATAL_ERROR; + } + + /* -CA certifies an existing request; without -in there is nothing to + * certify. Caught here so the operator is told which option is missing, + * rather than by the signature check inside caSignCert on an empty + * request, which reports "Req Failed verification". */ + if (ret == WOLFCLU_SUCCESS && caFile != NULL && reqFile == NULL) { + wolfCLU_LogError("-in was not set but -CA was passed; -CA signs a " + "request that already exists"); + ret = WOLFCLU_FATAL_ERROR; + } + + /* the two ask for mutually exclusive artifacts: a self signed root vs a + * CA issued leaf. The signing dispatch takes -CA first, so left unchecked + * this hands back a certificate the operator did not ask for, at exit 0 */ + if (ret == WOLFCLU_SUCCESS && caFile != NULL && genX509) { + wolfCLU_LogError("-x509 and -CA cannot be used together; -x509 self " + "signs while -CA signs with another certificate's key"); + ret = WOLFCLU_FATAL_ERROR; + } + + /* without -CA there is nothing to pair -CAkey with, the run would fall + * through to the CSR / -x509 path and hand back the wrong object */ + if (ret == WOLFCLU_SUCCESS && caKeyFile != NULL && caFile == NULL) { + wolfCLU_LogError("-CAkey was passed without -CA; -CAkey names the key " + "for the certificate given to -CA"); + ret = WOLFCLU_FATAL_ERROR; + } + + /* -CA passes the request through untouched, so reject anything that would + * alter it. Done here so -newkey cannot truncate -keyout before failing. */ + if (ret == WOLFCLU_SUCCESS && caFile != NULL && + (subj != NULL || configFile != NULL || addExt != NULL || + keyFile != NULL || keyType != 0)) { + wolfCLU_LogError("-subj, -config, -addext, -key/-inkey and -newkey " + "are not applied when signing with -CA"); + wolfCLU_LogError("create the request with those options first, then " + "sign it"); + ret = WOLFCLU_FATAL_ERROR; + } + + /* A PKCS#10 request has neither field, so say so rather than dropping + * the value silently, matching the cross-option checks above. */ + if (ret == WOLFCLU_SUCCESS && caFile == NULL && !genX509 && + (serialNumber >= 0 || days != 0)) { + WOLFCLU_LOG(WOLFCLU_L0, "Ignoring %s, a certificate request carries " + "neither; they apply to -x509 and -CA", + (serialNumber >= 0 && days != 0) ? "-set_serial and -days" : + (serialNumber >= 0 ? "-set_serial" : "-days")); + } + + if (ret == WOLFCLU_SUCCESS && serialNumber < 0 && (caFile != NULL + || genX509)) { + WC_RNG rng = {0}; + if (wc_InitRng(&rng) != 0) { + wolfCLU_LogError("Unable to initialize RNG for serial number"); + ret = WOLFCLU_FATAL_ERROR; } else { - if (genX509) { - ret = wolfSSL_PEM_write_bio_X509(bioOut, x509); + /* Fill a whole long with random bytes, accumulated separately + * because serialNumber still holds the all-ones -1 sentinel that + * OR-ing into would be a no-op. 'unsigned long' is the unsigned + * twin of the long the setters take, so the byte count, mask and + * final cast are one width on every target -- word64 matches long + * only on LP64 and does not exist without a 64-bit type. */ + word32 index = 0; + unsigned long serial = 0; + byte randBytes[sizeof(unsigned long)]; + + /* wolfCrypt returns 0 on success, not WOLFSSL_SUCCESS */ + if (wc_RNG_GenerateBlock(&rng, randBytes, (word32)sizeof(randBytes)) + != 0) { + wolfCLU_LogError("Unable to generate serial number"); + ret = WOLFCLU_FATAL_ERROR; } else { - ret = wolfSSL_PEM_write_bio_X509_REQ(bioOut, x509); + for (; index < (word32)sizeof(randBytes); index++) { + serial = (serial << 8) | randBytes[index]; + } + } + wc_FreeRng(&rng); + + if (ret == WOLFCLU_SUCCESS) { + /* Clear the sign bit: a serial has to be a positive integer + * (RFC 5280 4.1.2.2), and the setters below take a signed + * long. Zero is then steered away from because both signing + * helpers gate on "serial > 0" and would otherwise skip + * wolfSSL_X509_set_serialNumber entirely, silently emitting + * wolfSSL's default serial instead of the one drawn here. */ + serial &= ~0UL >> 1; + if (serial == 0) { + serial = 1; + } + serialNumber = (long)serial; } } + } - if (ret != WOLFSSL_SUCCESS) { - wolfCLU_LogError("Error %d writing out cert req", ret); - ret = WOLFCLU_FATAL_ERROR; + if (ret == WOLFCLU_SUCCESS) { + WOLFSSL_BIO* outBio = NULL; + /* -keyout is opened later, immediately before the generated key is + * written. Opening it here would truncate the file even on the paths + * that never generate a key -- including the case where -keyout names + * the same file as -key, which destroyed the user's private key + * before it had been read. */ + WOLFSSL_BIO* outKeyBio = NULL; + WOLFSSL_BIO* keyBio = NULL; + WOLFSSL_BIO* reqBio = NULL; + WOLFSSL_BIO* caBio = NULL; + WOLFSSL_BIO* caKeyBio = NULL; + WOLFSSL_X509* x509 = NULL; + /* -out names the same file as -keyout: one stream, one free */ + byte sharedOutBio = 0; + + + if (ret == WOLFCLU_SUCCESS && keyFile != NULL) { + keyBio = wolfSSL_BIO_new_file(keyFile, "rb"); + if (keyBio == NULL) { + wolfCLU_LogError("Could not open -key file %s", keyFile); + ret = WOLFCLU_FATAL_ERROR; + } } - else { - /* set WOLFSSL_SUCCESS case to success value */ - ret = WOLFCLU_SUCCESS; + + if (ret == WOLFCLU_SUCCESS && reqFile != NULL) { + reqBio = wolfSSL_BIO_new_file(reqFile, "rb"); + if (reqBio == NULL) { + wolfCLU_LogError("Could not open -in file %s", reqFile); + ret = WOLFCLU_FATAL_ERROR; + } } - } - if (ret == WOLFCLU_SUCCESS && keyType != NULL && keyInfo != NULL) { - WOLFSSL_BIO* keyOutBio; + if (ret == WOLFCLU_SUCCESS && caFile != NULL) { + caBio = wolfSSL_BIO_new_file(caFile, "rb"); + if (caBio == NULL) { + wolfCLU_LogError("Could not open -CA file %s", caFile); + ret = WOLFCLU_FATAL_ERROR; + } + } - if (keyOut != NULL) { - keyOutBio = wolfSSL_BIO_new_file(keyOut, "wb"); + if (ret == WOLFCLU_SUCCESS && caKeyFile != NULL) { + caKeyBio = wolfSSL_BIO_new_file(caKeyFile, "rb"); + if (caKeyBio == NULL) { + wolfCLU_LogError("Could not open -CAkey file %s", caKeyFile); + ret = WOLFCLU_FATAL_ERROR; + } } - else { - keyOutBio = wolfSSL_BIO_new(wolfSSL_BIO_s_file()); - if (keyOutBio != NULL) { - if (wolfSSL_BIO_set_fp(keyOutBio, stdout, BIO_NOCLOSE) - != WOLFSSL_SUCCESS) { + + /* gated on 'ret' like the BIO opens above, so a failure there is not + * followed by an unrelated parse error naming the wrong option */ + if (ret == WOLFCLU_SUCCESS && reqBio != NULL) { + if (inForm == PEM_FORM) { + wolfSSL_PEM_read_bio_X509_REQ(reqBio, &x509, NULL, NULL); + if (x509 == NULL) { + wolfCLU_LogError("Unable to create x509 object from PEM " + "req"); + ret = WOLFCLU_FATAL_ERROR; + } + } + else { + wolfSSL_d2i_X509_REQ_bio(reqBio, &x509); + if (x509 == NULL) { + wolfCLU_LogError("Unable to create x509 object from DER " + "req"); ret = WOLFCLU_FATAL_ERROR; } } } + else if (ret == WOLFCLU_SUCCESS) { + x509 = wolfSSL_X509_new(); + if (x509 == NULL) { + wolfCLU_LogError("Unable to create empty x509 object"); + ret = WOLFCLU_FATAL_ERROR; + } + } - if (keyOutBio == NULL) { - wolfCLU_LogError("Error opening keyout file %s", keyOut); - ret = WOLFCLU_FATAL_ERROR; + /* Default the request to v1 */ + if (ret == WOLFCLU_SUCCESS) { + if (wolfSSL_X509_REQ_set_version(x509, WOLFSSL_X509_V1) != + WOLFSSL_SUCCESS) { + wolfCLU_LogError("Error setting CSR version"); + ret = WOLFCLU_FATAL_ERROR; + } } if (ret == WOLFCLU_SUCCESS) { - if (useDes) { - if (!passout) { - byte pass[MAX_PASSWORD_SIZE]; - wolfCLU_GetStdinPassword(pass, (word32*)&passwordSz); + /* pkey is hoisted to the create-block scope so it stays alive for the + * dispatch below (makeReq/selfSignCert sign with it). Freed once at the + * end of this block. */ + byte reSign = 0; + + /* Setting up public key for x509 cert */ + WOLFSSL_EVP_PKEY *pkey = NULL; + if (ret == WOLFCLU_SUCCESS ) { + WOLFSSL_EVP_PKEY_CTX* ctx = NULL; + if (keyBio != NULL) { + pkey = wolfSSL_PEM_read_bio_PrivateKey(keyBio, NULL, NULL, NULL); + if (pkey == NULL) { + wolfCLU_LogError("Error reading key from file"); + ret = USER_INPUT_ERROR; + } + } + else if (keyType != 0 && keyInfo != 0) { + ctx = wolfSSL_EVP_PKEY_CTX_new_id(keyType, NULL); + + if (ctx == NULL) { + wolfCLU_LogError("Unknown/unsupported algo name"); + ret = WOLFCLU_FATAL_ERROR; + } + else if (wolfSSL_EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, + keyInfo) != WOLFSSL_SUCCESS) { + wolfCLU_LogError("Error setting rsa keygen bits to %d", + keyInfo); + ret = WOLFCLU_FATAL_ERROR; + } + + if (ret == WOLFCLU_SUCCESS) { + if (wolfSSL_EVP_PKEY_keygen(ctx, &pkey) != WOLFSSL_SUCCESS) { + wolfCLU_LogError("Error with keygen"); + ret = WOLFCLU_FATAL_ERROR; + } + } + + + } + else if (wolfSSL_X509_get_pubkey_type(x509) <= 0) { + wolfCLU_LogError("No public key provided for x509, " + "use -newkey, -key, or pass in a req"); + ret = WOLFCLU_FATAL_ERROR; + } - if (pass[0] == '\0') { - wolfCLU_LogError("Please enter a password"); + if (ret == WOLFCLU_SUCCESS && pkey != NULL) { + if (wolfSSL_X509_set_pubkey(x509, pkey) + != WOLFSSL_SUCCESS) { ret = WOLFCLU_FATAL_ERROR; } + else if (reqBio != NULL) { + /* -newkey/-key replaced an existing request's public + * key, so its old signature no longer covers it */ + reSign = 1; + } + } + + /* new key was made so we must write it out */ + if (ret == WOLFCLU_SUCCESS && keyType != 0 && keyInfo != 0) { + if (outKeyFile != NULL) { + outKeyBio = wolfSSL_BIO_new_file(outKeyFile, "wb"); + if (outKeyBio == NULL) { + wolfCLU_LogError("Could not open out -keyout " + "file %s", outKeyFile); + ret = WOLFCLU_FATAL_ERROR; + } + } + if (ret == WOLFCLU_SUCCESS) { + ret = writeOutPkey(outKeyBio, pkey, useDes, password, + (word32)sizeof(password), passoutSet); + } + } + else if (ret == WOLFCLU_SUCCESS && outKeyFile != NULL) { + /* no key was generated, so there is nothing to write out */ + WOLFCLU_LOG(WOLFCLU_L0, "Ignoring -keyout, it only applies " + "when -newkey generates a key"); + } + + wolfSSL_EVP_PKEY_CTX_free(ctx); + } + + /* Handle extensions in this block. The -CA case never gets here + * with any of these set, it was rejected up front before a key + * was read or generated. */ + if (ret == WOLFCLU_SUCCESS && caBio == NULL) { + + if (ret == WOLFCLU_SUCCESS && configFile != NULL) { + ret = wolfCLU_readConfig(x509, configFile, (char*)"req", ext); + reSign = 1; + } + + /* If subj was provided, parse it */ + if (ret == WOLFCLU_SUCCESS && subj != NULL) { + WOLFSSL_X509_NAME *name; + name = wolfCLU_ParseX509NameString(subj, (int)XSTRLEN(subj)); + if (name != NULL) { + wolfSSL_X509_REQ_set_subject_name(x509, name); + wolfSSL_X509_NAME_free(name); + } + else { + wolfCLU_LogError("Failed to parse -subj string"); + wolfCLU_certgenHelp(); + ret = USER_INPUT_ERROR; + } + reSign = 1; + } + + /* apply the -addext extension, if present */ + if (ret == WOLFCLU_SUCCESS && addExt != NULL) { + reSign = 1; + ret = wolfCLU_parseAddExt(x509, addExt); + } + + /* last try to source a subject name from stdin -- only when + * building a brand-new request. If a request was read in via -in, + * its subject must be preserved (e.g. a plain format conversion), + * so don't prompt/overwrite it here. */ + if (ret == WOLFCLU_SUCCESS && reqFile == NULL && subj == NULL && + configFile == NULL) { + WOLFSSL_X509_NAME *name; + + name = wolfSSL_X509_NAME_new(); + if (name == NULL) { + ret = MEMORY_E; + } else { - ret = wolfCLU_pKeyPEMtoPriKeyEnc(keyOutBio, pkey, DES3b, - pass, passwordSz); + ret = wolfCLU_CreateX509Name(name); + if (ret == WOLFCLU_SUCCESS) { + wolfSSL_X509_REQ_set_subject_name(x509, name); + } + wolfSSL_X509_NAME_free(name); + } + reSign = 1; + } + } + + if (ret == WOLFCLU_SUCCESS) { + if (caBio != NULL) { + /* -CA: issue a CA-signed cert. caSignCert reads */ + if (caKeyFile == NULL) { + wolfCLU_LogError("-CAkey was not set but -ca " + "was passed"); + ret = WOLFCLU_FATAL_ERROR; + } + if (ret == WOLFCLU_SUCCESS) { + ret = caSignCert(x509, caBio, caKeyBio, md, + days == 0 ? WOLFCLU_DEFAULT_VALIDITY : days, + serialNumber, doVerify); + isCSR = 0; } } + else if (genX509) { + /* -x509: self-signed cert, own key is issuer + signer */ + ret = selfSignCert(x509, pkey, md, + days == 0 ? WOLFCLU_DEFAULT_VALIDITY : days, + serialNumber); + isCSR = 0; + } + else if (reqBio == NULL || reSign) { + /* CSR: sign with the subject's own key. An -in request + * altered by -subj/-config/-addext must be re-signed. */ + ret = makeReq(x509, pkey, md, reSign); + } + + /* the request has become a certificate, so hand the rest of + * the flow an object that knows it is one */ + if (ret == WOLFCLU_SUCCESS && !isCSR) { + ret = reloadAsCert(&x509); + } + } + + wolfSSL_EVP_PKEY_free(pkey); + } + + /* -verify checks the signature, so it has to run after the dispatch + * above has signed the request or certificate. It also has to run + * after isCSR has been cleared by the -x509/-CA arms, otherwise a + * certificate would be handed to the request verifier. CA signed + * certs are already verified */ + if (ret == WOLFCLU_SUCCESS && doVerify && caBio == NULL) { + ret = verifyX509(keyBio, x509, isCSR); + } + + /* Nothing is opened when there is nothing to write: "wb" truncates, + * so "-noout -out f" used to leave f an empty file. */ + if (ret == WOLFCLU_SUCCESS && (!noOut || doTextOut)) { + if (outFile != NULL) { + /* "-keyout f -out f" appends both objects to one file, as + * OpenSSL's req does. Reopening with "wb" would truncate the + * key and leave both BIOs flushing from offset 0. */ + if (outKeyBio != NULL && + wolfCLU_isSameFile(outKeyFile, outFile)) { + outBio = outKeyBio; + sharedOutBio = 1; + } else { - ret = wolfCLU_pKeyPEMtoPriKeyEnc(keyOutBio, pkey, DES3b, - (byte*)password, passwordSz); + outBio = wolfSSL_BIO_new_file(outFile, "wb"); + if (outBio == NULL) { + wolfCLU_LogError("Could not open -out file %s", + outFile); + ret = WOLFCLU_FATAL_ERROR; + } } } else { - ret = wolfCLU_pKeyPEMtoPriKey(keyOutBio, pkey); + outBio = wolfSSL_BIO_new_fp(stdout, BIO_NOCLOSE); + if (outBio == NULL) { + wolfCLU_LogError("Could open stdout as default output"); + ret = WOLFCLU_FATAL_ERROR; + } } } - wolfSSL_BIO_free(keyOutBio); - } - (void)algCheck; - (void)in; - (void)oid; + if (ret == WOLFCLU_SUCCESS && doTextOut) { + int printRet; - if (keyType != NULL) { - XFREE(keyType, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (isCSR) { +#ifdef NO_WOLFSSL_REQ_PRINT + printRet = wolfSSL_X509_REQ_print(outBio, x509, isCSR); +#else + printRet = wolfSSL_X509_REQ_print(outBio, x509); +#endif + } + else { + /* -CA/-x509 produced a certificate, not a request. Both + * request printers label their output "Certificate Request:", + * so print it as the certificate it is instead. */ + printRet = wolfSSL_X509_print(outBio, x509); + } + + if (printRet != WOLFSSL_SUCCESS) { + ret = WOLFCLU_FATAL_ERROR; + } + } + + /* the encoded body goes last so that -text reads before it, matching + * OpenSSL */ + if (ret == WOLFCLU_SUCCESS && !noOut) { + ret = writeOutX509(outBio, x509, outForm, isCSR); + } + + wolfSSL_BIO_free(reqBio); + wolfSSL_BIO_free(keyBio); + if (!sharedOutBio) { + wolfSSL_BIO_free(outBio); + } + wolfSSL_BIO_free(caBio); + wolfSSL_BIO_free(caKeyBio); + wolfSSL_BIO_free(outKeyBio); + wolfSSL_X509_free(x509); } - wolfSSL_BIO_free(reqIn); - wolfSSL_BIO_free(keyIn); - wolfSSL_BIO_free(bioOut); - wolfSSL_X509_free(x509); - wolfSSL_EVP_PKEY_free(pkey); + wolfCLU_ForceZero(password, sizeof(password)); + return ret; #endif } diff --git a/src/x509/clu_x509_sign.c b/src/x509/clu_x509_sign.c index 75d5b382..c7219402 100644 --- a/src/x509/clu_x509_sign.c +++ b/src/x509/clu_x509_sign.c @@ -18,6 +18,7 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA */ +/* isalpha() below; do not rely on a wolfSSL header pulling this in */ #include #include @@ -1008,24 +1009,34 @@ enum wc_HashType wolfCLU_StringToHashType(char* in) } -static int _wolfCLU_CertSetDate(WOLFSSL_X509* x509, int days) +/* Set the validity window on 'x509' to 'days' starting now. Shared with + * wolfCLU_certSetup() so the two commands cannot drift apart. + * returns WOLFCLU_SUCCESS on success */ +int wolfCLU_CertSetDate(WOLFSSL_X509* x509, int days) { int ret = WOLFCLU_SUCCESS; if (x509 != NULL && days > 0) { - WOLFSSL_ASN1_TIME *notBefore, *notAfter; + WOLFSSL_ASN1_TIME *notBefore = NULL, *notAfter = NULL; time_t t; - t = time(NULL); + if ((t = time(NULL)) == (time_t)-1) { + wolfCLU_LogError("Error fetching time"); + return WOLFCLU_FATAL_ERROR; + } + notBefore = wolfSSL_ASN1_TIME_adj(NULL, t, 0, 0); notAfter = wolfSSL_ASN1_TIME_adj(NULL, t, days, 0); if (notBefore == NULL || notAfter == NULL) { wolfCLU_LogError("Error creating not before/after dates"); ret = WOLFCLU_FATAL_ERROR; } - else { - wolfSSL_X509_set_notBefore(x509, notBefore); - wolfSSL_X509_set_notAfter(x509, notAfter); + else if (wolfSSL_X509_set_notBefore(x509, notBefore) + != WOLFSSL_SUCCESS || + wolfSSL_X509_set_notAfter(x509, notAfter) + != WOLFSSL_SUCCESS) { + wolfCLU_LogError("Error setting not before/after dates"); + ret = WOLFCLU_FATAL_ERROR; } wolfSSL_ASN1_TIME_free(notBefore); @@ -1277,7 +1288,7 @@ int wolfCLU_CertSign(WOLFCLU_CERT_SIGN* csign, WOLFSSL_X509* x509) /* set cert date */ if (ret == WOLFCLU_SUCCESS) { - ret = _wolfCLU_CertSetDate(x509, csign->days); + ret = wolfCLU_CertSetDate(x509, csign->days); } /* set cert issuer */ @@ -1375,9 +1386,11 @@ int wolfCLU_CertSign(WOLFCLU_CERT_SIGN* csign, WOLFSSL_X509* x509) wolfSSL_ASN1_INTEGER_free(s); } - /* set extensions */ + /* set extensions. The signing certificate is handed over so an + * authorityKeyIdentifier names its key rather than the certified one. */ if (ret == WOLFCLU_SUCCESS && csign->ext != NULL) { - ret = wolfCLU_setExtensions(x509, csign->config, csign->ext); + ret = wolfCLU_setExtensions(x509, csign->config, csign->ext, + csign->ca == x509 ? NULL : csign->ca); } /* sign the certificate */ diff --git a/tests/x509/x509-ca-test.py b/tests/x509/x509-ca-test.py index 514d00d7..2613d371 100644 --- a/tests/x509/x509-ca-test.py +++ b/tests/x509/x509-ca-test.py @@ -391,6 +391,28 @@ def test_override_extensions_md_days_cert_keyfile(self): os.path.join(CERTS_DIR, "ca-ecc-key.pem")) self.assertEqual(r.returncode, 0, r.stderr) + def test_days_bounds(self): + """-days is bounded the same way as `req -days` and `x509 -days`. + + The bound is WOLFCLU_MAX_VALIDITY, the largest day count that still + fits an int once converted to seconds; ca used to accept up to INT_MAX + and only fail later, from inside the time formatter.""" + for bad in ("0", "-1", "24856", "2147483647", "abc", "10.5"): + with self.subTest(days=bad): + r = run_wolfssl("ca", "-config", self.conf, + "-in", self.csr, + "-out", "test_ca_days_bad.pem", + "-days", bad) + self.assertNotEqual(r.returncode, 0, + "-days {} must be rejected".format(bad)) + self.assertIn("-days", r.stdout + r.stderr) + + out_name = "test_ca_days_max.pem" + self._clean(_tmp(out_name)) + r = run_wolfssl("ca", "-config", self.conf, + "-in", self.csr, "-out", out_name, + "-days", "24855") + self.assertEqual(r.returncode, 0, r.stderr) class TestCAKeyMismatch(unittest.TestCase): diff --git a/tests/x509/x509-req-test.py b/tests/x509/x509-req-test.py index 772a7bef..8dfd52c5 100644 --- a/tests/x509/x509-req-test.py +++ b/tests/x509/x509-req-test.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 """Tests for wolfssl req and x509 -req (converted from x509-req-test.sh).""" +import datetime import os import re import shutil @@ -12,6 +13,32 @@ from wolfclu_test import WOLFSSL_BIN, CERTS_DIR, is_fips, run_wolfssl, test_main +def _ext_value(text, header): + """Return the value line following an X509v3 extension header. + + `x509 -text` prints each extension as a header line ending in ':' with the + value indented on the line below. A leading 'keyid:' is stripped so an + Authority Key Id can be compared directly against a Subject Key Id. + Returns None when the extension is absent.""" + lines = text.splitlines() + for i, line in enumerate(lines): + if header in line and i + 1 < len(lines): + value = lines[i + 1].strip() + return value[len("keyid:"):] if value.startswith("keyid:") \ + else value + return None + + +def _parse_time(text, label): + """Parse a notBefore/notAfter line out of `x509 -text` output.""" + m = re.search(re.escape(label) + r"\s*:\s*(.+)", text) + assert m, "could not find '{}' in cert text".format(label) + # e.g. "Jul 22 22:25:31 2026 GMT"; collapse the double-space padding + # used for single-digit days and drop the trailing timezone. + raw = " ".join(m.group(1).replace("GMT", "").split()) + return datetime.datetime.strptime(raw, "%b %d %H:%M:%S %Y") + + def _tmp(name): """Return an absolute path for a temp file in the current working directory. @@ -280,9 +307,10 @@ def test_req_inline_subjectaltname_openssl_compat(self): not the @section indirection) is accepted and applied to the cert. Pins the behavior in wolfCLU_setExtensions (clu_config.c): the inline - form is parsed via the same path as -addext (wolfCLU_setInlineAltNames), - so an OpenSSL-style config is neither silently dropped nor rejected. - Whitespace after the comma must be tolerated. Skipped on builds without + form is parsed via the same path as -addext + (wolfCLU_setInlineSubjectAltNames), so an OpenSSL-style config is + neither silently dropped nor rejected. Whitespace after the comma + must be tolerated. Skipped on builds without cert extensions, where the parsing path is absent.""" conf = _tmp("test_req_inline_san.conf") out = _tmp("test_req_inline_san.crt") @@ -325,7 +353,8 @@ def test_req_inline_subjectaltname_trims_whitespace(self): """Inline subjectAltName entries are trimmed of surrounding whitespace like OpenSSL: whitespace BEFORE the comma (a trailing space on the value) and AFTER the colon must not end up in the stored name. Pins the - trailing/leading trim in wolfCLU_setInlineAltNames (clu_config.c).""" + trailing/leading trim in wolfCLU_setInlineSubjectAltNames + (clu_config.c).""" conf = _tmp("test_req_inline_san_ws.conf") out = _tmp("test_req_inline_san_ws.crt") self._clean(conf, out) @@ -470,9 +499,17 @@ def test_req_addext_san_entry_no_colon_fails(self): def test_req_addext_unsupported_extension_fails(self): """req -addext with an unsupported extension name should fail.""" - self._addext_fails("keyUsage=digitalSignature", + self._addext_fails("bogusExtension=1", "test_req_addext_unsupported_ext.crt") + def test_req_addext_name_prefix_not_matched(self): + """An extension name is matched up to the '=', not as a prefix. + + "keyUsagePeriod" starts with the supported name "keyUsage", so a + prefix-only comparison would wrongly accept it as a key usage.""" + self._addext_fails("keyUsagePeriod=foo", + "test_req_addext_prefix.crt") + def test_req_addext_unsupported_alt_type_fails(self): """req -addext with an unsupported subjectAltName type should fail.""" self._addext_fails("subjectAltName=otherName:foo", @@ -521,6 +558,103 @@ def test_pem_to_der_to_pem(self): self.assertEqual(f1.read(), f2.read(), "PEM -> DER -> PEM round-trip mismatch") + def test_addext_on_existing_request_is_signed_over(self): + """-in plus -addext must re-sign, not copy the request through. + + The mutation is applied to the parsed struct, but PEM output writes + the cached input DER and DER output re-attaches the original + signature, so skipping the re-sign either drops the extension at exit + 0 or emits a request whose signature does not cover its own body.""" + for form in ("pem", "der"): + with self.subTest(outform=form): + new = _tmp("test_req_mod." + form) + self._clean(new) + r = run_wolfssl("req", "-in", self.csr, + "-key", os.path.join(CERTS_DIR, + "server-key.pem"), + "-addext", + "subjectAltName=DNS:added.example.com", + "-outform", form, "-out", new) + self.assertEqual(r.returncode, 0, r.stderr) + + t = run_wolfssl("req", "-inform", form, "-in", new, + "-text", "-noout") + self.assertEqual(t.returncode, 0, t.stderr) + self.assertIn("added.example.com", t.stdout, + "-addext was dropped on the -in path") + + v = run_wolfssl("req", "-inform", form, "-in", new, + "-noout", "-verify") + self.assertEqual(v.returncode, 0, + "altered request was not re-signed: " + + v.stderr) + + def test_newkey_on_existing_request_is_signed_over(self): + """-in plus -newkey must re-sign over the replaced public key. + + -newkey swaps the request's public key but is not one of the arms + that sets reSign, so the signing dispatch used to skip makeReq + entirely: PEM output re-emitted the cached input DER (dropping the + new key at exit 0) and DER output re-encoded a request whose + signature did not cover its own public key.""" + for form in ("pem", "der"): + with self.subTest(outform=form): + new = _tmp("test_req_newkey." + form) + new_key = _tmp("test_req_newkey.key") + self._clean(new, new_key) + r = run_wolfssl("req", "-in", self.csr, + "-newkey", "rsa:2048", + "-keyout", new_key, + "-passout", "pass:wolfssl", + "-outform", form, "-out", new) + self.assertEqual(r.returncode, 0, r.stderr) + + v = run_wolfssl("req", "-inform", form, "-in", new, + "-noout", "-verify") + self.assertEqual(v.returncode, 0, + "request with a replaced key was not " + "re-signed: " + v.stderr) + + with open(new, "rb") as f1, open(self.csr, "rb") as f2: + self.assertNotEqual( + f1.read(), f2.read(), + "-newkey was dropped, input copied through verbatim") + + def test_in_place_conversion_does_not_truncate_input(self): + """-out naming the same file as -in must not destroy the request. + + -out is opened "wb", which truncates the moment it is opened. If it + is opened before -in has been read, an in-place conversion wipes the + user's CSR and then fails on an empty input. The command must either + succeed with a valid converted request or leave the original intact. + """ + in_place = _tmp("test_req_rt_inplace.csr") + self._clean(in_place) + shutil.copyfile(self.csr, in_place) + with open(in_place, "rb") as f: + original = f.read() + + r = run_wolfssl("req", "-inform", "pem", "-outform", "der", + "-in", in_place, "-out", in_place) + + with open(in_place, "rb") as f: + after = f.read() + self.assertGreater(len(after), 0, + "in-place conversion truncated the input file") + + if r.returncode == 0: + # Converted in place: the file must now be a readable DER request. + check = run_wolfssl("req", "-inform", "der", "-in", in_place, + "-noout", "-verify") + self.assertEqual(check.returncode, 0, + "in-place conversion left an unreadable request: " + + check.stderr) + else: + # Refused the aliasing: the original must be untouched. + self.assertEqual(after, original, + "failed in-place conversion still modified the " + "input file") + class TestReqVerify(unittest.TestCase): """Test req -verify, including that a tampered CSR fails (F-5363).""" @@ -552,6 +686,36 @@ def test_verify_good_csr(self): self.assertEqual(r.returncode, 0, r.stderr) self.assertIn("verify OK", r.stdout + r.stderr) + def test_verify_newly_created_csr(self): + """-verify on a request this same command creates must succeed. + + Every other -verify test reads an already signed request from -in, so + none of them notice if the verify runs before the signing step. This + one does: the request is built and signed in the same invocation.""" + tmp = _tmp("test_req_new_verify.csr") + self._clean(tmp) + r = run_wolfssl("req", "-new", + "-key", os.path.join(CERTS_DIR, "server-key.pem"), + "-subj", "CN=test", + "-out", tmp, "-verify", "-noout") + self.assertEqual(r.returncode, 0, r.stderr) + self.assertIn("verify OK", r.stdout + r.stderr) + + def test_verify_newly_created_self_signed_cert(self): + """-x509 -verify checks the certificate the command just self signed. + + This is the only path that reaches the X509_verify arm of verifyX509; + isCSR is cleared by the -x509 dispatch, so the verify has to run after + it to get there at all.""" + tmp = _tmp("test_req_x509_verify.crt") + self._clean(tmp) + r = run_wolfssl("req", "-x509", "-days", "3650", + "-key", os.path.join(CERTS_DIR, "server-key.pem"), + "-subj", "CN=test", + "-out", tmp, "-verify", "-noout") + self.assertEqual(r.returncode, 0, r.stderr) + self.assertIn("verify OK", r.stdout + r.stderr) + def test_verify_tampered_csr_der_fails(self): """A CSR with a corrupted signature must fail verification (F-5363).""" bad = _tmp("test_req_verify_bad.der") @@ -612,6 +776,34 @@ def test_x509_in_csr_no_req_flag_fails(self): self._clean(_tmp("tmp_sign.cert")) self.assertNotEqual(r.returncode, 0) + def test_x509_days_on_a_cert_without_req_fails(self): + """-days alters the cert, which needs a re-sign, so -req is required. + + Uses a real certificate rather than a CSR so the failure comes from + the -days/-req check and not from the "input is a request" check.""" + out = _tmp("tmp_x509_days_noreq.cert") + self._clean(out) + r = run_wolfssl("x509", "-in", os.path.join(CERTS_DIR, "ca-cert.pem"), + "-days", "100", "-out", out) + self.assertNotEqual(r.returncode, 0) + self.assertIn("-req", r.stdout + r.stderr) + + def test_x509_days_rejects_out_of_range(self): + """-days must reject values that would wrap an int, not truncate them. + + XATOI wraps silently, so 4294967396 would be accepted as 100 and + issue a 100-day certificate at exit 0.""" + for bad in ("0", "-1", "abc", "4294967396", "99999999999999999999"): + with self.subTest(days=bad): + out = _tmp("tmp_x509_days_bad.cert") + self._clean(out) + r = run_wolfssl("x509", "-req", "-in", self.csr, + "-days", bad, "-signkey", + os.path.join(CERTS_DIR, "server-key.pem"), + "-out", out) + self.assertNotEqual(r.returncode, 0, + "-days {} must be rejected".format(bad)) + def test_x509_req_without_signkey_fails(self): """x509 -req without -signkey should fail.""" r = run_wolfssl("x509", "-req", "-in", self.csr, "-days", "3650", @@ -637,6 +829,721 @@ def test_x509_req_signkey_succeeds(self): "-out", out) self.assertEqual(r.returncode, 0, r.stderr) + def test_x509_req_signkey_days_sets_validity_window(self): + """x509 -req -signkey -days N sets a notAfter exactly N days after + notBefore. + + Exercises the -days handling in wolfCLU_certSetup: it stamps the + signed cert with a fresh notBefore of now and a notAfter of now + N + days, rather than leaving the CSR's (absent) validity in place.""" + out = _tmp("tmp_x509req_days.cert") + self._clean(out) + r = run_wolfssl("x509", "-req", "-in", self.csr, "-days", "100", + "-signkey", + os.path.join(CERTS_DIR, "server-key.pem"), + "-out", out) + self.assertEqual(r.returncode, 0, r.stderr) + + r2 = run_wolfssl("x509", "-in", out, "-text", "-noout") + self.assertEqual(r2.returncode, 0, r2.stderr) + text = r2.stdout + r2.stderr + before = _parse_time(text, "Not Before") + after = _parse_time(text, "Not After") + self.assertEqual((after - before).days, 100, + "validity window should be 100 days") + + +class TestReqCASign(unittest.TestCase): + """Test `req -CA -CAkey` CA-signing of a CSR (caSignCert path). + + This is distinct from `req -x509` self-signing: here the issuer name and + signature come from a separate CA cert/key, while the subject and public + key come from the incoming request. `-CA` implies certificate issuance, + so it works with or without `-x509`. + """ + + # Subjects baked into the request CSRs, used to assert the issued cert + # preserves the requester's subject. + RSA_SUBJ = "/O=wolfSSL/C=US/ST=MT/L=Bozeman/CN=leaf.example.com/OU=test" + ECC_SUBJ = "/O=eccleaf/C=US/CN=ecc.example.com" + + @classmethod + def setUpClass(cls): + cls.rsa_csr = _tmp("test_reqca_rsa.csr") + r = run_wolfssl("req", "-new", + "-key", os.path.join(CERTS_DIR, "server-key.pem"), + "-subj", cls.RSA_SUBJ, "-out", cls.rsa_csr) + assert r.returncode == 0, "setup RSA CSR failed: " + r.stderr + + cls.ecc_csr = _tmp("test_reqca_ecc.csr") + r = run_wolfssl("req", "-new", + "-key", os.path.join(CERTS_DIR, "server-ecc-key.pem"), + "-subj", cls.ECC_SUBJ, "-out", cls.ecc_csr) + assert r.returncode == 0, "setup ECC CSR failed: " + r.stderr + + # Requests that state their own Basic Constraints, for the tests that + # pin what the CA path does with a requested CA flag. + cls.ca_true_csr = _tmp("test_reqca_catrue.csr") + r = run_wolfssl("req", "-new", + "-key", os.path.join(CERTS_DIR, "server-key.pem"), + "-subj", cls.RSA_SUBJ, + "-addext", "basicConstraints=CA:TRUE", + "-out", cls.ca_true_csr) + assert r.returncode == 0, "setup CA:TRUE CSR failed: " + r.stderr + + cls.ca_false_csr = _tmp("test_reqca_cafalse.csr") + r = run_wolfssl("req", "-new", + "-key", os.path.join(CERTS_DIR, "server-key.pem"), + "-subj", cls.RSA_SUBJ, + "-addext", "basicConstraints=CA:FALSE", + "-out", cls.ca_false_csr) + assert r.returncode == 0, "setup CA:FALSE CSR failed: " + r.stderr + + @classmethod + def tearDownClass(cls): + _cleanup(cls.rsa_csr, cls.ecc_csr, cls.ca_true_csr, cls.ca_false_csr) + + def _clean(self, *files): + for f in files: + self.addCleanup(lambda p=f: _cleanup(p)) + + def _ca_sign(self, out_name, *extra, csr=None, ca="ca-cert.pem", + cakey="ca-key.pem"): + """Run `req -CA -CAkey -in ` and return (result, out_path). + + Deliberately omits `-x509`: `-CA` alone must issue a certificate, so + the whole suite exercises that canonical invocation.""" + out = _tmp(out_name) + self._clean(out) + r = run_wolfssl("req", + "-CA", os.path.join(CERTS_DIR, ca), + "-CAkey", os.path.join(CERTS_DIR, cakey), + "-in", csr if csr is not None else self.rsa_csr, + "-out", out, *extra) + return r, out + + def _text(self, cert, *extra): + r = run_wolfssl("x509", "-in", cert, "-text", "-noout", *extra) + self.assertEqual(r.returncode, 0, r.stderr) + return r.stdout + r.stderr + + def test_ca_sign_rsa_verifies(self): + """A CA-signed cert chains back to the signing CA.""" + r, out = self._ca_sign("reqca_rsa.pem") + self.assertEqual(r.returncode, 0, r.stderr) + + v = run_wolfssl("verify", "-CAfile", + os.path.join(CERTS_DIR, "ca-cert.pem"), out) + self.assertEqual(v.returncode, 0, v.stderr) + + def test_ca_sign_wrong_ca_does_not_verify(self): + """A cert signed by ca-cert must not verify against an unrelated CA.""" + r, out = self._ca_sign("reqca_rsa_wrongca.pem") + self.assertEqual(r.returncode, 0, r.stderr) + + v = run_wolfssl("verify", "-CAfile", + os.path.join(CERTS_DIR, "ca-ecc-cert.pem"), out) + self.assertNotEqual(v.returncode, 0) + + def test_ca_sign_issuer_is_ca_subject(self): + """Issuer of the issued cert equals the CA cert's subject.""" + r, out = self._ca_sign("reqca_issuer.pem") + self.assertEqual(r.returncode, 0, r.stderr) + + issuer = run_wolfssl("x509", "-in", out, "-issuer", "-noout") + ca_subj = run_wolfssl("x509", "-in", + os.path.join(CERTS_DIR, "ca-cert.pem"), + "-subject", "-noout") + self.assertEqual(issuer.stdout.strip(), ca_subj.stdout.strip(), + "issued cert issuer should match CA subject") + + def test_ca_sign_preserves_request_subject(self): + """Subject of the issued cert comes from the request, not the CA.""" + r, out = self._ca_sign("reqca_subject.pem") + self.assertEqual(r.returncode, 0, r.stderr) + + subj = run_wolfssl("x509", "-in", out, "-subject", "-noout") + self.assertIn("leaf.example.com", subj.stdout, + "issued cert should keep the requester's CN") + + def test_ca_sign_leaf_is_ca_false(self): + """A CA-signed leaf gets Basic Constraints CA:FALSE.""" + r, out = self._ca_sign("reqca_bc.pem") + self.assertEqual(r.returncode, 0, r.stderr) + self.assertIn("CA:FALSE", self._text(out)) + + def test_ca_sign_ca_true_request_is_downgraded_with_a_warning(self): + """A request asking for CA:TRUE is still issued as a leaf, and said so. + + The requester does not get to choose its own Basic Constraints: an + honored CA:TRUE would let anyone who can get a CSR signed mint itself + a sub-CA just by asking. OpenSSL draws the same line -- it ignores + request extensions unless -copy_extensions says otherwise -- but it + prints "ignoring any extensions in the request" rather than changing + the meaning of the request silently. The warning is the assertion that + matters here; dropping it would make the override invisible.""" + req = run_wolfssl("req", "-in", self.ca_true_csr, "-text", "-noout") + self.assertEqual(req.returncode, 0, req.stderr) + self.assertIn("CA:TRUE", req.stdout + req.stderr, + "the request under test must actually ask for CA:TRUE") + + r, out = self._ca_sign("reqca_catrue.pem", csr=self.ca_true_csr) + self.assertEqual(r.returncode, 0, r.stderr) + + text = self._text(out) + self.assertIn("CA:FALSE", text) + self.assertNotIn("CA:TRUE", text) + self.assertIn("Warning", r.stderr, + "the CA:TRUE downgrade must be reported, not silent") + self.assertIn("CA:TRUE", r.stderr) + + def test_ca_sign_ca_false_request_does_not_warn(self): + """A request that already asks for CA:FALSE gets no warning. + + Guards the warning above from the other side: it must fire on a + meaning-changing override, not on every request that happens to carry + Basic Constraints.""" + r, out = self._ca_sign("reqca_cafalse.pem", csr=self.ca_false_csr) + self.assertEqual(r.returncode, 0, r.stderr) + self.assertIn("CA:FALSE", self._text(out)) + self.assertNotIn("Warning", r.stderr) + + def test_ca_sign_plain_request_does_not_warn(self): + """Nor does a request carrying no Basic Constraints at all.""" + r, out = self._ca_sign("reqca_nobc.pem") + self.assertEqual(r.returncode, 0, r.stderr) + self.assertNotIn("Warning", r.stderr) + + def test_ca_sign_request_prints_extensions(self): + """Reqest gains extensions from CA and they are printed""" + ca = "ca-cert.pem" + cakey = "ca-key.pem" + r = run_wolfssl("req", + "-CA", os.path.join(CERTS_DIR, ca), + "-CAkey", os.path.join(CERTS_DIR, cakey), + "-in", self.rsa_csr, + "-text") + self.assertEqual(r.returncode, 0, r.stderr) + self.assertIn("CA:FALSE", r.stdout) + + def test_ca_sign_has_key_identifiers(self): + """Leaf carries its own Subject Key Id and the CA's Authority Key Id.""" + r, out = self._ca_sign("reqca_keyid.pem") + self.assertEqual(r.returncode, 0, r.stderr) + text = self._text(out) + self.assertIn("Subject Key Identifier", text) + self.assertIn("Authority Key Identifier", text) + + def test_ca_sign_ecc_ca_verifies(self): + """An ECC CA can issue and the leaf verifies against it.""" + r, out = self._ca_sign("reqca_ecc.pem", ca="ca-ecc-cert.pem", + cakey="ca-ecc-key.pem") + self.assertEqual(r.returncode, 0, r.stderr) + + v = run_wolfssl("verify", "-CAfile", + os.path.join(CERTS_DIR, "ca-ecc-cert.pem"), out) + self.assertEqual(v.returncode, 0, v.stderr) + + def test_ca_sign_ecc_request_rsa_ca(self): + """An RSA CA can certify a request carrying an ECC public key.""" + r, out = self._ca_sign("reqca_eccreq.pem", csr=self.ecc_csr) + self.assertEqual(r.returncode, 0, r.stderr) + + v = run_wolfssl("verify", "-CAfile", + os.path.join(CERTS_DIR, "ca-cert.pem"), out) + self.assertEqual(v.returncode, 0, v.stderr) + + def test_ca_sign_days_sets_validity_window(self): + """-days N produces a notAfter exactly N days after notBefore.""" + r, out = self._ca_sign("reqca_days.pem", "-days", "100") + self.assertEqual(r.returncode, 0, r.stderr) + + text = self._text(out) + before = _parse_time(text, "Not Before") + after = _parse_time(text, "Not After") + self.assertEqual((after - before).days, 100, + "validity window should be 100 days") + + def test_ca_sign_set_serial(self): + """-set_serial assigns the requested serial (regression: no leak).""" + r, out = self._ca_sign("reqca_serial.pem", "-set_serial", "12345") + self.assertEqual(r.returncode, 0, r.stderr) + + s = run_wolfssl("x509", "-in", out, "-serial", "-noout") + self.assertEqual(s.returncode, 0, s.stderr) + # 12345 == 0x3039 + self.assertEqual(s.stdout.strip(), "serial=3039") + + def test_ca_sign_default_serial_is_wide_and_unique(self): + """The generated default serial must be wide, positive and non zero. + + It used to be drawn from sizeof(int)-1 bytes, i.e. 24 bits, which + collides with about 50% probability after ~4100 certificates from the + same CA. A zero draw was worse: both signing helpers gate on + "serial > 0", so it skipped set_serialNumber entirely and the cert + carried wolfSSL's default instead.""" + seen = set() + for i in range(4): + r, out = self._ca_sign("reqca_defserial{}.pem".format(i)) + self.assertEqual(r.returncode, 0, r.stderr) + + s = run_wolfssl("x509", "-in", out, "-serial", "-noout") + self.assertEqual(s.returncode, 0, s.stderr) + hexval = s.stdout.strip().split("=", 1)[1] + + value = int(hexval, 16) + self.assertGreater(value, 0, "serial must be positive and non zero") + # 24 bits of entropy fits in 6 hex digits; require more than that + self.assertGreater(len(hexval), 8, + "serial {!r} is too narrow".format(hexval)) + # No high-bit check on the first printed octet: -serial prints the + # magnitude, like OpenSSL does, not the DER content octets. A + # positive INTEGER whose top magnitude byte has the high bit set is + # encoded with a leading 0x00 pad that is not printed, so a leading + # octet >= 0x80 here says nothing about the sign. assertGreater + # above is the positivity check. + seen.add(hexval) + + self.assertEqual(len(seen), 4, "serials repeated: {}".format(seen)) + + def test_ca_sign_sha512_digest(self): + """-sha512 is reflected in the certificate signature algorithm.""" + r, out = self._ca_sign("reqca_sha512.pem", "-sha512") + self.assertEqual(r.returncode, 0, r.stderr) + self.assertIn("sha512", self._text(out).lower()) + + def test_ca_sign_outform_der(self): + """-outform DER writes a DER cert readable back as DER.""" + r, out = self._ca_sign("reqca.der", "-outform", "DER") + self.assertEqual(r.returncode, 0, r.stderr) + + subj = run_wolfssl("x509", "-inform", "DER", "-in", out, + "-subject", "-noout") + self.assertEqual(subj.returncode, 0, subj.stderr) + self.assertIn("leaf.example.com", subj.stdout) + + def test_ca_without_cakey_fails(self): + """-CA without -CAkey has no signing key and must fail.""" + out = _tmp("reqca_nokey.pem") + self._clean(out) + r = run_wolfssl("req", + "-CA", os.path.join(CERTS_DIR, "ca-cert.pem"), + "-in", self.rsa_csr, "-out", out) + self.assertNotEqual(r.returncode, 0) + + def test_cakey_without_ca_fails(self): + """-CAkey alone must not fall through to the CSR / -x509 path.""" + out = _tmp("reqca_keyonly.pem") + self._clean(out) + r = run_wolfssl("req", + "-CAkey", os.path.join(CERTS_DIR, "ca-key.pem"), + "-in", self.rsa_csr, "-out", out) + self.assertNotEqual(r.returncode, 0) + self.assertIn("-CAkey", r.stdout + r.stderr) + + def test_x509_with_ca_is_rejected(self): + """-x509 and -CA ask for mutually exclusive artifacts. + + The signing dispatch tests -CA first, so left unchecked this issued a + CA-signed leaf and silently ignored the self-signing the operator + asked for, at exit 0.""" + out = _tmp("reqca_x509_conflict.pem") + self._clean(out) + r = run_wolfssl("req", "-in", self.rsa_csr, "-x509", + "-CA", os.path.join(CERTS_DIR, "ca-cert.pem"), + "-CAkey", os.path.join(CERTS_DIR, "ca-key.pem"), + "-out", out) + self.assertNotEqual(r.returncode, 0, + "-x509 with -CA was silently accepted") + self.assertIn("-x509", r.stdout + r.stderr) + + def test_ca_sign_rejects_request_altering_options(self): + """-CA passes the request through, so mutating options are rejected.""" + for opt in (("-subj", "/CN=other"), + ("-addext", "basicConstraints=CA:FALSE"), + ("-key", os.path.join(CERTS_DIR, "server-key.pem")), + ("-newkey", "rsa:2048")): + with self.subTest(opt=opt[0]): + r, _ = self._ca_sign("reqca_mutate.pem", *opt) + self.assertNotEqual(r.returncode, 0, + "{} must be rejected with -CA".format( + opt[0])) + + def test_ca_sign_newkey_does_not_truncate_keyout(self): + """-newkey with -CA must be rejected before -keyout is truncated.""" + keyout = _tmp("reqca_keyout.pem") + self._clean(keyout) + with open(keyout, "w") as f: + f.write("PRE-EXISTING\n") + + r, _ = self._ca_sign("reqca_newkey.pem", "-newkey", "rsa:2048", + "-keyout", keyout, "-nodes") + self.assertNotEqual(r.returncode, 0) + with open(keyout) as f: + self.assertEqual(f.read(), "PRE-EXISTING\n", + "-keyout was truncated before the run was " + "rejected") + + def test_ca_sign_non_ca_cert_fails(self): + """A CA:FALSE cert cannot be used to issue certificates.""" + r, out = self._ca_sign("reqca_notca.pem", ca="server-ecc.pem", + cakey="server-ecc-key.pem") + self.assertNotEqual(r.returncode, 0) + + @unittest.skipUnless(HAS_OPENSSL, "needs openssl to mark an EKU critical") + def test_ca_sign_non_ca_cert_with_critical_eku_fails(self): + """A CA:FALSE cert with a *critical* EKU must still not issue. + + wolfSSL_X509_check_ca() answers 1 for a real CA but also 4 for a leaf + that merely carries a critical extendedKeyUsage, so a `< 1` test lets + this shape through. Built with openssl because wolfCLU does not emit a + critical EKU itself.""" + conf = _tmp("reqca_criteku.cnf") + ca_pem = _tmp("reqca_criteku_ca.pem") + out = _tmp("reqca_criteku_out.pem") + self._clean(conf, ca_pem, out) + + with open(conf, "w") as f: + f.write("[ req ]\ndistinguished_name = dn\nprompt = no\n" + "[ dn ]\ncountryName = US\ncommonName = crit-eku-leaf\n" + "[ v3_leaf ]\nbasicConstraints = CA:FALSE\n" + "extendedKeyUsage = critical, serverAuth\n") + + key = os.path.join(CERTS_DIR, "server-key.pem") + gen = subprocess.run( + ["openssl", "req", "-new", "-x509", "-config", conf, + "-extensions", "v3_leaf", "-key", key, "-days", "365", + "-out", ca_pem], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + universal_newlines=True) + self.assertEqual(gen.returncode, 0, gen.stdout) + + r = run_wolfssl("req", "-CA", ca_pem, "-CAkey", key, + "-in", self.rsa_csr, "-out", out) + self.assertNotEqual(r.returncode, 0, + "a CA:FALSE cert with a critical EKU must not " + "be accepted as an issuer") + self.assertFalse(os.path.exists(out), + "no certificate should have been written") + + def test_ca_sign_missing_ca_file_fails(self): + """A nonexistent -CA file is rejected.""" + out = _tmp("reqca_missing.pem") + self._clean(out) + r = run_wolfssl("req", + "-CA", _tmp("no_such_ca.pem"), + "-CAkey", os.path.join(CERTS_DIR, "ca-key.pem"), + "-in", self.rsa_csr, "-out", out) + self.assertNotEqual(r.returncode, 0) + + def test_ca_sign_tampered_request_fails(self): + """A request with a corrupted signature must not be certified.""" + der_csr = _tmp("test_reqca_tamper.csr") + bad_csr = _tmp("test_reqca_tamper_bad.csr") + out = _tmp("reqca_tamper.pem") + self._clean(der_csr, bad_csr, out) + + r = run_wolfssl("req", "-new", + "-key", os.path.join(CERTS_DIR, "server-key.pem"), + "-subj", self.RSA_SUBJ, + "-outform", "DER", "-out", der_csr) + self.assertEqual(r.returncode, 0, r.stderr) + _flip_last_der_byte(der_csr, bad_csr) + + r = run_wolfssl("req", + "-CA", os.path.join(CERTS_DIR, "ca-cert.pem"), + "-CAkey", os.path.join(CERTS_DIR, "ca-key.pem"), + "-inform", "DER", "-in", bad_csr, "-out", out) + self.assertNotEqual(r.returncode, 0) + + def test_ca_sign_without_in_request_fails(self): + """-CA with no -in has nothing to certify and must fail. + + Unlike -x509, the CA path never invents a subject/key: without a + request there is no public key to certify, so the run is rejected + rather than falling through to an interactive name prompt.""" + out = _tmp("reqca_noin.pem") + self._clean(out) + r = run_wolfssl("req", + "-CA", os.path.join(CERTS_DIR, "ca-cert.pem"), + "-CAkey", os.path.join(CERTS_DIR, "ca-key.pem"), + "-out", out) + self.assertNotEqual(r.returncode, 0) + + def test_ca_sign_missing_ca_key_file_fails(self): + """A nonexistent -CAkey file is rejected.""" + out = _tmp("reqca_missing_key.pem") + self._clean(out) + r = run_wolfssl("req", + "-CA", os.path.join(CERTS_DIR, "ca-cert.pem"), + "-CAkey", _tmp("no_such_ca_key.pem"), + "-in", self.rsa_csr, "-out", out) + self.assertNotEqual(r.returncode, 0) + + def _write_junk(self, name): + """Write a file that exists but holds no PEM object.""" + path = _tmp(name) + self._clean(path) + with open(path, "w", encoding="utf-8", newline="\n") as f: + f.write("this is not a PEM object\n") + return path + + def test_ca_sign_unparsable_ca_cert_fails(self): + """A -CA file that exists but is not a certificate is rejected. + + Distinct from the missing-file case: the access() check passes and the + failure has to come from the PEM read inside caSignCert.""" + junk = self._write_junk("reqca_junk_cert.pem") + out = _tmp("reqca_junkcert.pem") + self._clean(out) + r = run_wolfssl("req", "-CA", junk, + "-CAkey", os.path.join(CERTS_DIR, "ca-key.pem"), + "-in", self.rsa_csr, "-out", out) + self.assertNotEqual(r.returncode, 0) + self.assertIn("Unable to read ca cert", r.stdout + r.stderr) + + def test_ca_sign_unparsable_ca_key_fails(self): + """A -CAkey file that exists but is not a key is rejected. + + The error must name the key, not the cert: both reads happen in + caSignCert and the diagnostics are reported separately.""" + junk = self._write_junk("reqca_junk_key.pem") + out = _tmp("reqca_junkkey.pem") + self._clean(out) + r = run_wolfssl("req", + "-CA", os.path.join(CERTS_DIR, "ca-cert.pem"), + "-CAkey", junk, + "-in", self.rsa_csr, "-out", out) + self.assertNotEqual(r.returncode, 0) + self.assertIn("Unable to read ca key", r.stdout + r.stderr) + + def test_ca_sign_mismatched_ca_key_is_rejected(self): + """A -CAkey that is not the -CA cert's key fails the issue. + + Signing would otherwise succeed and produce a certificate carrying the + CA's issuer name over a signature that does not chain to it -- valid + looking, and useless. The pair is checked before signing, so the error + names the option that was wrong.""" + r, _ = self._ca_sign("reqca_wrongkey.pem", cakey="server-key.pem") + self.assertNotEqual(r.returncode, 0, r.stderr) + self.assertIn("does not match", r.stdout + r.stderr) + + def test_ca_sign_authority_key_id_matches_ca_subject_key_id(self): + """The leaf's Authority Key Id equals the CA cert's Subject Key Id. + + This is the link that lets a verifier find the issuer, so the value + must be copied from the CA -- asserting only that the extension is + present would pass on an empty or self-derived id.""" + r, out = self._ca_sign("reqca_akid.pem") + self.assertEqual(r.returncode, 0, r.stderr) + + ca_ski = _ext_value( + self._text(os.path.join(CERTS_DIR, "ca-cert.pem")), + "X509v3 Subject Key Identifier") + self.assertIsNotNone(ca_ski, "CA cert has no Subject Key Identifier") + + leaf_akid = _ext_value(self._text(out), + "X509v3 Authority Key Identifier") + self.assertIsNotNone(leaf_akid, "leaf has no Authority Key Identifier") + self.assertEqual(leaf_akid, ca_ski, + "leaf AKID should be the CA's SKID") + + def test_ca_sign_subject_key_id_is_leafs_own(self): + """The leaf's Subject Key Id is derived from its own public key. + + It must differ from the CA's, otherwise the id was copied from the + issuer instead of computed from the certified request key.""" + r, out = self._ca_sign("reqca_skid.pem") + self.assertEqual(r.returncode, 0, r.stderr) + + text = self._text(out) + leaf_skid = _ext_value(text, "X509v3 Subject Key Identifier") + leaf_akid = _ext_value(text, "X509v3 Authority Key Identifier") + self.assertIsNotNone(leaf_skid, "leaf has no Subject Key Identifier") + self.assertNotEqual(leaf_skid, leaf_akid, + "leaf SKID should come from its own key, not the " + "CA's") + + def test_ca_sign_produces_v3_certificate(self): + """The issued cert is X.509 v3. + + The incoming request is v1; issuance has to bump the version, since + the extensions the CA adds are only legal in a v3 certificate.""" + r, out = self._ca_sign("reqca_version.pem") + self.assertEqual(r.returncode, 0, r.stderr) + self.assertIn("Version: 3 (0x2)", self._text(out)) + + def test_ca_sign_default_digest_is_sha256(self): + """With no digest option the CA signs with SHA-256.""" + r, out = self._ca_sign("reqca_defaultmd.pem") + self.assertEqual(r.returncode, 0, r.stderr) + self.assertIn("sha256", self._text(out).lower()) + + def test_ca_sign_inform_der_request(self): + """A DER-encoded request is accepted via -inform DER and certified.""" + der_csr = _tmp("test_reqca_in.der") + out = _tmp("reqca_derin.pem") + self._clean(der_csr, out) + + r = run_wolfssl("req", "-inform", "pem", "-outform", "der", + "-in", self.rsa_csr, "-out", der_csr) + self.assertEqual(r.returncode, 0, r.stderr) + + r = run_wolfssl("req", + "-CA", os.path.join(CERTS_DIR, "ca-cert.pem"), + "-CAkey", os.path.join(CERTS_DIR, "ca-key.pem"), + "-inform", "DER", "-in", der_csr, "-out", out) + self.assertEqual(r.returncode, 0, r.stderr) + + v = run_wolfssl("verify", "-CAfile", + os.path.join(CERTS_DIR, "ca-cert.pem"), out) + self.assertEqual(v.returncode, 0, v.stderr) + + def test_ca_sign_ecc_ca_ecc_request(self): + """An ECC CA certifying an ECC request produces a chaining leaf.""" + r, out = self._ca_sign("reqca_ecc_ecc.pem", csr=self.ecc_csr, + ca="ca-ecc-cert.pem", cakey="ca-ecc-key.pem") + self.assertEqual(r.returncode, 0, r.stderr) + + v = run_wolfssl("verify", "-CAfile", + os.path.join(CERTS_DIR, "ca-ecc-cert.pem"), out) + self.assertEqual(v.returncode, 0, v.stderr) + + subj = run_wolfssl("x509", "-in", out, "-subject", "-noout") + self.assertIn("ecc.example.com", subj.stdout) + + def test_ca_sign_writes_pem_to_stdout_without_out(self): + """With no -out the issued cert goes to stdout as PEM.""" + r = run_wolfssl("req", + "-CA", os.path.join(CERTS_DIR, "ca-cert.pem"), + "-CAkey", os.path.join(CERTS_DIR, "ca-key.pem"), + "-in", self.rsa_csr) + self.assertEqual(r.returncode, 0, r.stderr) + self.assertIn("-----BEGIN CERTIFICATE-----", r.stdout) + self.assertIn("-----END CERTIFICATE-----", r.stdout) + + def test_ca_sign_noout_suppresses_certificate(self): + """-noout still signs but emits no certificate.""" + r = run_wolfssl("req", + "-CA", os.path.join(CERTS_DIR, "ca-cert.pem"), + "-CAkey", os.path.join(CERTS_DIR, "ca-key.pem"), + "-in", self.rsa_csr, "-noout") + self.assertEqual(r.returncode, 0, r.stderr) + self.assertNotIn("BEGIN CERTIFICATE", r.stdout) + + def test_ca_sign_text_prints_a_certificate(self): + """-text on the CA path renders a certificate, not a request. + + `-CA` issues a cert, so -text must use the certificate printer: it + labels the output "Certificate:", reports v3, and shows the Issuer, + none of which a request printer does. Both request printers hardcode + the "Certificate Request:" header, so this is gated on the isCSR flag + rather than on which printer the build provides.""" + r = run_wolfssl("req", + "-CA", os.path.join(CERTS_DIR, "ca-cert.pem"), + "-CAkey", os.path.join(CERTS_DIR, "ca-key.pem"), + "-in", self.rsa_csr, "-days", "365", + "-text", "-noout") + self.assertEqual(r.returncode, 0, r.stderr) + self.assertIn("Certificate:", r.stdout) + self.assertNotIn("Certificate Request:", r.stdout) + self.assertIn("Version: 3 (0x2)", r.stdout) + self.assertIn("Issuer:", r.stdout) + self.assertIn("leaf.example.com", r.stdout) + + def test_req_text_still_prints_a_request(self): + """Without -CA/-x509 the same -text path still prints a request. + + Guards the isCSR flag from the opposite direction: routing certs to + the certificate printer must not drag plain CSR output along.""" + r = run_wolfssl("req", "-in", self.rsa_csr, "-text", "-noout") + self.assertEqual(r.returncode, 0, r.stderr) + self.assertIn("Certificate Request:", r.stdout) + self.assertIn("leaf.example.com", r.stdout) + + def test_x509_self_signed_text_prints_a_certificate(self): + """-x509 -text also renders a certificate rather than a request.""" + r = run_wolfssl("req", "-new", "-x509", + "-key", os.path.join(CERTS_DIR, "server-key.pem"), + "-subj", "/O=wolfSSL/C=US/CN=selfsigned.example.com", + "-days", "365", "-text", "-noout") + self.assertEqual(r.returncode, 0, r.stderr) + self.assertIn("Certificate:", r.stdout) + self.assertNotIn("Certificate Request:", r.stdout) + self.assertIn("selfsigned.example.com", r.stdout) + + +class TestReqX509SelfSign(unittest.TestCase): + """`req -x509` self-signing (the selfSignCert path). + + TestReqCASign pins -days and -set_serial against caSignCert, but + selfSignCert carries its own copy of both blocks, so passing there says + nothing about this path. These mirror the -CA versions. + """ + + SUBJ = "/O=wolfSSL/C=US/CN=selfsign.example.com" + + def _clean(self, *files): + for f in files: + self.addCleanup(lambda p=f: _cleanup(p)) + + def _self_sign(self, out_name, *extra): + """Run `req -new -x509` and return (result, out_path).""" + out = _tmp(out_name) + self._clean(out) + r = run_wolfssl("req", "-new", "-x509", + "-key", os.path.join(CERTS_DIR, "server-key.pem"), + "-subj", self.SUBJ, "-out", out, *extra) + return r, out + + def test_x509_set_serial(self): + """-x509 -set_serial assigns the requested serial.""" + r, out = self._self_sign("reqx509_serial.pem", "-set_serial", "12345") + self.assertEqual(r.returncode, 0, r.stderr) + + s = run_wolfssl("x509", "-in", out, "-serial", "-noout") + self.assertEqual(s.returncode, 0, s.stderr) + # 12345 == 0x3039 + self.assertEqual(s.stdout.strip(), "serial=3039") + + def test_x509_default_serial_is_wide_and_unique(self): + """The default serial on the self-signed path is wide and non zero. + + Same reasoning as test_ca_sign_default_serial_is_wide_and_unique: a + 24-bit draw collides after a few thousand certs, and a zero draw would + skip set_serialNumber entirely because this path also gates on + "serial > 0".""" + seen = set() + for i in range(4): + r, out = self._self_sign("reqx509_defserial{}.pem".format(i)) + self.assertEqual(r.returncode, 0, r.stderr) + + s = run_wolfssl("x509", "-in", out, "-serial", "-noout") + self.assertEqual(s.returncode, 0, s.stderr) + hexval = s.stdout.strip().split("=", 1)[1] + + value = int(hexval, 16) + self.assertGreater(value, 0, "serial must be positive and non zero") + # 24 bits of entropy fits in 6 hex digits; require more than that + self.assertGreater(len(hexval), 8, + "serial {!r} is too narrow".format(hexval)) + seen.add(hexval) + + self.assertEqual(len(seen), 4, "serials repeated: {}".format(seen)) + + def test_x509_days_sets_validity_window(self): + """-x509 -days N produces a notAfter exactly N days after notBefore.""" + r, out = self._self_sign("reqx509_days.pem", "-days", "100") + self.assertEqual(r.returncode, 0, r.stderr) + + t = run_wolfssl("x509", "-in", out, "-text", "-noout") + self.assertEqual(t.returncode, 0, t.stderr) + text = t.stdout + t.stderr + before = _parse_time(text, "Not Before") + after = _parse_time(text, "Not After") + self.assertEqual((after - before).days, 100, + "validity window should be 100 days") + class TestX509ReqHashAlgorithms(unittest.TestCase): """Test hash algorithm options for x509 -req.""" @@ -806,6 +1713,51 @@ def test_default_ca_true(self): self.assertEqual(r2.returncode, 0, r2.stderr) self.assertIn("CA:TRUE", r2.stdout) + def test_basic_constraints_critical_position_independent(self): + """"critical" must parse anywhere in the value, not just at the ends. + + The value was tokenized on ':' while the keyword branches consumed + their value with ',', so the two delimiters crossed: after CA the + next token scanned past the comma and came out as "critical,pathlen", + which matched no keyword and rejected an otherwise valid value.""" + for value in ("critical,CA:TRUE,pathlen:1", + "CA:TRUE,critical,pathlen:1", + "CA:TRUE,pathlen:1,critical", + "CA:TRUE , critical , pathlen:1"): + with self.subTest(value=value): + tmp = _tmp("test_req_bc_crit.csr") + self._clean(tmp) + r = run_wolfssl("req", "-new", + "-key", os.path.join(CERTS_DIR, + "server-key.pem"), + "-subj", "/CN=bc.example.com", + "-addext", "basicConstraints=" + value, + "-out", tmp) + self.assertEqual(r.returncode, 0, r.stderr) + + t = run_wolfssl("req", "-in", tmp, "-text", "-noout") + self.assertEqual(t.returncode, 0, t.stderr) + self.assertIn("CA:TRUE", t.stdout) + self.assertIn("pathlen:1", t.stdout) + self.assertIn("Basic Constraints: critical", t.stdout) + + def test_basic_constraints_bad_value_names_the_value(self): + """A bad value must be reported as the value, not the keyword.""" + for value, expect in (("CA:MAYBE", "MAYBE"), + ("pathlen:zzz", "zzz"), + ("bogus:1", "bogus")): + with self.subTest(value=value): + tmp = _tmp("test_req_bc_bad.csr") + self._clean(tmp) + r = run_wolfssl("req", "-new", + "-key", os.path.join(CERTS_DIR, + "server-key.pem"), + "-subj", "/CN=bc.example.com", + "-addext", "basicConstraints=" + value, + "-out", tmp) + self.assertNotEqual(r.returncode, 0) + self.assertIn(expect, r.stdout + r.stderr) + class TestReqFIPS(unittest.TestCase): """FIPS-conditional tests.""" @@ -854,6 +1806,26 @@ def test_newkey_keyout_with_passout(self): "-passin", "pass:123456789wolfssl") self.assertEqual(r2.returncode, 0, r2.stderr) + def test_empty_passout_is_rejected(self): + """"-passout pass:" must be rejected as an empty password. + + Pins the outcome rather than the mechanism: writeOutPkey() used to + reach this by handing the stdin prompt a capacity of 0 (the password + length and the buffer capacity shared one variable), which read + nothing and fell into the same error.""" + if is_fips(): + self.skipTest("FIPS build") + tmp = _tmp("test_req_emptypass.cert") + key = _tmp("test_req_emptypass.pem") + self._clean(tmp, key) + r = run_wolfssl("req", "-new", "-newkey", "rsa:2048", + "-keyout", key, "-config", self.conf_file, + "-out", tmp, "-passout", "pass:", + stdin_data="unused password\n") + self.assertNotEqual(r.returncode, 0, + "an empty -passout must not be accepted") + self.assertIn("password", (r.stdout + r.stderr).lower()) + def test_newkey_with_passout_keyout(self): """req -newkey rsa:2048 -keyout with -passout stdin.""" if is_fips(): @@ -898,9 +1870,6 @@ def _test_algo(self, algo_flag): def test_rsa(self): self._test_algo("rsa") - def test_ed25519(self): - self._test_algo("ed25519") - def test_sha(self): self._test_algo("sha") @@ -916,6 +1885,50 @@ def test_sha384(self): def test_sha512(self): self._test_algo("sha512") + def test_ecc_and_ed25519_keygen_rejected(self): + """-ecc/-ed25519 keygen is a stub, it must fail loudly not silently.""" + for algo in ("-ecc", "-ed25519"): + with self.subTest(algo=algo): + r = run_wolfssl("req", "-new", algo, "-subj", "/CN=test") + self.assertNotEqual(r.returncode, 0, + "{} keygen must be rejected".format(algo)) + self.assertIn("not yet supported", r.stdout + r.stderr) + + def test_ecc_and_ed25519_accepted_with_supplied_key(self): + """The flags only ask for keygen; with -key there is nothing to + generate, so the request must still be built.""" + for algo in ("-ecc", "-ed25519"): + with self.subTest(algo=algo): + out = _tmp("test_req_algo_key{}.csr".format(algo)) + self._clean(out) + r = run_wolfssl("req", "-new", algo, + "-key", os.path.join(CERTS_DIR, + "server-ecc-key.pem"), + "-subj", "/C=US/CN=test", "-out", out) + self.assertEqual(r.returncode, 0, r.stdout + r.stderr) + self.assertTrue(os.path.exists(out)) + + def test_newkey_rsa_accepts_standard_sizes(self): + """rsa:2048/3072/4096 are the sizes -newkey generates.""" + for bits in ("2048", "3072", "4096"): + with self.subTest(bits=bits): + out = _tmp("test_req_newkey_{}.csr".format(bits)) + self._clean(out) + r = run_wolfssl("req", "-new", "-newkey", + "rsa:" + bits, "-nodes", + "-subj", "/C=US/CN=test", "-out", out) + self.assertEqual(r.returncode, 0, r.stdout + r.stderr) + + def test_newkey_rsa_rejects_other_sizes(self): + """Anything outside the three standard steps is rejected up front, + rather than being coerced by XATOI and handed to keygen.""" + for bits in ("-5", "0", "512", "1024", "2049", "20488", "abc", ""): + with self.subTest(bits=bits): + r = run_wolfssl("req", "-new", "-newkey", "rsa:" + bits, + "-nodes", "-subj", "/C=US/CN=test") + self.assertNotEqual(r.returncode, 0, + "rsa:{} must be rejected".format(bits)) + class TestReqAltNamesFullSkip(unittest.TestCase): @@ -1163,5 +2176,632 @@ def test_challenge_password_attribute(self): self.assertEqual(r.returncode, 0, r.stderr) + + +class TestReqAddExtNames(unittest.TestCase): + """-addext support for each extension name wolfCLU recognizes. + + Covers the name=value dispatch in wolfCLU_parseAddExt and the per-NID + cases it routes to in wolfCLU_parseExtension.""" + + # names accepted on the left of the '=' that wolfSSL cannot store on a + # WOLFSSL_X509, so wolfCLU has to report them instead of dropping them + UNSUPPORTED = [ + "issuerAltName=DNS:a.example.com", + "nameConstraints=permitted;DNS:a.example.com", + "policyConstraints=requireExplicitPolicy:0", + "policyMappings=1.2.3:1.2.4", + "inhibitAnyPolicy=0", + ] + + def _clean(self, *files): + for f in files: + self.addCleanup(lambda p=f: _cleanup(p)) + + def _addext_text(self, addexts, crt_name): + """Self-sign a cert with each string in `addexts` passed as its own + -addext option, and return the `x509 -text` rendering of the result.""" + crt = _tmp(crt_name) + self._clean(crt) + args = ["req", "-new", "-days", "3650", + "-key", os.path.join(CERTS_DIR, "server-key.pem"), + "-subj", "CN=test"] + for ext in addexts: + args += ["-addext", ext] + args += ["-x509", "-out", crt] + r = run_wolfssl(*args) + self.assertEqual(r.returncode, 0, r.stderr) + + r2 = run_wolfssl("x509", "-in", crt, "-text", "-noout") + self.assertEqual(r2.returncode, 0, r2.stderr) + return r2.stdout + + def _addext_fails(self, addext, crt_name): + crt = _tmp(crt_name) + self._clean(crt) + r = run_wolfssl("req", "-new", "-days", "3650", + "-key", os.path.join(CERTS_DIR, "server-key.pem"), + "-subj", "CN=test", + "-addext", addext, + "-x509", "-out", crt) + self.assertNotEqual(r.returncode, 0, + "expected failure for -addext {!r}".format(addext)) + + def test_addext_basic_constraints(self): + """-addext basicConstraints=CA:TRUE marks the cert a CA.""" + text = self._addext_text(["basicConstraints=CA:TRUE"], + "test_addext_bc.crt") + self.assertIn("CA:TRUE", text) + + def test_addext_subject_key_identifier(self): + """-addext subjectKeyIdentifier=hash derives the SKID from the key.""" + text = self._addext_text(["subjectKeyIdentifier=hash"], + "test_addext_skid.crt") + skid = _ext_value(text, "X509v3 Subject Key Identifier") + self.assertIsNotNone(skid, "SKID not found in cert output") + # a SHA-1 hash rendered as colon separated hex + self.assertEqual(len(skid.split(":")), 20, "got {!r}".format(skid)) + + def test_addext_subj_alt_name_critical_prefix_token_fails(self): + """A token merely starting with "critical" is reported, not dropped.""" + self._addext_fails("subjectAltName=criticalDNS:mixed.example.com", + "test_addext_subjan_crit_prefix_fail.crt") + + def test_addext_subj_alt_name_critical_prefix_token_passes(self): + """A token merely starting with "critical," is accepted""" + text = self._addext_text( + ["subjectAltName=critical,DNS:mixed.example.com"], + "test_addext_subjan_crit_prefix_pass.crt") + self.assertIn("DNS:mixed.example.com", text) + self.assertIn("critical", text) + + def test_addext_authority_key_identifier_always_suffix(self): + """The OpenSSL ":always" suffix on keyid is accepted.""" + text = self._addext_text(["authorityKeyIdentifier=keyid:always"], + "test_addext_akid_always.crt") + self.assertIn("X509v3 Authority Key Identifier", text) + + def test_addext_key_usage(self): + """-addext keyUsage=... sets each named usage bit.""" + text = self._addext_text( + ["keyUsage=digitalSignature,keyEncipherment"], + "test_addext_ku.crt") + usage = _ext_value(text, "X509v3 Key Usage") + self.assertIsNotNone(usage, "key usage not found in cert output") + self.assertIn("Digital Signature", usage) + self.assertIn("Key Encipherment", usage) + + def test_addext_extended_key_usage(self): + """-addext extendedKeyUsage=... sets every named purpose.""" + text = self._addext_text( + ["extendedKeyUsage=serverAuth,clientAuth,OCSPSigning"], + "test_addext_eku.crt") + eku = _ext_value(text, "X509v3 Extended Key Usage") + self.assertIsNotNone(eku, "extended key usage not found in output") + self.assertIn("TLS Web Server Authentication", eku) + self.assertIn("TLS Web Client Authentication", eku) + self.assertIn("OCSP Signing", eku) + + def test_addext_key_usage_unknown_value_fails(self): + """An unrecognized keyUsage bit is rejected, not silently dropped. + + keyUsage and extendedKeyUsage validate the same way; a typo in either + must fail rather than issue a cert missing the requested bit.""" + self._addext_fails("keyUsage=digitalSignature,bogusUsage", + "test_addext_ku_bad.crt") + + def test_addext_key_usage_empty_fails(self): + """keyUsage with no bits set is rejected.""" + self._addext_fails("keyUsage=", "test_addext_ku_empty.crt") + + def test_addext_key_usage_tolerates_spaces(self): + """Spaces around the comma separators are ordinary config style.""" + text = self._addext_text( + ["keyUsage=digitalSignature , keyEncipherment"], + "test_addext_ku_spaces.crt") + usage = _ext_value(text, "X509v3 Key Usage") + self.assertIsNotNone(usage, "key usage not found in cert output") + self.assertIn("Digital Signature", usage) + self.assertIn("Key Encipherment", usage) + + def test_addext_extended_key_usage_tolerates_spaces(self): + """Same for extendedKeyUsage, which used to reject a trailing space.""" + text = self._addext_text( + ["extendedKeyUsage=serverAuth , clientAuth"], + "test_addext_eku_spaces.crt") + eku = _ext_value(text, "X509v3 Extended Key Usage") + self.assertIsNotNone(eku, "extended key usage not found in output") + self.assertIn("TLS Web Server Authentication", eku) + self.assertIn("TLS Web Client Authentication", eku) + + def test_addext_basic_constraints_pathlen_round_trips(self): + """pathlen must be emitted as the value asked for. + + A pathLenConstraint larger than requested permits more intermediate + CAs than the operator intended, so a wrong value is worse than none. + pathlen:0 is the case that matters most and the one most likely to + regress, since 0 differs from the DER encoding length.""" + for n in ("0", "1", "5"): + with self.subTest(pathlen=n): + text = self._addext_text( + ["basicConstraints=CA:TRUE,pathlen:" + n], + "test_addext_pathlen{}.crt".format(n)) + bc = _ext_value(text, "X509v3 Basic Constraints") + self.assertIsNotNone(bc, "basic constraints not found") + self.assertIn("pathlen:" + n, bc) + + def test_addext_basic_constraints_pathlen_either_order(self): + """pathlen before CA parses the same as CA before pathlen.""" + text = self._addext_text(["basicConstraints=pathlen:2,CA:TRUE"], + "test_addext_pathlen_order.crt") + bc = _ext_value(text, "X509v3 Basic Constraints") + self.assertIsNotNone(bc, "basic constraints not found") + self.assertIn("CA:TRUE", bc) + self.assertIn("pathlen:2", bc) + + def test_addext_basic_constraints_critical_either_position(self): + """critical must not swallow the clause it follows. + + The critical key word is detected anywhere in the value, but only a + leading one is stepped over. Skipping to the first comma regardless + of where it appeared consumed the "CA:TRUE" of "CA:TRUE,critical" and + silently emitted CA:FALSE for a cert the operator asked to be a CA.""" + for value in ("critical,CA:TRUE", "CA:TRUE,critical"): + with self.subTest(value=value): + text = self._addext_text( + ["basicConstraints=" + value], + "test_addext_bc_crit_{}.crt".format( + value.index("critical"))) + bc = _ext_value(text, "X509v3 Basic Constraints") + self.assertIsNotNone(bc, "basic constraints not found") + self.assertIn("CA:TRUE", bc) + self.assertIn("critical", text) + + def test_addext_basic_constraints_critical_trailing_with_pathlen(self): + """A trailing critical leaves both CA and pathlen intact.""" + text = self._addext_text( + ["basicConstraints=CA:TRUE,pathlen:2,critical"], + "test_addext_bc_crit_pathlen.crt") + bc = _ext_value(text, "X509v3 Basic Constraints") + self.assertIsNotNone(bc, "basic constraints not found") + self.assertIn("CA:TRUE", bc) + self.assertIn("pathlen:2", bc) + + def test_addext_basic_constraints_critical_prefix_token_fails(self): + """A token merely starting with "critical" is reported, not dropped.""" + self._addext_fails("basicConstraints=criticalfoo,CA:TRUE", + "test_addext_bc_crit_prefix.crt") + + def test_addext_basic_constraints_pathlen_out_of_range_fails(self): + """A pathlen that cannot fit the encoder's byte is rejected.""" + self._addext_fails("basicConstraints=CA:TRUE,pathlen:999", + "test_addext_pathlen_big.crt") + + def test_addext_authority_key_identifier_issuer_only_is_skipped(self): + """authorityKeyIdentifier=issuer is skipped, not an error. + + The issuer name/serial form needs the issuing certificate, so wolfCLU + cannot build it -- but OpenSSL accepts the value, and the message says + "Skipping", so the command must still succeed.""" + text = self._addext_text(["authorityKeyIdentifier=issuer"], + "test_addext_akid_issuer.crt") + self.assertNotIn("X509v3 Authority Key Identifier", text) + + def test_addext_authority_key_identifier_unknown_value_fails(self): + """A typo'd authorityKeyIdentifier key word is rejected. + + "keyidalways" must not be accepted by the prefix match that exists to + allow the "keyid:always" qualifier.""" + self._addext_fails("authorityKeyIdentifier=keyidalways", + "test_addext_akid_typo.crt") + + def test_addext_subject_key_identifier_unknown_value_fails(self): + """A typo'd subjectKeyIdentifier key word is rejected, not dropped. + + Every failure path in the skid parser returns NULL, so a fail-open + here emits a certificate silently missing the skid at exit 0.""" + self._addext_fails("subjectKeyIdentifier=typo", + "test_addext_skid_typo.crt") + + def test_addext_extended_key_usage_unknown_value_fails(self): + """An unrecognized extendedKeyUsage purpose is rejected, not dropped.""" + self._addext_fails("extendedKeyUsage=serverAuth,bogusUsage", + "test_addext_eku_bad.crt") + + def test_addext_extended_key_usage_empty_fails(self): + """extendedKeyUsage with no purposes is rejected.""" + self._addext_fails("extendedKeyUsage=", "test_addext_eku_empty.crt") + + def test_addext_unsupported_extensions_fail(self): + """Extensions wolfSSL cannot store must error, not silently vanish.""" + for i, ext in enumerate(self.UNSUPPORTED): + with self.subTest(addext=ext): + self._addext_fails(ext, + "test_addext_unsup{}.crt".format(i)) + + def test_addext_unsupported_error_names_the_extension(self): + """The error must name the extension, not print a raw nid. + + wolfSSL's extension NID_* macros are its internal OID sums, so + NID_issuer_alt_name is 0x7fed1daa rather than OpenSSL's 86, and three + of these five are absent from its object table -- neither nid2ln nor + nid2obj can name them. Printing the number gave the operator a ten + digit value matching nothing they could look up.""" + for i, ext in enumerate(self.UNSUPPORTED): + name = ext.split("=")[0] + with self.subTest(addext=name): + crt = _tmp("test_addext_unsup_name{}.crt".format(i)) + self._clean(crt) + r = run_wolfssl("req", "-new", + "-key", os.path.join(CERTS_DIR, + "server-key.pem"), + "-subj", "CN=test", "-addext", ext, + "-x509", "-out", crt) + self.assertNotEqual(r.returncode, 0) + out = r.stdout + r.stderr + self.assertIn(name, out, + "error did not name the extension: " + out) + + def test_addext_truncated_name_fails(self): + """A name shorter than a supported one must not match it. + + The comparison spans the name plus its '=', so "keyUsag=" differs + from "keyUsage=" at the '=' rather than matching as a prefix.""" + self._addext_fails("keyUsag=digitalSignature", + "test_addext_truncated.crt") + +EXT_SECTION_CONF = """\ +[ ext_all ] +basicConstraints = CA:TRUE +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always,issuer:always +keyUsage = critical,keyCertSign,cRLSign +extendedKeyUsage = serverAuth,clientAuth +subjectAltName = DNS:inline.example.com,IP:10.0.0.1 + +[ ext_section_san ] +subjectAltName = @alt_names + +[ alt_names ] +DNS.1 = section.example.com +IP.1 = 192.0.2.7 + +[ ext_bad_eku ] +extendedKeyUsage = serverAuth,bogusUsage +""" + + +class TestX509ExtFileExtensionTypes(unittest.TestCase): + """Extensions applied from a config section by x509 -req -extfile. + + This is the wolfCLU_setExtensions path, as opposed to the -addext path + above; both funnel into wolfCLU_parseExtension.""" + + @classmethod + def setUpClass(cls): + cls.conf = _tmp("test_extfile_types.conf") + with open(cls.conf, "w", encoding="utf-8", newline="\n") as f: + f.write(EXT_SECTION_CONF) + cls.csr = _tmp("test_extfile_types.csr") + r = run_wolfssl("req", "-new", + "-key", os.path.join(CERTS_DIR, "server-key.pem"), + "-subj", "CN=test", "-out", cls.csr) + assert r.returncode == 0, "setup CSR creation failed: " + r.stderr + + @classmethod + def tearDownClass(cls): + _cleanup(cls.conf, cls.csr) + + def _clean(self, *files): + for f in files: + self.addCleanup(lambda p=f: _cleanup(p)) + + def _sign_with_section(self, section, crt_name): + """Self-sign the shared CSR applying `section` from the config, and + return the `x509 -text` rendering of the issued cert.""" + crt = _tmp(crt_name) + self._clean(crt) + r = run_wolfssl("x509", "-req", "-in", self.csr, "-days", "3650", + "-extfile", self.conf, "-extensions", section, + "-signkey", + os.path.join(CERTS_DIR, "server-key.pem"), + "-out", crt) + self.assertEqual(r.returncode, 0, r.stderr) + + r2 = run_wolfssl("x509", "-in", crt, "-text", "-noout") + self.assertEqual(r2.returncode, 0, r2.stderr) + return r2.stdout + + def test_extfile_without_req_is_reported(self): + """Applying extensions mutates the cert, and a mutated cert is only + written out when -req makes it re-signed. Without -req the extensions + were applied in memory, the original DER was written, and the command + exited 0 -- the same situation -days already refused.""" + crt = _tmp("test_extfile_noreq.crt") + self._clean(crt) + r = run_wolfssl("x509", "-in", os.path.join(CERTS_DIR, "ca-cert.pem"), + "-extfile", self.conf, "-extensions", "ext_all", + "-out", crt) + self.assertNotEqual(r.returncode, 0, + "-extfile without -req must not silently drop " + "the extensions") + self.assertIn("-req", r.stdout + r.stderr) + + def test_extfile_extended_key_usage(self): + """extendedKeyUsage is read from the config section. + + wolfCLU_setExtensions has to look the key up for the extension to be + reachable from a config file at all.""" + text = self._sign_with_section("ext_all", "test_extfile_eku.crt") + eku = _ext_value(text, "X509v3 Extended Key Usage") + self.assertIsNotNone(eku, "extended key usage not found in output") + self.assertIn("TLS Web Server Authentication", eku) + self.assertIn("TLS Web Client Authentication", eku) + + def test_extfile_authority_key_identifier_matches_skid(self): + """A self signed cert's AKID keyid equals its own SKID.""" + text = self._sign_with_section("ext_all", "test_extfile_akid.crt") + skid = _ext_value(text, "X509v3 Subject Key Identifier") + akid = _ext_value(text, "X509v3 Authority Key Identifier") + self.assertIsNotNone(skid, "SKID not found in cert output") + self.assertIsNotNone(akid, "AKID not found in cert output") + self.assertEqual(akid, skid) + + def test_extfile_key_usage_and_basic_constraints(self): + """keyUsage and basicConstraints come through the same section.""" + text = self._sign_with_section("ext_all", "test_extfile_ku.crt") + self.assertIn("CA:TRUE", text) + usage = _ext_value(text, "X509v3 Key Usage") + self.assertIsNotNone(usage, "key usage not found in cert output") + self.assertIn("Certificate Sign", usage) + self.assertIn("CRL Sign", usage) + + def test_extfile_subject_alt_name_inline(self): + """The inline "TYPE:value,..." subjectAltName form is applied.""" + text = self._sign_with_section("ext_all", "test_extfile_san.crt") + san = _ext_value(text, "X509v3 Subject Alternative Name") + self.assertIsNotNone(san, "SAN not found in cert output") + self.assertIn("DNS:inline.example.com", san) + self.assertIn("IP Address:10.0.0.1", san) + + def test_extfile_bad_extended_key_usage_fails(self): + """A bad value in a config section fails the command. + + The config path used to discard wolfCLU_parseExtension's return, so a + rejected value printed an error and still issued a certificate without + the extension, at exit status 0 -- while the same value via -addext + correctly failed.""" + crt = _tmp("test_extfile_bad_eku.crt") + self._clean(crt) + r = run_wolfssl("x509", "-req", "-in", self.csr, "-days", "3650", + "-extfile", self.conf, "-extensions", "ext_bad_eku", + "-signkey", + os.path.join(CERTS_DIR, "server-key.pem"), + "-out", crt) + self.assertNotEqual(r.returncode, 0, + "a rejected extension value must fail the command") + + def test_extfile_subject_alt_name_section(self): + """The "subjectAltName = @section" form is applied. + + This form has to be resolved by wolfCLU_setExtensions, which holds the + conf handle; wolfCLU_parseExtension only ever sees the value string + and so cannot look a section name up.""" + text = self._sign_with_section("ext_section_san", + "test_extfile_san_sect.crt") + san = _ext_value(text, "X509v3 Subject Alternative Name") + self.assertIsNotNone(san, "SAN not found in cert output") + self.assertIn("DNS:section.example.com", san) + self.assertIn("IP Address:192.0.2.7", san) + + +class TestReqOptionHandling(unittest.TestCase): + """Option level behaviour: file aliasing, diagnostics, dropped values.""" + + def _clean(self, *files): + for f in files: + self.addCleanup(lambda p=f: _cleanup(p)) + + def test_keyout_same_file_as_out_keeps_both(self): + """"-keyout f -out f" appends both objects to one file. + + Opening the path a second time with "wb" truncated the key that had + just been written and left the two BIOs flushing from offset 0 into + each other.""" + both = _tmp("test_req_keyout_same.pem") + self._clean(both) + + r = run_wolfssl("req", "-new", "-newkey", "rsa:2048", "-nodes", + "-subj", "/C=US/CN=test", + "-keyout", both, "-out", both) + self.assertEqual(r.returncode, 0, r.stdout + r.stderr) + + with open(both) as f: + text = f.read() + self.assertIn("PRIVATE KEY", text, "the key was lost") + self.assertIn("BEGIN CERTIFICATE REQUEST", text, + "the request was lost") + + # both objects still have to parse out of the combined file + v = run_wolfssl("req", "-in", both, "-noout", "-verify") + self.assertEqual(v.returncode, 0, v.stdout + v.stderr) + + def test_inform_error_names_inform(self): + """The -inform diagnostic used to name -outform, contradicting the + usage line wolfCLU_checkInform prints just above it.""" + r = run_wolfssl("req", "-new", "-inform", "raw", + "-key", os.path.join(CERTS_DIR, "server-key.pem"), + "-subj", "/C=US/CN=test") + self.assertNotEqual(r.returncode, 0) + out = r.stdout + r.stderr + self.assertIn("-inform", out) + self.assertNotIn("pem or der to -outform", out) + + def test_help_exits_success_after_passout(self): + """-help used to return from inside the parse loop, skipping the + password wipe that -passout had just filled.""" + r = run_wolfssl("req", "-passout", "pass:secret", "-help") + self.assertEqual(r.returncode, 0, r.stdout + r.stderr) + + def test_serial_and_days_reported_as_ignored_for_csr(self): + """A PKCS#10 request has neither field, so say so rather than + dropping the value silently.""" + out = _tmp("test_req_csr_serial.csr") + self._clean(out) + r = run_wolfssl("req", "-new", "-set_serial", "42", "-days", "30", + "-key", os.path.join(CERTS_DIR, "server-key.pem"), + "-subj", "/C=US/CN=test", "-out", out) + self.assertEqual(r.returncode, 0, r.stdout + r.stderr) + self.assertIn("Ignoring", r.stdout + r.stderr) + + def test_addext_akid_without_a_key_id_is_reported(self): + """authorityKeyIdentifier values that name no key id must fail. + + "critical" on its own and an empty value both used to build no + extension and still exit 0, silently dropping what was asked for -- + unlike every sibling extension parser.""" + for value in ("critical", ""): + with self.subTest(value=value): + crt = _tmp("test_req_akid_empty.crt") + self._clean(crt) + r = run_wolfssl("req", "-new", "-days", "3650", + "-key", os.path.join(CERTS_DIR, + "server-key.pem"), + "-subj", "/C=US/CN=test", + "-addext", + "authorityKeyIdentifier=" + value, + "-x509", "-out", crt) + self.assertNotEqual(r.returncode, 0, + "authorityKeyIdentifier={!r} must be " + "reported".format(value)) + + # "issuer" alone stays a success: it is the one documented skip, and + # is covered by TestReqAddExtNames. + # test_addext_authority_key_identifier_issuer_only_is_skipped + + def test_keyout_same_file_spelled_differently_keeps_both(self): + """The shared-stream case is decided by which file the paths resolve + to, not by the two option strings being byte-identical.""" + both = _tmp("test_req_keyout_alias.pem") + self._clean(both) + # name the same file two ways: absolute, and via an explicit "." + aliased = os.path.join(os.path.dirname(both), ".", + os.path.basename(both)).replace("\\", "/") + + r = run_wolfssl("req", "-new", "-newkey", "rsa:2048", "-nodes", + "-subj", "/C=US/CN=test", + "-keyout", both, "-out", aliased) + self.assertEqual(r.returncode, 0, r.stdout + r.stderr) + + with open(both) as f: + text = f.read() + self.assertIn("PRIVATE KEY", text, + "the key was truncated by the -out open") + self.assertIn("BEGIN CERTIFICATE REQUEST", text) + + def test_newkey_algorithm_conflict_either_order(self): + """The -newkey/-ecc agreement check must not depend on argv order.""" + for args in (("-ecc", "-newkey", "rsa:2048"), + ("-newkey", "rsa:2048", "-ecc"), + ("-newkey", "rsa:2048", "-ed25519")): + with self.subTest(args=args): + r = run_wolfssl("req", "-new", "-nodes", + "-subj", "/C=US/CN=test", *args) + self.assertNotEqual(r.returncode, 0, + "{} must be rejected".format(args)) + + def test_duplicate_digest_rejected(self): + """-sha and -sha1 are one digest under two names, so GetOpt's + duplicate detection cannot see the repeat.""" + for args in (("-sha", "-sha1"), ("-sha256", "-sha512")): + with self.subTest(args=args): + r = run_wolfssl("req", "-new", + "-key", os.path.join(CERTS_DIR, + "server-key.pem"), + "-subj", "/C=US/CN=test", *args) + self.assertNotEqual(r.returncode, 0, + "{} must be rejected".format(args)) + + def test_addext_name_may_be_spaced(self): + """OpenSSL routes -addext through its conf parser, which trims around + the '=', so "name = value" has to work here too.""" + crt = _tmp("test_req_addext_spaced.crt") + self._clean(crt) + r = run_wolfssl("req", "-new", "-days", "3650", + "-key", os.path.join(CERTS_DIR, "server-key.pem"), + "-subj", "/C=US/CN=test", + "-addext", "keyUsage = digitalSignature", + "-x509", "-out", crt) + self.assertEqual(r.returncode, 0, r.stdout + r.stderr) + + t = run_wolfssl("x509", "-in", crt, "-text", "-noout") + self.assertEqual(t.returncode, 0, t.stderr) + self.assertIn("Digital Signature", t.stdout) + + def test_addext_empty_basic_constraints_is_reported(self): + """An empty value used to add a CA:FALSE extension nobody asked for.""" + crt = _tmp("test_req_bc_empty.crt") + self._clean(crt) + r = run_wolfssl("req", "-new", "-days", "3650", + "-key", os.path.join(CERTS_DIR, "server-key.pem"), + "-subj", "/C=US/CN=test", + "-addext", "basicConstraints=", + "-x509", "-out", crt) + self.assertNotEqual(r.returncode, 0, r.stdout + r.stderr) + + def test_addext_eku_accepts_dotted_oids(self): + """Conf files written for OpenSSL commonly spell an EKU as its OID.""" + crt = _tmp("test_req_eku_oid.crt") + self._clean(crt) + r = run_wolfssl("req", "-new", "-days", "3650", + "-key", os.path.join(CERTS_DIR, "server-key.pem"), + "-subj", "/C=US/CN=test", + "-addext", + "extendedKeyUsage=1.3.6.1.5.5.7.3.1," + "1.3.6.1.5.5.7.3.2", + "-x509", "-out", crt) + self.assertEqual(r.returncode, 0, r.stdout + r.stderr) + + t = run_wolfssl("x509", "-in", crt, "-text", "-noout") + self.assertEqual(t.returncode, 0, t.stderr) + self.assertIn("TLS Web Server Authentication", t.stdout) + self.assertIn("TLS Web Client Authentication", t.stdout) + + def test_addext_san_critical_any_position(self): + """"critical" is position independent for every other extension + parser, so subjectAltName must not be the exception.""" + for value in ("critical,DNS:a.example.com", + "DNS:a.example.com,critical"): + with self.subTest(value=value): + crt = _tmp("test_req_san_crit.crt") + self._clean(crt) + r = run_wolfssl("req", "-new", "-days", "3650", + "-key", os.path.join(CERTS_DIR, + "server-key.pem"), + "-subj", "/C=US/CN=test", + "-addext", "subjectAltName=" + value, + "-x509", "-out", crt) + self.assertEqual(r.returncode, 0, r.stdout + r.stderr) + + t = run_wolfssl("x509", "-in", crt, "-text", "-noout") + self.assertEqual(t.returncode, 0, t.stderr) + self.assertIn("a.example.com", t.stdout) + + def test_san_named_critical_is_still_a_dns_name(self): + """A name that merely starts with "critical" is a name, not a flag.""" + crt = _tmp("test_req_san_critname.crt") + self._clean(crt) + r = run_wolfssl("req", "-new", "-days", "3650", + "-key", os.path.join(CERTS_DIR, "server-key.pem"), + "-subj", "/C=US/CN=test", + "-addext", "subjectAltName=DNS:critical.example.com", + "-x509", "-out", crt) + self.assertEqual(r.returncode, 0, r.stdout + r.stderr) + + t = run_wolfssl("x509", "-in", crt, "-text", "-noout") + self.assertEqual(t.returncode, 0, t.stderr) + self.assertIn("critical.example.com", t.stdout) + + if __name__ == "__main__": test_main() diff --git a/wolfclu/clu_header_main.h b/wolfclu/clu_header_main.h index b3fa9c93..62eb5f6f 100644 --- a/wolfclu/clu_header_main.h +++ b/wolfclu/clu_header_main.h @@ -556,8 +556,13 @@ int wolfCLU_readConfig(WOLFSSL_X509* x509, char* config, char* sect, char* ext); /** * @brief used to read the 'extensions' section from a config file and put the * extensions found into 'x509' + * + * @param issuer the certificate that will sign 'x509', or NULL when it signs + * itself. The authority key identifier is taken from it when given, since + * deriving one from 'x509' is only correct for a self signed certificate. */ -int wolfCLU_setExtensions(WOLFSSL_X509* x509, WOLFSSL_CONF* conf, char* sect); +int wolfCLU_setExtensions(WOLFSSL_X509* x509, WOLFSSL_CONF* conf, char* sect, + WOLFSSL_X509* issuer); /** * @brief parse a command line "-addext name=value" argument and apply it to diff --git a/wolfclu/clu_optargs.h b/wolfclu/clu_optargs.h index ef77a8fd..41562e01 100644 --- a/wolfclu/clu_optargs.h +++ b/wolfclu/clu_optargs.h @@ -98,6 +98,8 @@ enum { WOLFCLU_CONFIG, WOLFCLU_EXTENSIONS, WOLFCLU_ADDEXT, + WOLFCLU_SERIAL, + WOLFCLU_CAKEY, WOLFCLU_CURVE_NAME, WOLFCLU_DAYS, WOLFCLU_SUBJECT, diff --git a/wolfclu/x509/clu_cert.h b/wolfclu/x509/clu_cert.h index 5256ae17..6dd9ef71 100644 --- a/wolfclu/x509/clu_cert.h +++ b/wolfclu/x509/clu_cert.h @@ -26,6 +26,11 @@ #define DER_FORM 2 #define RAW_FORM 3 +/* Default validity, in days, for a cert */ +#define WOLFCLU_DEFAULT_VALIDITY 20 +/* Max number of days that when converted to seconds will not overflow an int */ +#define WOLFCLU_MAX_VALIDITY 24855 + /* handles incoming arguments for certificate generation */ int wolfCLU_certSetup(int argc, char** argv); diff --git a/wolfclu/x509/clu_x509_sign.h b/wolfclu/x509/clu_x509_sign.h index 67ab5d1a..e0ab6349 100644 --- a/wolfclu/x509/clu_x509_sign.h +++ b/wolfclu/x509/clu_x509_sign.h @@ -36,6 +36,8 @@ int wolfCLU_GenChimeraCertSign(WOLFSSL_BIO *bioCaKey, WOLFSSL_BIO *bioAltCaKey, void wolfCLU_CertSignSetHash(WOLFCLU_CERT_SIGN* csign, enum wc_HashType hashType); void wolfCLU_CertSignSetDate(WOLFCLU_CERT_SIGN* csign, int d); +/* set notBefore/notAfter on 'x509' to a window of 'days' starting now */ +int wolfCLU_CertSetDate(WOLFSSL_X509* x509, int days); int wolfCLU_CertSign(WOLFCLU_CERT_SIGN* csign, WOLFSSL_X509* x509); WOLFCLU_CERT_SIGN* wolfCLU_readSignConfig(char* config, char* sect); int wolfCLU_CertSignAppendOut(WOLFCLU_CERT_SIGN* csign, char* out); From 28c600d4679616c66109ed870de3260a5b1cc6d0 Mon Sep 17 00:00:00 2001 From: Aidan Keefe Date: Mon, 24 Aug 2026 16:41:17 -0600 Subject: [PATCH 2/3] Skoll review fixes and refactors --- manpages/wolfssl-ca.1 | 4 +- manpages/wolfssl-req.1 | 83 +++- manpages/wolfssl-x509.1 | 28 +- src/clu_main.c | 7 +- src/x509/clu_cert_setup.c | 1 - src/x509/clu_config.c | 762 +++++++++++++++++++---------------- src/x509/clu_request_setup.c | 529 +++++++++++++++--------- tests/x509/x509-ca-test.py | 1 + tests/x509/x509-req-test.py | 333 +++++++++++++-- wolfclu/clu_optargs.h | 1 + wolfclu/x509/clu_cert.h | 7 +- 11 files changed, 1170 insertions(+), 586 deletions(-) diff --git a/manpages/wolfssl-ca.1 b/manpages/wolfssl-ca.1 index 04495b17..98b78ac2 100644 --- a/manpages/wolfssl-ca.1 +++ b/manpages/wolfssl-ca.1 @@ -42,7 +42,9 @@ the configuration file (see -config). -md digest signing digest to use, e.g. sha256. .br .LP --days n number of days the certificate is valid for. +-days n number of days the certificate is valid for, in the +.br + range [1, 24855]. .br .LP -extensions section section of the config file to read extensions from. diff --git a/manpages/wolfssl-req.1 b/manpages/wolfssl-req.1 index ec1c1f9e..4deb511d 100644 --- a/manpages/wolfssl-req.1 +++ b/manpages/wolfssl-req.1 @@ -4,13 +4,14 @@ .SH NAME wolfssl-req, req \- generate certificate requests and self-signed certificates .SH SYNOPSIS -wolfssl req [-new] [-in file] [-out file] [-key file] [-newkey type:bits] [-keyout file] [-inform PEM|DER] [-outform PEM|DER] [-config file] [-days n] [-x509] [-subj name] [-extensions section] [-nodes] [-passout source] [-sha|-sha224|-sha256|-sha384|-sha512] [-verify] [-text] [-noout] +wolfssl req [-new] [-in file] [-out file] [-key file] [-inkey file] [-newkey rsa:bits] [-keyout file] [-inform PEM|DER] [-outform PEM|DER] [-config file] [-days n] [-set_serial n] [-x509] [-CA file] [-CAkey file] [-copy_extensions none|copy|copyall] [-subj name] [-extensions section] [-addext ext] [-nodes] [-passout source] [-sha|-sha224|-sha256|-sha384|-sha512] [-verify] [-text] [-noout] .SH DESCRIPTION Creates a PKCS#10 certificate signing request (CSR), or with \-x509 a -self-signed certificate. The signing key may be supplied with \-key or -generated on the fly with \-newkey. Without -subj or -config, subject -fields are collected interactively. Without -keyout, a generated private -key is written to stdout. +self-signed certificate, or with \-CA a certificate issued for an existing +request. The signing key may be supplied with \-key or generated on the +fly with \-newkey. Without -subj or -config, subject fields are collected +interactively. Without -keyout, a generated private key is written to +stdout. .SH OPTIONS -new OpenSSL compatibility flag (no-op). .br @@ -24,13 +25,16 @@ key is written to stdout. -key file private key used to sign the certificate request. .br .LP --newkey type:bits generate the private key to use with the request. +-inkey file alias for -key. .br - RSA: rsa:2048. Dilithium (with -x509 and certgen): +.LP +-newkey rsa:bits generate the private key to use with the request. +.br + RSA only: rsa:2048, rsa:3072 or rsa:4096. ECC and .br - dilithium:2, dilithium:3, dilithium:5, or ml-dsa:N. + Dilithium keys must be generated with ecparam or .br - For ECC keys, generate with ecparam first. + genkey and passed in with -key. .br .LP -keyout file file to output the generated key to. @@ -45,12 +49,46 @@ key is written to stdout. -config file file to parse for certificate configuration. .br .LP --days n number of days the certificate should be valid for. +-days n number of days the certificate should be valid for, +.br + in the range [1, 24855] (default 30). Applies to +.br + -x509 and -CA only; ignored for a plain CSR. +.br +.LP +-set_serial n serial number for the issued certificate. Without it +.br + a random serial number is generated. .br .LP -x509 generate a self-signed certificate instead of a CSR. .br .LP +-CA file certificate of the CA that issues the certificate, +.br + signing the request given with -in. Requires -CAkey. +.br +.LP +-CAkey file private key belonging to the certificate given to -CA. +.br +.LP +-copy_extensions arg whether -CA carries the extensions the request asked +.br + for into the issued certificate: none (the default), +.br + or copy/copyall. A request only states what its sender +.br + wants, so by default the issued certificate takes just +.br + the subject name and public key from it and carries no +.br + requested subjectAltName, keyUsage, extendedKeyUsage or +.br + certificate policies. Basic Constraints stays CA:FALSE +.br + either way. Ignored without -CA. +.br +.LP -subj name subject name in /key=value/... format, e.g. .br /C=US/ST=WA/L=Seattle/O=wolfSSL/CN=wolfSSL. @@ -59,6 +97,13 @@ key is written to stdout. -extensions section section of the config file to read extensions from. .br .LP +-addext ext add a single extension, e.g. +.br + "subjectAltName=DNS:example.com,IP:192.168.1.2". +.br + Only one -addext may be given. +.br +.LP -nodes do not encrypt the private key on output. Without .br -nodes, generated keys are encrypted and the user is @@ -76,7 +121,13 @@ key is written to stdout. signing digest (default SHA-256 when omitted). .br .LP --verify check the signature on the request. +-verify check the signature on the request. Not needed with +.br + -CA, which always verifies both the request it is +.br + given and the certificate it issues; there -verify +.br + only adds the "verify OK" line to the output. .br .LP -text output human readable text of the request. @@ -94,6 +145,16 @@ Create a self-signed certificate valid for 365 days: wolfssl req -new -x509 -newkey rsa:2048 -nodes -keyout mykey.pem -out mycert.pem -days 365 -subj "/C=US/O=Test/CN=localhost" .RE .LP +Issue a certificate for an existing request under a CA: +.RS +wolfssl req -CA ca-cert.pem -CAkey ca-key.pem -in mycsr.pem -days 365 -out mycert.pem +.RE +.LP +The same, carrying the extensions the request asked for: +.RS +wolfssl req -CA ca-cert.pem -CAkey ca-key.pem -copy_extensions copy -in mycsr.pem -out mycert.pem +.RE +.LP View a CSR in human-readable format: .RS wolfssl req -in mycsr.pem -text -noout diff --git a/manpages/wolfssl-x509.1 b/manpages/wolfssl-x509.1 index 01e9dc84..3502f538 100644 --- a/manpages/wolfssl-x509.1 +++ b/manpages/wolfssl-x509.1 @@ -4,7 +4,7 @@ .SH NAME wolfssl-x509, x509 \- X.509 certificate processing and conversion .SH SYNOPSIS -wolfssl x509 [-inform PEM|DER] [-in file] [-outform PEM|DER] [-out file] [-req] [-signkey file] [-extfile file] [-extensions section] [-sha1|-sha224|-sha256|-sha384|-sha512] [-noout] [-text] [-subject] [-issuer] [-serial] [-dates] [-email] [-fingerprint] [-purpose] [-hash] [-modulus] [-pubkey] +wolfssl x509 [-inform PEM|DER] [-in file] [-outform PEM|DER] [-out file] [-req] [-signkey file] [-days n] [-extfile file] [-extensions section] [-sha1|-sha224|-sha256|-sha384|-sha512] [-noout] [-text] [-subject] [-issuer] [-serial] [-dates] [-email] [-fingerprint] [-purpose] [-hash] [-modulus] [-pubkey] .SH DESCRIPTION Reads an X.509 certificate and converts it between PEM and DER formats and/or prints selected fields. By default the (re-encoded) certificate is @@ -38,18 +38,21 @@ written to the output. -signkey file private key used when signing a CSR with -req. .br .LP --extfile file configuration file for extensions. Only takes effect +-days n number of days the re-signed certificate is valid .br - when both -req and -signkey are also given; otherwise + for, in the range [1, 24855]. Requires -req; see .br - silently ignored (see NOTES). + NOTES. .br .LP --extensions section section of the config file to use. Only takes effect +-extfile file configuration file for extensions. Requires -req; .br - when both -req and -signkey are also given; otherwise + see NOTES. .br - silently ignored (see NOTES). +.LP +-extensions section section of the config file to use. Requires -req; +.br + see NOTES. .br .LP -sha1, -sha224, -sha256, -sha384, -sha512 @@ -94,10 +97,13 @@ written to the output. .LP -pubkey print out the public key. .SH NOTES -Unless both -req and -signkey are used, -extfile and -extensions are -silently ignored on output: the (re-encoded) certificate is written from -the original input DER, not from any in-memory changes made while parsing -extensions. +-days, -extfile and -extensions alter the certificate, and an altered +certificate is only written out when -req has it re-signed: without a +re-sign the output is written from the original input DER, not from any +in-memory changes. Passing any of the three without -req is therefore an +error ("Altering a Cert requires a resign and -req was not set") and the +command exits non-zero, rather than silently dropping the change as +earlier releases did. .SH EXAMPLES View certificate details in human-readable format: .RS diff --git a/src/clu_main.c b/src/clu_main.c index 4dcce6ef..6e9f6ff4 100644 --- a/src/clu_main.c +++ b/src/clu_main.c @@ -353,12 +353,17 @@ int main(int argc, char** argv) ret = WOLFCLU_FATAL_ERROR; } + /* WOLFCLU_FAILURE is 0, which the return below would hand back as a + * success status, so fold every non-positive code into one error first */ if (ret <= 0) { wolfCLU_LogError("Error returned: %d.", ret); ret = WOLFCLU_FATAL_ERROR; } wolfSSL_Cleanup(); - return ret == WOLFCLU_FATAL_ERROR ? 1 : 0; + + /* main function we want to return 0 on success so that the executable + * returns the expected 0 on success */ + return (ret == WOLFCLU_SUCCESS)? 0 : ret; } #ifdef FREERTOS diff --git a/src/x509/clu_cert_setup.c b/src/x509/clu_cert_setup.c index ee568f76..5bd51a9e 100644 --- a/src/x509/clu_cert_setup.c +++ b/src/x509/clu_cert_setup.c @@ -319,7 +319,6 @@ int wolfCLU_certSetup(int argc, char **argv) break; } - case ARG_FOUND_TWICE: wolfCLU_LogError("Found duplicate argument"); ret = WOLFCLU_FATAL_ERROR; diff --git a/src/x509/clu_config.c b/src/x509/clu_config.c index 300ae340..0ccb43d3 100644 --- a/src/x509/clu_config.c +++ b/src/x509/clu_config.c @@ -28,24 +28,26 @@ #ifndef WOLFCLU_NO_FILESYSTEM /* return WOLFCLU_SUCCESS on success */ -static int wolfCLU_setAttributes(WOLFSSL_X509* x509, WOLFSSL_CONF* conf, - char* sect) +static int wolfCLU_setAttributes(WOLFSSL_X509 *x509, WOLFSSL_CONF *conf, + char *sect) { - const char* current; + const char *current; int currentSz; current = wolfSSL_NCONF_get_string(conf, sect, "challengePassword"); if (current != NULL) { currentSz = (int)XSTRLEN(current); - wolfSSL_X509_REQ_add1_attr_by_NID(x509, NID_pkcs9_challengePassword, - MBSTRING_ASC, (const unsigned char*)current, currentSz); + wolfSSL_X509_REQ_add1_attr_by_NID( + x509, NID_pkcs9_challengePassword, MBSTRING_ASC, + (const unsigned char *)current, currentSz); } current = wolfSSL_NCONF_get_string(conf, sect, "unstructuredName"); if (current != NULL) { currentSz = (int)XSTRLEN(current); - wolfSSL_X509_REQ_add1_attr_by_NID(x509, NID_pkcs9_unstructuredName, - MBSTRING_ASC, (const unsigned char*)current, currentSz); + wolfSSL_X509_REQ_add1_attr_by_NID( + x509, NID_pkcs9_unstructuredName, MBSTRING_ASC, + (const unsigned char *)current, currentSz); } return WOLFCLU_SUCCESS; @@ -57,14 +59,16 @@ static int wolfCLU_setAttributes(WOLFSSL_X509* x509, WOLFSSL_CONF* conf, * definition and every call site live inside this guard, so the declaration * has to as well or a !WOLFSSL_CERT_EXT build carries a static function that * is declared and never defined. */ -static char* wolfCLU_trimToken(char* word); +static char *wolfCLU_trimToken(char *word); #ifdef WOLFSSL_ALT_NAMES /* defined further down, forward declared for wolfCLU_parseExtension */ -static int wolfCLU_setInlineSubjectAltNames(WOLFSSL_X509* x509, char* val); +static int wolfCLU_setInlineSubjectAltNames(WOLFSSL_X509 *x509, char *val); #endif -WOLFSSL_ASN1_OBJECT* wolfCLU_extenstionGetObjectNID(WOLFSSL_X509_EXTENSION *ext, int nid, int crit) { +WOLFSSL_ASN1_OBJECT *wolfCLU_extenstionGetObjectNID(WOLFSSL_X509_EXTENSION *ext, + int nid, int crit) +{ WOLFSSL_ASN1_OBJECT *obj; if (ext == NULL) return NULL; @@ -87,9 +91,9 @@ WOLFSSL_ASN1_OBJECT* wolfCLU_extenstionGetObjectNID(WOLFSSL_X509_EXTENSION *ext, return obj; } -static WOLFSSL_X509_EXTENSION* wolfCLU_parseBasicConstraint(char* in, int crit) +static WOLFSSL_X509_EXTENSION *wolfCLU_parseBasicConstraint(char *in, int crit) { - char* word, *end, *str = in; + char *word, *end, *str = in; WOLFSSL_X509_EXTENSION *ext; WOLFSSL_ASN1_OBJECT *obj; /* an empty value would otherwise tokenize to nothing and hand back a @@ -120,17 +124,17 @@ static WOLFSSL_X509_EXTENSION* wolfCLU_parseBasicConstraint(char* in, int crit) * rejected a valid value. Splitting on ',' first also makes "critical" * position independent, since it is then always a token of its own. */ for (word = XSTRTOK(str, ",", &end); word != NULL; - word = XSTRTOK(NULL, ",", &end)) { + word = XSTRTOK(NULL, ",", &end)) { /* hold on to the keyword: 'val' is the part after the colon, and * testing that against the next keyword would let "CA:pathlen" style * nonsense through */ - char* tok = wolfCLU_trimToken(word); - char* val = XSTRSTR(tok, ":"); + char *tok = wolfCLU_trimToken(word); + char *val = XSTRSTR(tok, ":"); if (val != NULL) { *val = '\0'; - val = wolfCLU_trimToken(val + 1); - tok = wolfCLU_trimToken(tok); + val = wolfCLU_trimToken(val + 1); + tok = wolfCLU_trimToken(tok); } if (XSTRCMP(tok, "CA") == 0) { @@ -138,7 +142,7 @@ static WOLFSSL_X509_EXTENSION* wolfCLU_parseBasicConstraint(char* in, int crit) if (val == NULL) { wolfCLU_LogError("basicConstraints CA is missing a value, " - "expected \"CA:TRUE\" or \"CA:FALSE\""); + "expected \"CA:TRUE\" or \"CA:FALSE\""); wolfSSL_X509_EXTENSION_free(ext); return NULL; } @@ -161,24 +165,23 @@ static WOLFSSL_X509_EXTENSION* wolfCLU_parseBasicConstraint(char* in, int crit) } wolfCLU_LogError("Unable to parse basic constraint CA value " - "%s, expected \"TRUE\" or \"FALSE\"", - valSz ? val : "\"\""); + "%s, expected \"TRUE\" or \"FALSE\"", + valSz ? val : "\"\""); wolfSSL_X509_EXTENSION_free(ext); return NULL; } if (XSTRCMP(tok, "pathlen") == 0) { - long pathLen = 0; + long pathLen = -1; /* 0 is a valid path length: the CA may issue end entity * certificates but no further CAs */ - if (val == NULL || - wolfCLU_parseDecimalBounded(val, 0, 127, &pathLen) != - WOLFCLU_SUCCESS) { + if (val == NULL || wolfCLU_parseDecimalBounded( + val, 0, 127, &pathLen) != WOLFCLU_SUCCESS) { wolfCLU_LogError("Unable to parse basic constraint " - "pathlen value %s, it must be a number in the " - "range [0, 127]", - (val != NULL && XSTRLEN(val)) ? val : "\"\""); + "pathlen value %s, it must be a number in the " + "range [0, 127]", + (val != NULL && XSTRLEN(val)) ? val : "\"\""); wolfSSL_X509_EXTENSION_free(ext); return NULL; } @@ -193,12 +196,6 @@ static WOLFSSL_X509_EXTENSION* wolfCLU_parseBasicConstraint(char* in, int crit) wolfSSL_X509_EXTENSION_free(ext); return NULL; } - if (wolfSSL_ASN1_INTEGER_set(obj->pathlen, pathLen) - != WOLFSSL_SUCCESS) { - wolfCLU_LogError("Unable to set the basic constraint pathlen"); - wolfSSL_X509_EXTENSION_free(ext); - return NULL; - } /* NOTE: Not undoing the set above: wolfSSL_X509_add_ext() reads the * pathlen *value* out of ->length, which otherwise holds the DER @@ -218,7 +215,16 @@ static WOLFSSL_X509_EXTENSION* wolfCLU_parseBasicConstraint(char* in, int crit) } wolfCLU_LogError("Unknown token \"%s\" while parsing " - "basicConstraints", tok); + "basicConstraints", + tok); + wolfSSL_X509_EXTENSION_free(ext); + return NULL; + } + + /* pathLenConstraint may only appear when cA is TRUE (RFC 5280 4.2.1.9) */ + if (obj->pathlen != NULL && !obj->ca) { + wolfCLU_LogError("basicConstraints pathlen requires CA:TRUE " + "(RFC 5280 4.2.1.9)"); wolfSSL_X509_EXTENSION_free(ext); return NULL; } @@ -227,7 +233,7 @@ static WOLFSSL_X509_EXTENSION* wolfCLU_parseBasicConstraint(char* in, int crit) * CA:FALSE extension that was never asked for */ if (!sawValue) { wolfCLU_LogError("no basicConstraints value found, expected " - "\"CA:TRUE\" or \"CA:FALSE\""); + "\"CA:TRUE\" or \"CA:FALSE\""); wolfSSL_X509_EXTENSION_free(ext); return NULL; } @@ -238,16 +244,19 @@ static WOLFSSL_X509_EXTENSION* wolfCLU_parseBasicConstraint(char* in, int crit) /* Trim spaces and tabs from both ends of 'word', in place. Returns the new * start. Config values are commonly written "a, b" or even "a , b", so a * token has to survive whitespace on either side. */ -static char* wolfCLU_trimToken(char* word) +static char *wolfCLU_trimToken(char *word) { int sz; + if (word == NULL) { + return NULL; + } while (*word == ' ' || *word == '\t' || *word == '\n' || *word == '\r') { word++; } sz = (int)XSTRLEN(word); - while (sz > 0 && (word[sz-1] == ' ' || word[sz-1] == '\t' || - word[sz-1] == '\n' || word[sz-1] == '\r')) { + while (sz > 0 && (word[sz - 1] == ' ' || word[sz - 1] == '\t' || + word[sz - 1] == '\n' || word[sz - 1] == '\r')) { word[--sz] = '\0'; } @@ -261,20 +270,21 @@ static char* wolfCLU_trimToken(char* word) * "subjectAltName=DNS:critical.example.com". 'str' is not modified, this runs * before the parsers tokenize it in place. * returns 1 when the key word is present, 0 otherwise */ -static int wolfCLU_hasCriticalToken(const char* str) +static int wolfCLU_hasCriticalToken(const char *str) { - const char* tok = str; + const char *tok = str; while (tok != NULL) { - const char* end = XSTRSTR(tok, ","); - int sz; + const char *end = XSTRSTR(tok, ","); + int sz; - while (*tok == ' ' || *tok == '\t') { + while (*tok == ' ' || *tok == '\t' || *tok == '\n' || *tok == '\r') { tok++; } sz = (end != NULL) ? (int)(end - tok) : (int)XSTRLEN(tok); - while (sz > 0 && (tok[sz - 1] == ' ' || tok[sz - 1] == '\t')) { + while (sz > 0 && (tok[sz - 1] == ' ' || tok[sz - 1] == '\t' || + tok[sz - 1] == '\n' || tok[sz - 1] == '\r')) { sz--; } @@ -292,7 +302,7 @@ static int wolfCLU_hasCriticalToken(const char* str) * that OpenSSL's "keyid:always" / "issuer:optional" spellings match, while a * typo such as "keyidalways" does not. * returns 1 on a match, 0 otherwise */ -static int wolfCLU_tokenIs(const char* word, const char* kw) +static int wolfCLU_tokenIs(const char *word, const char *kw) { int kwSz = (int)XSTRLEN(kw); @@ -308,13 +318,13 @@ static int wolfCLU_tokenIs(const char* word, const char* kw) * expect. On success the caller owns '*pkey' and must wolfSSL_EVP_PKEY_free() * it; '*key' points into it and must not outlive it. * return WOLFCLU_SUCCESS on success */ -static int wolfCLU_getPubKeyForId(WOLFSSL_X509* x509, WOLFSSL_EVP_PKEY** pkey, - void** key, int* keyType) +static int wolfCLU_getPubKeyForId(WOLFSSL_X509 *x509, WOLFSSL_EVP_PKEY **pkey, + void **key, int *keyType) { int type; *pkey = NULL; - *key = NULL; + *key = NULL; type = wolfSSL_X509_get_pubkey_type(x509); @@ -360,15 +370,15 @@ static int wolfCLU_getPubKeyForId(WOLFSSL_X509* x509, WOLFSSL_EVP_PKEY** pkey, * names and so cannot tell a self signed certificate from an RFC 5280 4.2.1.1 * key rollover one. An issuer with no entries is the self signed case too. * returns 1 when self issued, 0 otherwise */ -static int wolfCLU_isSelfIssued(WOLFSSL_X509* x509) +static int wolfCLU_isSelfIssued(WOLFSSL_X509 *x509) { - WOLFSSL_X509_NAME* issuer = wolfSSL_X509_get_issuer_name(x509); + WOLFSSL_X509_NAME *issuer = wolfSSL_X509_get_issuer_name(x509); if (issuer == NULL || wolfSSL_X509_NAME_entry_count(issuer) == 0) { return 1; } - return wolfSSL_X509_NAME_cmp(wolfSSL_X509_get_subject_name(x509), issuer) - == 0; + return wolfSSL_X509_NAME_cmp(wolfSSL_X509_get_subject_name(x509), issuer) == + 0; } /* Create an authority key identifier extension from the config values @@ -381,19 +391,19 @@ static int wolfCLU_isSelfIssued(WOLFSSL_X509* x509) * On success '*out' holds the new extension, or NULL when it was applied * directly or every key word present was a skipped one. * return WOLFCLU_SUCCESS on success */ -static int wolfCLU_parseAuthorityKeyId(char* str, int crit, - WOLFSSL_X509* x509, WOLFSSL_X509* issuer, - WOLFSSL_X509_EXTENSION** out) +static int wolfCLU_parseAuthorityKeyId(char *str, int crit, WOLFSSL_X509 *x509, + WOLFSSL_X509 *issuer, + WOLFSSL_X509_EXTENSION **out) { - WOLFSSL_X509_EXTENSION* ext = NULL; + WOLFSSL_X509_EXTENSION *ext = NULL; WOLFSSL_EVP_PKEY *pkey = NULL; - char* word, *end; - char* deli = (char*)","; - int ret = WOLFCLU_SUCCESS; + char *word, *end; + char *deli = (char *)","; + int ret = WOLFCLU_SUCCESS; /* A value naming only the skipped "issuer" is a success with no * extension; one naming nothing usable at all is an error. */ - int sawSkipped = 0; - int sawKeyId = 0; + int sawSkipped = 0; + int sawKeyId = 0; if (x509 == NULL || str == NULL || out == NULL) return BAD_FUNC_ARG; @@ -403,14 +413,15 @@ static int wolfCLU_parseAuthorityKeyId(char* str, int crit, /* RFC 5280 4.2.1.1 says the AKID MUST be non-critical, so the key word is * accepted and reported rather than honoured. */ if (crit) { - WOLFCLU_LOG(WOLFCLU_L0, "Ignoring \"critical\" on " - "authorityKeyIdentifier, RFC 5280 requires it be " - "non-critical"); + WOLFCLU_LOG(WOLFCLU_L0, + "Ignoring \"critical\" on " + "authorityKeyIdentifier, RFC 5280 requires it be " + "non-critical"); } for (word = XSTRTOK(str, deli, &end); - word != NULL && ret == WOLFCLU_SUCCESS; - word = XSTRTOK(NULL, deli, &end)) { + word != NULL && ret == WOLFCLU_SUCCESS; + word = XSTRTOK(NULL, deli, &end)) { word = wolfCLU_trimToken(word); /* the critical key word was already handled by the caller */ @@ -423,7 +434,7 @@ static int wolfCLU_parseAuthorityKeyId(char* str, int crit, if (wolfCLU_tokenIs(word, "keyid") || wolfCLU_tokenIs(word, "hash")) { WOLFSSL_ASN1_STRING *data; void *key = NULL; - int keyType; + int keyType; sawKeyId = 1; @@ -434,15 +445,16 @@ static int wolfCLU_parseAuthorityKeyId(char* str, int crit, * guards its own call. */ if (issuer != NULL) { #ifndef NO_SHA - if (wolfSSL_X509_set_authority_key_id_ex(x509, issuer) - != WOLFSSL_SUCCESS) { + if (wolfSSL_X509_set_authority_key_id_ex(x509, issuer) != + WOLFSSL_SUCCESS) { wolfCLU_LogError("error setting the authority key id from " - "the issuing certificate"); + "the issuing certificate"); ret = WOLFCLU_FATAL_ERROR; } #else - wolfCLU_LogError("cannot derive an authority key id from the " - "issuing certificate, wolfSSL was built with NO_SHA"); + wolfCLU_LogError( + "cannot derive an authority key id from the " + "issuing certificate, wolfSSL was built with NO_SHA"); ret = NOT_COMPILED_IN; #endif continue; @@ -454,8 +466,8 @@ static int wolfCLU_parseAuthorityKeyId(char* str, int crit, continue; } - if (wolfCLU_getPubKeyForId(x509, &pkey, &key, &keyType) - != WOLFCLU_SUCCESS) { + if (wolfCLU_getPubKeyForId(x509, &pkey, &key, &keyType) != + WOLFCLU_SUCCESS) { ret = WOLFCLU_FATAL_ERROR; break; } @@ -463,49 +475,51 @@ static int wolfCLU_parseAuthorityKeyId(char* str, int crit, /* Cert is several kilobytes, so it is scoped to the one branch * that needs it rather than sitting on the frame throughout. */ { - Cert cert; /* temporary to use existing auth key id api */ + Cert cert; /* temporary to use existing auth key id api */ - XMEMSET(&cert, 0, sizeof(Cert)); - if (wc_SetAuthKeyIdFromPublicKey_ex(&cert, keyType, key) < 0) { - wolfCLU_LogError("error hashing public key"); - ret = WOLFCLU_FATAL_ERROR; - } - else { - data = wolfSSL_ASN1_STRING_new(); - if (data == NULL) { - ret = MEMORY_E; + XMEMSET(&cert, 0, sizeof(Cert)); + if (wc_SetAuthKeyIdFromPublicKey_ex(&cert, keyType, key) < 0) { + wolfCLU_LogError("error hashing public key"); + ret = WOLFCLU_FATAL_ERROR; } else { - if (wolfSSL_ASN1_STRING_set(data, cert.akid, cert.akidSz) - != WOLFSSL_SUCCESS) { - wolfCLU_LogError("error setting the akid"); - ret = WOLFCLU_FATAL_ERROR; + data = wolfSSL_ASN1_STRING_new(); + if (data == NULL) { + ret = MEMORY_E; } else { - /* RFC 5280 4.2.1.1 requires a non-critical AKID, so - * the extension is built that way on both paths even - * when the config asked for critical. */ - ext = wolfSSL_X509_EXTENSION_new(); - if (ext != NULL && - wolfCLU_extenstionGetObjectNID(ext, - NID_authority_key_identifier, 0) - == NULL) { - /* extension was free'd on failure */ - ext = NULL; - } - if (ext == NULL) { + if (wolfSSL_ASN1_STRING_set(data, cert.akid, + cert.akidSz) != + WOLFSSL_SUCCESS) { + wolfCLU_LogError("error setting the akid"); ret = WOLFCLU_FATAL_ERROR; } - else if (wolfSSL_X509_EXTENSION_set_data(ext, data) - != WOLFSSL_SUCCESS) { - wolfCLU_LogError("error setting the akid data"); - ret = WOLFCLU_FATAL_ERROR; + else { + /* RFC 5280 4.2.1.1 requires a non-critical + * AKID, so the extension is built that way on + * both paths even when the config asked for + * critical. */ + ext = wolfSSL_X509_EXTENSION_new(); + if (ext != NULL && + wolfCLU_extenstionGetObjectNID( + ext, NID_authority_key_identifier, 0) == + NULL) { + /* extension was free'd on failure */ + ext = NULL; + } + if (ext == NULL) { + ret = WOLFCLU_FATAL_ERROR; + } + else if (wolfSSL_X509_EXTENSION_set_data( + ext, data) != WOLFSSL_SUCCESS) { + wolfCLU_LogError("error setting the akid data"); + ret = WOLFCLU_FATAL_ERROR; + } } + wolfSSL_ASN1_STRING_free(data); } - wolfSSL_ASN1_STRING_free(data); } } - } wolfSSL_EVP_PKEY_free(pkey); pkey = NULL; } @@ -514,11 +528,11 @@ static int wolfCLU_parseAuthorityKeyId(char* str, int crit, * issuing certificate, the keyid alone is still a valid AKID */ sawSkipped = 1; WOLFCLU_LOG(WOLFCLU_L0, "Skipping authority key identifier " - "\"issuer\", only \"keyid\" is supported"); + "\"issuer\", only \"keyid\" is supported"); } else { wolfCLU_LogError("unsupported authority key identifier \"%s\"", - word); + word); ret = WOLFCLU_FATAL_ERROR; } } @@ -534,7 +548,7 @@ static int wolfCLU_parseAuthorityKeyId(char* str, int crit, * dropped, matching every sibling parser in this file. */ if (ext == NULL && !sawKeyId && !sawSkipped) { wolfCLU_LogError("no authority key identifier value found, " - "expected \"keyid\" or \"issuer\""); + "expected \"keyid\" or \"issuer\""); return WOLFCLU_FATAL_ERROR; } @@ -546,20 +560,20 @@ static int wolfCLU_parseAuthorityKeyId(char* str, int crit, /* The extended key usages wolfSSL can express, by both the OpenSSL key word * and the dotted OID that conf files commonly use for the same purpose. */ typedef struct WOLFCLU_EKU_MAP { - const char* name; - const char* oid; - byte flag; + const char *name; + const char *oid; + byte flag; } WOLFCLU_EKU_MAP; static const WOLFCLU_EKU_MAP wolfCLU_ekuMap[] = { - {"anyExtendedKeyUsage", "2.5.29.37.0", EXTKEYUSE_ANY}, - {"any", NULL, EXTKEYUSE_ANY}, - {"serverAuth", "1.3.6.1.5.5.7.3.1", EXTKEYUSE_SERVER_AUTH}, - {"clientAuth", "1.3.6.1.5.5.7.3.2", EXTKEYUSE_CLIENT_AUTH}, - {"codeSigning", "1.3.6.1.5.5.7.3.3", EXTKEYUSE_CODESIGN}, - {"emailProtection", "1.3.6.1.5.5.7.3.4", EXTKEYUSE_EMAILPROT}, - {"timeStamping", "1.3.6.1.5.5.7.3.8", EXTKEYUSE_TIMESTAMP}, - {"OCSPSigning", "1.3.6.1.5.5.7.3.9", EXTKEYUSE_OCSP_SIGN} + { "anyExtendedKeyUsage", "2.5.29.37.0", EXTKEYUSE_ANY }, + { "any", NULL, EXTKEYUSE_ANY }, + { "serverAuth", "1.3.6.1.5.5.7.3.1", EXTKEYUSE_SERVER_AUTH }, + { "clientAuth", "1.3.6.1.5.5.7.3.2", EXTKEYUSE_CLIENT_AUTH }, + { "codeSigning", "1.3.6.1.5.5.7.3.3", EXTKEYUSE_CODESIGN }, + { "emailProtection", "1.3.6.1.5.5.7.3.4", EXTKEYUSE_EMAILPROT }, + { "timeStamping", "1.3.6.1.5.5.7.3.8", EXTKEYUSE_TIMESTAMP }, + { "OCSPSigning", "1.3.6.1.5.5.7.3.9", EXTKEYUSE_OCSP_SIGN } }; /* Create an extended key usage extension from a comma separated list of the @@ -568,20 +582,20 @@ static const WOLFCLU_EKU_MAP wolfCLU_ekuMap[] = { * files written for OpenSSL commonly use it. * * returns the new extension on success, NULL on failure */ -static WOLFSSL_X509_EXTENSION* wolfCLU_parseExtKeyUsage(char* str, int crit) +static WOLFSSL_X509_EXTENSION *wolfCLU_parseExtKeyUsage(char *str, int crit) { WOLFSSL_ASN1_STRING *data; WOLFSSL_X509_EXTENSION *ext = NULL; - char* word, *end; - char* deli = (char*)","; - byte extKeyUseFlag = 0; + char *word, *end; + char *deli = (char *)","; + byte extKeyUseFlag = 0; size_t i; if (str == NULL) return NULL; for (word = XSTRTOK(str, deli, &end); word != NULL; - word = XSTRTOK(NULL, deli, &end)) { + word = XSTRTOK(NULL, deli, &end)) { int found = 0; word = wolfCLU_trimToken(word); @@ -592,10 +606,10 @@ static WOLFSSL_X509_EXTENSION* wolfCLU_parseExtKeyUsage(char* str, int crit) } for (i = 0; i < sizeof(wolfCLU_ekuMap) / sizeof(wolfCLU_ekuMap[0]); - i++) { + i++) { if (XSTRCMP(word, wolfCLU_ekuMap[i].name) == 0 || - (wolfCLU_ekuMap[i].oid != NULL && - XSTRCMP(word, wolfCLU_ekuMap[i].oid) == 0)) { + (wolfCLU_ekuMap[i].oid != NULL && + XSTRCMP(word, wolfCLU_ekuMap[i].oid) == 0)) { extKeyUseFlag |= wolfCLU_ekuMap[i].flag; found = 1; break; @@ -604,8 +618,9 @@ static WOLFSSL_X509_EXTENSION* wolfCLU_parseExtKeyUsage(char* str, int crit) if (!found) { wolfCLU_LogError("unsupported extended key usage \"%s\"", word); - wolfCLU_LogError("supported: any, serverAuth, clientAuth, " - "codeSigning, emailProtection, timeStamping, OCSPSigning"); + wolfCLU_LogError( + "supported: any, serverAuth, clientAuth, " + "codeSigning, emailProtection, timeStamping, OCSPSigning"); return NULL; } } @@ -615,6 +630,16 @@ static WOLFSSL_X509_EXTENSION* wolfCLU_parseExtKeyUsage(char* str, int crit) return NULL; } + /* wolfSSL's SetExtKeyUsage() short circuits on EXTKEYUSE_ANY and emits + * anyExtendedKeyUsage alone, so every named purpose listed beside it + * would be dropped from the certificate without a word. OpenSSL emits + * them all, so refuse the combination rather than quietly diverging. */ + if ((extKeyUseFlag & EXTKEYUSE_ANY) && (extKeyUseFlag != EXTKEYUSE_ANY)) { + wolfCLU_LogError("extended key usage \"any\" cannot be combined with " + "other purposes; it already covers all of them"); + return NULL; + } + ext = wolfSSL_X509_EXTENSION_new(); if (ext == NULL) { return NULL; @@ -633,9 +658,9 @@ static WOLFSSL_X509_EXTENSION* wolfCLU_parseExtKeyUsage(char* str, int crit) } /* a single byte of flags is what wolfSSL_X509_add_ext() expects */ - if (wolfSSL_ASN1_STRING_set(data, &extKeyUseFlag, (int)sizeof(byte)) - != WOLFSSL_SUCCESS || - wolfSSL_X509_EXTENSION_set_data(ext, data) != WOLFSSL_SUCCESS) { + if (wolfSSL_ASN1_STRING_set(data, &extKeyUseFlag, (int)sizeof(byte)) != + WOLFSSL_SUCCESS || + wolfSSL_X509_EXTENSION_set_data(ext, data) != WOLFSSL_SUCCESS) { wolfCLU_LogError("error setting the extended key use"); wolfSSL_X509_EXTENSION_free(ext); ext = NULL; @@ -649,13 +674,13 @@ static WOLFSSL_X509_EXTENSION* wolfCLU_parseExtKeyUsage(char* str, int crit) * derived by hashing the public key held in 'x509'. * * returns the new extension on success, NULL on failure */ -static WOLFSSL_X509_EXTENSION* wolfCLU_parseSubjectKeyID(char* str, int crit, - WOLFSSL_X509* x509) +static WOLFSSL_X509_EXTENSION *wolfCLU_parseSubjectKeyID(char *str, int crit, + WOLFSSL_X509 *x509) { WOLFSSL_X509_EXTENSION *ext = NULL; WOLFSSL_EVP_PKEY *pkey = NULL; - char* word, *end; - char* deli = (char*)","; + char *word, *end; + char *deli = (char *)","; /* separates "the config named no key id" from "deriving one failed", which * both left ext NULL and reported the former */ int sawHash = 0; @@ -664,7 +689,7 @@ static WOLFSSL_X509_EXTENSION* wolfCLU_parseSubjectKeyID(char* str, int crit, return NULL; for (word = XSTRTOK(str, deli, &end); word != NULL; - word = XSTRTOK(NULL, deli, &end)) { + word = XSTRTOK(NULL, deli, &end)) { word = wolfCLU_trimToken(word); /* the critical key word was already handled by the caller */ @@ -674,7 +699,7 @@ static WOLFSSL_X509_EXTENSION* wolfCLU_parseSubjectKeyID(char* str, int crit, if (XSTRCMP(word, "hash") == 0) { WOLFSSL_ASN1_STRING *data; - int keyType; + int keyType; void *key = NULL; /* Cert is several kilobytes, so it is scoped to this branch */ Cert cert; /* temporary to use existing subject key id api */ @@ -686,8 +711,8 @@ static WOLFSSL_X509_EXTENSION* wolfCLU_parseSubjectKeyID(char* str, int crit, continue; } - if (wolfCLU_getPubKeyForId(x509, &pkey, &key, &keyType) - != WOLFCLU_SUCCESS) { + if (wolfCLU_getPubKeyForId(x509, &pkey, &key, &keyType) != + WOLFCLU_SUCCESS) { return NULL; } @@ -705,16 +730,16 @@ static WOLFSSL_X509_EXTENSION* wolfCLU_parseSubjectKeyID(char* str, int crit, wolfCLU_LogError("out of memory building the skid"); } else { - if (wolfSSL_ASN1_STRING_set(data, cert.skid, cert.skidSz) - != WOLFSSL_SUCCESS) { + if (wolfSSL_ASN1_STRING_set(data, cert.skid, cert.skidSz) != + WOLFSSL_SUCCESS) { wolfCLU_LogError("error setting the skid"); } else { ext = wolfSSL_X509V3_EXT_i2d(NID_subject_key_identifier, - crit, data); + crit, data); if (ext == NULL) { wolfCLU_LogError("error encoding the skid " - "extension"); + "extension"); } } wolfSSL_ASN1_STRING_free(data); @@ -724,8 +749,7 @@ static WOLFSSL_X509_EXTENSION* wolfCLU_parseSubjectKeyID(char* str, int crit, pkey = NULL; } else { - wolfCLU_LogError("unsupported subject key identifier \"%s\"", - word); + wolfCLU_LogError("unsupported subject key identifier \"%s\"", word); if (ext != NULL) { wolfSSL_X509_EXTENSION_free(ext); } @@ -737,7 +761,7 @@ static WOLFSSL_X509_EXTENSION* wolfCLU_parseSubjectKeyID(char* str, int crit, * while deriving the key id has already logged its own cause */ if (ext == NULL && !sawHash) { wolfCLU_LogError("no subject key identifier value found, " - "expected \"hash\""); + "expected \"hash\""); } return ext; @@ -749,19 +773,19 @@ static WOLFSSL_X509_EXTENSION* wolfCLU_parseSubjectKeyID(char* str, int crit, * matching wolfCLU_parseExtKeyUsage(). * * returns the new extension on success, NULL on failure */ -static WOLFSSL_X509_EXTENSION* wolfCLU_parseKeyUsage(char* str, int crit) +static WOLFSSL_X509_EXTENSION *wolfCLU_parseKeyUsage(char *str, int crit) { WOLFSSL_ASN1_STRING *data; WOLFSSL_X509_EXTENSION *ext = NULL; - char* word, *end; - char* deli = (char*)","; + char *word, *end; + char *deli = (char *)","; word16 keyUseFlag = 0; if (str == NULL) return NULL; for (word = XSTRTOK(str, deli, &end); word != NULL; - word = XSTRTOK(NULL, deli, &end)) { + word = XSTRTOK(NULL, deli, &end)) { word = wolfCLU_trimToken(word); /* the critical key word was already handled by the caller */ @@ -772,7 +796,7 @@ static WOLFSSL_X509_EXTENSION* wolfCLU_parseKeyUsage(char* str, int crit) keyUseFlag |= KEYUSE_DIGITAL_SIG; } else if (XSTRCMP(word, "nonRepudiation") == 0 || - XSTRCMP(word, "contentCommitment") == 0) { + XSTRCMP(word, "contentCommitment") == 0) { keyUseFlag |= KEYUSE_CONTENT_COMMIT; } else if (XSTRCMP(word, "keyEncipherment") == 0) { @@ -797,8 +821,8 @@ static WOLFSSL_X509_EXTENSION* wolfCLU_parseKeyUsage(char* str, int crit) keyUseFlag |= KEYUSE_DECIPHER_ONLY; } else { - wolfCLU_LogError("unsupported key usage \"%s\"", XSTRLEN(word) ? - word : ""); + wolfCLU_LogError("unsupported key usage \"%s\"", + XSTRLEN(word) ? word : "\"\""); return NULL; } } @@ -810,8 +834,8 @@ static WOLFSSL_X509_EXTENSION* wolfCLU_parseKeyUsage(char* str, int crit) data = wolfSSL_ASN1_STRING_new(); if (data != NULL) { - if (wolfSSL_ASN1_STRING_set(data, (byte*)&keyUseFlag, sizeof(word16)) - != WOLFSSL_SUCCESS) { + if (wolfSSL_ASN1_STRING_set(data, (byte *)&keyUseFlag, + sizeof(word16)) != WOLFSSL_SUCCESS) { wolfCLU_LogError("error setting the key use"); } else { @@ -827,7 +851,7 @@ static WOLFSSL_X509_EXTENSION* wolfCLU_parseKeyUsage(char* str, int crit) * generic extension, so nothing is handed back to the caller. 'str' is * tokenized in place, callers pass a writable copy. * return WOLFCLU_SUCCESS on success */ -static int wolfCLU_parseSubjectAltNames(WOLFSSL_X509* x509, char* str, int crit) +static int wolfCLU_parseSubjectAltNames(WOLFSSL_X509 *x509, char *str, int crit) { #ifndef WOLFSSL_ALT_NAMES (void)x509; @@ -836,8 +860,9 @@ static int wolfCLU_parseSubjectAltNames(WOLFSSL_X509* x509, char* str, int crit) /* alt names were explicitly requested, so fail rather than silently * emitting a cert without them */ - wolfCLU_LogError("wolfSSL not compiled with alt name support " - "(WOLFSSL_ALT_NAMES); cannot apply requested subjectAltName"); + wolfCLU_LogError( + "wolfSSL not compiled with alt name support " + "(WOLFSSL_ALT_NAMES); cannot apply requested subjectAltName"); return NOT_COMPILED_IN; #else /* wolfSSL has no way to mark alt names critical. Said out loud because @@ -846,7 +871,7 @@ static int wolfCLU_parseSubjectAltNames(WOLFSSL_X509* x509, char* str, int crit) * itself is skipped wherever it appears, by the tokenizer below. */ if (crit) { WOLFCLU_LOG(WOLFCLU_L0, "Warning: wolfSSL cannot mark subjectAltName " - "critical, emitting it as non-critical"); + "critical, emitting it as non-critical"); } return wolfCLU_setInlineSubjectAltNames(x509, str); @@ -862,10 +887,10 @@ static int wolfCLU_parseSubjectAltNames(WOLFSSL_X509* x509, char* str, int crit) * hands the operator a ten digit number that matches nothing they can look * up, and it reads like memory corruption. Prefer the long name, fall back to * the dotted OID, and only use the number when neither is available. */ -static const char* wolfCLU_extNidName(int nid, char* buf, int bufSz) +static const char *wolfCLU_extNidName(int nid, char *buf, int bufSz) { - const char* ln = wolfSSL_OBJ_nid2ln(nid); - WOLFSSL_ASN1_OBJECT* obj; + const char *ln = wolfSSL_OBJ_nid2ln(nid); + WOLFSSL_ASN1_OBJECT *obj; if (ln != NULL) { return ln; @@ -887,10 +912,11 @@ static const char* wolfCLU_extNidName(int nid, char* buf, int bufSz) return buf; } -static int wolfCLU_extNotSupported(const char* name) +static int wolfCLU_extNotSupported(const char *name) { wolfCLU_LogError("extension %s is not supported by wolfSSL when creating " - "a certificate", name); + "a certificate", + name); return WOLFCLU_FATAL_ERROR; } @@ -898,14 +924,14 @@ static int wolfCLU_extNotSupported(const char* name) * certificate that will sign it, or NULL when it signs itself or the signer is * not known here; only the authority key identifier uses it. * return WOLFCLU_SUCCESS on success */ -static int wolfCLU_parseExtension(WOLFSSL_X509* x509, char* str, int nid, - WOLFSSL_X509* issuer) +static int wolfCLU_parseExtension(WOLFSSL_X509 *x509, char *str, int nid, + WOLFSSL_X509 *issuer) { char nameBuf[80]; WOLFSSL_X509_EXTENSION *ext = NULL; - int ret = WOLFCLU_SUCCESS; - int crit = 0; + int ret = WOLFCLU_SUCCESS; + int crit = 0; if (x509 == NULL || str == NULL) { return BAD_FUNC_ARG; @@ -930,9 +956,10 @@ static int wolfCLU_parseExtension(WOLFSSL_X509* x509, char* str, int nid, * derived from the subject's own key, which needs a self signed * cert. (The -CA path in clu_request_setup.c never lands here.) */ if (issuer == NULL && !wolfCLU_isSelfIssued(x509)) { - WOLFCLU_LOG(WOLFCLU_L0, "Skipping authority key identifier, " - "deriving it for a certificate that is not self " - "issued needs the issuing certificate"); + WOLFCLU_LOG(WOLFCLU_L0, + "Skipping authority key identifier, " + "deriving it for a certificate that is not self " + "issued needs the issuing certificate"); return WOLFCLU_SUCCESS; } ret = wolfCLU_parseAuthorityKeyId(str, crit, x509, issuer, &ext); @@ -947,9 +974,24 @@ static int wolfCLU_parseExtension(WOLFSSL_X509* x509, char* str, int nid, } break; case NID_key_usage: + /* EncodeExtensions() sets the criticality flag unconditionally + * whenever a key usage is present, and CopyX509ToCert() never + * carries keyUsageCrit across, so the emitted extension is + * critical either way. Say so when the config asked for the + * opposite rather than letting the two silently disagree. */ + if (!crit) { + WOLFCLU_LOG(WOLFCLU_L0, "Note: wolfSSL always emits keyUsage " + "as critical when creating a certificate"); + } ext = wolfCLU_parseKeyUsage(str, crit); break; case NID_ext_key_usage: + if (crit) { + WOLFCLU_LOG(WOLFCLU_L0, + "Warning: wolfSSL cannot mark " + "extendedKeyUsage critical, emitting it as " + "non-critical"); + } ext = wolfCLU_parseExtKeyUsage(str, crit); break; /* alt names are stored on the x509 struct instead of being added as @@ -975,23 +1017,26 @@ static int wolfCLU_parseExtension(WOLFSSL_X509* x509, char* str, int nid, return wolfCLU_extNotSupported("inhibitAnyPolicy"); default: - wolfCLU_LogError("unknown / unsupported extension %s", - wolfCLU_extNidName(nid, nameBuf, (int)sizeof(nameBuf))); + wolfCLU_LogError( + "unknown / unsupported extension %s", + wolfCLU_extNidName(nid, nameBuf, (int)sizeof(nameBuf))); return WOLFCLU_FATAL_ERROR; } /* note that 'str' has been tokenized in place by now, so it is not worth * echoing back in the error */ if (ext == NULL) { - wolfCLU_LogError("unable to create extension %s", - wolfCLU_extNidName(nid, nameBuf, (int)sizeof(nameBuf))); + wolfCLU_LogError( + "unable to create extension %s", + wolfCLU_extNidName(nid, nameBuf, (int)sizeof(nameBuf))); return WOLFCLU_FATAL_ERROR; } /* wolfSSL only supports appending, loc must be negative */ if (wolfSSL_X509_add_ext(x509, ext, -1) != WOLFSSL_SUCCESS) { - wolfCLU_LogError("error adding extension %s", - wolfCLU_extNidName(nid, nameBuf, (int)sizeof(nameBuf))); + wolfCLU_LogError( + "error adding extension %s", + wolfCLU_extNidName(nid, nameBuf, (int)sizeof(nameBuf))); ret = WOLFCLU_FATAL_ERROR; } wolfSSL_X509_EXTENSION_free(ext); @@ -1003,15 +1048,15 @@ static int wolfCLU_parseExtension(WOLFSSL_X509* x509, char* str, int nid, * with value 'value' to x509, shared by the config and -addext paths. * return WOLFCLU_SUCCESS on success */ #ifdef WOLFSSL_ALT_NAMES -static int wolfCLU_addAltName(WOLFSSL_X509* x509, const char* name, - const char* value) +static int wolfCLU_addAltName(WOLFSSL_X509 *x509, const char *name, + const char *value) { int ret = WOLFCLU_SUCCESS; - WOLFSSL_ASN1_STRING *ipStr = NULL; + WOLFSSL_ASN1_STRING *ipStr = NULL; WOLFSSL_ASN1_OBJECT *ridObj = NULL; - char *token, *ptr, *s = NULL; - int sSz = 0; - int type = 0; + char *token, *ptr, *s = NULL; + int sSz = 0; + int type = 0; byte oid[ASN1_OID_DOTTED_MAX_SZ]; word32 oidSz = ASN1_OID_DOTTED_MAX_SZ; word32 decodedCount = 0; @@ -1021,39 +1066,37 @@ static int wolfCLU_addAltName(WOLFSSL_X509* x509, const char* name, ipStr = wolfSSL_a2i_IPADDRESS(value); if (ipStr != NULL) { - s = (char*)wolfSSL_ASN1_STRING_data(ipStr); + s = (char *)wolfSSL_ASN1_STRING_data(ipStr); sSz = wolfSSL_ASN1_STRING_length(ipStr); type = ASN_IP_TYPE; - } else { wolfCLU_LogError("bad IP found %s", value); return WOLFCLU_FATAL_ERROR; } - } else if (XSTRNCMP(name, "DNS", 3) == 0) { type = ASN_DNS_TYPE; - s = (char*)value; + s = (char *)value; sSz = (int)XSTRLEN(value); } else if (XSTRNCMP(name, "URI", 3) == 0) { type = ASN_URI_TYPE; - s = (char*)value; + s = (char *)value; sSz = (int)XSTRLEN(value); } else if (XSTRNCMP(name, "RID", 3) == 0) { if ((ridObj = wolfSSL_OBJ_txt2obj(value, 0)) == NULL) { - #if defined(HAVE_OID_ENCODING) && !defined(NO_WC_ENCODE_OBJECT_ID) +#if defined(HAVE_OID_ENCODING) && !defined(NO_WC_ENCODE_OBJECT_ID) /* If RID value is not named OID, manually encode * dotted OID into byte array. Tokenize a copy so the * original value stays intact for error messages. */ - int ridLen = (int)XSTRLEN(value); - char* ridDup = (char*)XMALLOC(ridLen + 1, NULL, - DYNAMIC_TYPE_TMP_BUFFER); + int ridLen = (int)XSTRLEN(value); + char *ridDup = + (char *)XMALLOC(ridLen + 1, NULL, DYNAMIC_TYPE_TMP_BUFFER); if (ridDup == NULL) { wolfCLU_LogError("Failed to allocate memory for RID"); return MEMORY_E; @@ -1063,13 +1106,13 @@ static int wolfCLU_addAltName(WOLFSSL_X509* x509, const char* name, token = XSTRTOK(ridDup, ".", &ptr); while (token != NULL) { - char* digit; + char *digit; int n; if (decodedCount >= ASN1_OID_DOTTED_MAX_SZ) { wolfCLU_LogError("RID has too many components " - "(max %d): %s", - ASN1_OID_DOTTED_MAX_SZ, value); + "(max %d): %s", + ASN1_OID_DOTTED_MAX_SZ, value); ret = WOLFCLU_FATAL_ERROR; break; } @@ -1079,8 +1122,8 @@ static int wolfCLU_addAltName(WOLFSSL_X509* x509, const char* name, for (digit = token; *digit != '\0'; digit++) { if (*digit < '0' || *digit > '9') { wolfCLU_LogError("Non-numeric RID " - "component '%s' in: %s", token, - value); + "component '%s' in: %s", + token, value); ret = WOLFCLU_FATAL_ERROR; break; } @@ -1091,7 +1134,8 @@ static int wolfCLU_addAltName(WOLFSSL_X509* x509, const char* name, n = XATOI(token); if (n < 0 || n > 0xFFFF) { wolfCLU_LogError("RID component out of range " - "[0, 65535]: %s", token); + "[0, 65535]: %s", + token); ret = WOLFCLU_FATAL_ERROR; break; } @@ -1106,16 +1150,15 @@ static int wolfCLU_addAltName(WOLFSSL_X509* x509, const char* name, return ret; } - if (wc_EncodeObjectId(decoded, decodedCount, oid, &oidSz) - == 0) { - s = (char*)oid; + if (wc_EncodeObjectId(decoded, decodedCount, oid, &oidSz) == 0) { + s = (char *)oid; sSz = (int)oidSz; } else { wolfCLU_LogError("bad RID found %s", value); return WOLFCLU_FATAL_ERROR; } - #else +#else (void)token; (void)ptr; (void)decoded; @@ -1124,13 +1167,13 @@ static int wolfCLU_addAltName(WOLFSSL_X509* x509, const char* name, (void)oidSz; wolfCLU_LogError("Couldn't encode RID. OID encoding is not" - " compiled in"); + " compiled in"); return WOLFCLU_FATAL_ERROR; - #endif +#endif } else { - s = (char*)wolfSSL_OBJ_get0_data(ridObj); + s = (char *)wolfSSL_OBJ_get0_data(ridObj); sSz = (int)wolfSSL_OBJ_length(ridObj); } @@ -1140,7 +1183,7 @@ static int wolfCLU_addAltName(WOLFSSL_X509* x509, const char* name, else if (XSTRNCMP(name, "email", 5) == 0) { type = ASN_RFC822_TYPE; - s = (char*)value; + s = (char *)value; sSz = (int)XSTRLEN(value); } @@ -1149,8 +1192,7 @@ static int wolfCLU_addAltName(WOLFSSL_X509* x509, const char* name, return WOLFCLU_FATAL_ERROR; } - if (wolfSSL_X509_add_altname_ex(x509, s, sSz, type) - != WOLFSSL_SUCCESS) { + if (wolfSSL_X509_add_altname_ex(x509, s, sSz, type) != WOLFSSL_SUCCESS) { wolfCLU_LogError("error adding alt name %s", value); ret = WOLFCLU_FATAL_ERROR; } @@ -1166,8 +1208,8 @@ static int wolfCLU_addAltName(WOLFSSL_X509* x509, const char* name, #endif /* WOLFSSL_ALT_NAMES */ /* return WOLFCLU_SUCCESS on success, searches for IP's and DNS's */ -static int wolfCLU_setAltNames(WOLFSSL_X509* x509, WOLFSSL_CONF* conf, - char* sect) +static int wolfCLU_setAltNames(WOLFSSL_X509 *x509, WOLFSSL_CONF *conf, + char *sect) { WOLFSSL_STACK *altNames; int i, ret = WOLFCLU_SUCCESS; @@ -1184,9 +1226,11 @@ static int wolfCLU_setAltNames(WOLFSSL_X509* x509, WOLFSSL_CONF* conf, /* the config named an alt name section, so fail rather than silently * emitting a cert without those names */ - wolfCLU_LogError("wolfSSL not compiled with alt name support " - "(WOLFSSL_ALT_NAMES); cannot apply requested subjectAltName " - "section \"%s\"", sect); + wolfCLU_LogError( + "wolfSSL not compiled with alt name support " + "(WOLFSSL_ALT_NAMES); cannot apply requested subjectAltName " + "section \"%s\"", + sect); ret = NOT_COMPILED_IN; #else altNames = wolfSSL_NCONF_get_section(conf, sect); @@ -1200,7 +1244,7 @@ static int wolfCLU_setAltNames(WOLFSSL_X509* x509, WOLFSSL_CONF* conf, c = wolfSSL_sk_CONF_VALUE_value(altNames, i); if (c == NULL) { WOLFCLU_LOG(WOLFCLU_L0, "Unexpected null value found in alt " - "names stack"); + "names stack"); ret = WOLFCLU_FATAL_ERROR; break; } @@ -1226,11 +1270,12 @@ static int wolfCLU_setAltNames(WOLFSSL_X509* x509, WOLFSSL_CONF* conf, * Buffer is tokenized in place, so callers pass a writable string. Returns * WOLFCLU_SUCCESS, or WOLFCLU_FATAL_ERROR on a malformed entry so a bad SAN is * never silently ignored. */ -static int wolfCLU_setInlineSubjectAltNames(WOLFSSL_X509* x509, char* val) +static int wolfCLU_setInlineSubjectAltNames(WOLFSSL_X509 *x509, char *val) { int ret = WOLFCLU_SUCCESS; - char* token; - char* ptr = NULL; + int sawName = 0; + char *token; + char *ptr = NULL; if (x509 == NULL || val == NULL) { return WOLFCLU_FATAL_ERROR; @@ -1238,12 +1283,13 @@ static int wolfCLU_setInlineSubjectAltNames(WOLFSSL_X509* x509, char* val) token = XSTRTOK(val, ",", &ptr); while (token != NULL) { - char* colon; - char* value; + char *colon; + char *value; size_t len; /* trim whitespace around entries and trailing whitespace */ - while (*token == ' ' || *token == '\t' || *token == '\r' || *token == '\n') { + while (*token == ' ' || *token == '\t' || *token == '\r' || + *token == '\n') { token++; } len = XSTRLEN(token); @@ -1262,7 +1308,8 @@ static int wolfCLU_setInlineSubjectAltNames(WOLFSSL_X509* x509, char* val) colon = XSTRSTR(token, ":"); if (colon == NULL) { wolfCLU_LogError("bad subjectAltName entry \"%s\", expected " - "TYPE:value", token); + "TYPE:value", + token); ret = WOLFCLU_FATAL_ERROR; break; } @@ -1271,7 +1318,8 @@ static int wolfCLU_setInlineSubjectAltNames(WOLFSSL_X509* x509, char* val) * at the correct boundary, so value needs no second trailing trim. */ /* drop whitespace between the colon and the value */ value = colon + 1; - while (*value == ' ' || *value == '\t' || *value == '\r' || *value == '\n') { + while (*value == ' ' || *value == '\t' || *value == '\r' || + *value == '\n') { value++; } @@ -1282,7 +1330,8 @@ static int wolfCLU_setInlineSubjectAltNames(WOLFSSL_X509* x509, char* val) break; } if (XSTRLEN(value) == 0) { - wolfCLU_LogError("bad subjectAltName entry: empty value for type \"%s\"", token); + wolfCLU_LogError( + "bad subjectAltName entry: empty value for type \"%s\"", token); ret = WOLFCLU_FATAL_ERROR; break; } @@ -1291,38 +1340,41 @@ static int wolfCLU_setInlineSubjectAltNames(WOLFSSL_X509* x509, char* val) if (ret != WOLFCLU_SUCCESS) { break; } + sawName = 1; token = XSTRTOK(NULL, ",", &ptr); } + + /* The value was empty or held only "critical". Reported rather than + * dropped, matching every sibling parser in this file. */ + if (ret == WOLFCLU_SUCCESS && !sawName) { + wolfCLU_LogError("no subjectAltName value found, expected TYPE:value"); + ret = WOLFCLU_FATAL_ERROR; + } + return ret; } #endif /* WOLFSSL_ALT_NAMES */ -/* Look 'key' up in the config section and hand its value to - * wolfCLU_parseExtension() as the extension 'nid'. The value is copied first: - * the extension parsers tokenize (and upper case) in place, and the string - * returned by wolfSSL_NCONF_get_string() belongs to the WOLFSSL_CONF. - * A key that is not present in the section is not an error. +/* Hand 'val' to wolfCLU_parseExtension() as the extension 'nid'. The value is + * copied first: the extension parsers tokenize (and upper case) in place, and + * the string returned by wolfSSL_NCONF_get_string() belongs to the + * WOLFSSL_CONF. 'key' only names the value in the out of memory message. * return WOLFCLU_SUCCESS on success */ -static int wolfCLU_setExtensionFromConf(WOLFSSL_X509* x509, WOLFSSL_CONF* conf, - char* sect, const char* key, int nid, WOLFSSL_X509* issuer) +static int wolfCLU_setExtensionFromValue(WOLFSSL_X509 *x509, const char *val, + const char *key, int nid, + WOLFSSL_X509 *issuer) { - char* current; - char* dup; - int len; - int ret; - - current = wolfSSL_NCONF_get_string(conf, sect, key); - if (current == NULL) { - return WOLFCLU_SUCCESS; /* not set in this section */ - } + char *dup; + int len; + int ret; - len = (int)XSTRLEN(current); - dup = (char*)XMALLOC(len + 1, NULL, DYNAMIC_TYPE_TMP_BUFFER); + len = (int)XSTRLEN(val); + dup = (char *)XMALLOC(len + 1, NULL, DYNAMIC_TYPE_TMP_BUFFER); if (dup == NULL) { wolfCLU_LogError("out of memory duplicating %s value", key); return MEMORY_E; } - XMEMCPY(dup, current, len + 1); + XMEMCPY(dup, val, len + 1); ret = wolfCLU_parseExtension(x509, dup, nid, issuer); XFREE(dup, NULL, DYNAMIC_TYPE_TMP_BUFFER); @@ -1330,49 +1382,69 @@ static int wolfCLU_setExtensionFromConf(WOLFSSL_X509* x509, WOLFSSL_CONF* conf, return ret; } +/* Look 'key' up in the config section and apply its value as extension 'nid'. + * A key that is not present in the section is not an error. + * return WOLFCLU_SUCCESS on success */ +static int wolfCLU_setExtensionFromConf(WOLFSSL_X509 *x509, WOLFSSL_CONF *conf, + char *sect, const char *key, int nid, + WOLFSSL_X509 *issuer) +{ + char *current; + + current = wolfSSL_NCONF_get_string(conf, sect, key); + if (current == NULL) { + return WOLFCLU_SUCCESS; /* not set in this section */ + } + + return wolfCLU_setExtensionFromValue(x509, current, key, nid, issuer); +} + /* return WOLFCLU_SUCCESS on success */ -int wolfCLU_setExtensions(WOLFSSL_X509* x509, WOLFSSL_CONF* conf, char* sect, - WOLFSSL_X509* issuer) +int wolfCLU_setExtensions(WOLFSSL_X509 *x509, WOLFSSL_CONF *conf, char *sect, + WOLFSSL_X509 *issuer) { char *current; - int ret = WOLFCLU_SUCCESS; + int ret = WOLFCLU_SUCCESS; if (sect == NULL) { return WOLFCLU_SUCCESS; /* none set */ } - ret = wolfCLU_setExtensionFromConf(x509, conf, sect, - "basicConstraints", NID_basic_constraints, issuer); + ret = wolfCLU_setExtensionFromConf(x509, conf, sect, "basicConstraints", + NID_basic_constraints, issuer); if (ret == WOLFCLU_SUCCESS) { ret = wolfCLU_setExtensionFromConf(x509, conf, sect, - "subjectKeyIdentifier", NID_subject_key_identifier, issuer); + "subjectKeyIdentifier", + NID_subject_key_identifier, issuer); } if (ret == WOLFCLU_SUCCESS) { - ret = wolfCLU_setExtensionFromConf(x509, conf, sect, - "authorityKeyIdentifier", NID_authority_key_identifier, issuer); + ret = wolfCLU_setExtensionFromConf( + x509, conf, sect, "authorityKeyIdentifier", + NID_authority_key_identifier, issuer); } if (ret == WOLFCLU_SUCCESS) { - ret = wolfCLU_setExtensionFromConf(x509, conf, sect, - "keyUsage", NID_key_usage, issuer); + ret = wolfCLU_setExtensionFromConf(x509, conf, sect, "keyUsage", + NID_key_usage, issuer); } if (ret == WOLFCLU_SUCCESS) { - ret = wolfCLU_setExtensionFromConf(x509, conf, sect, - "extendedKeyUsage", NID_ext_key_usage, issuer); + ret = wolfCLU_setExtensionFromConf(x509, conf, sect, "extendedKeyUsage", + NID_ext_key_usage, issuer); } if (ret == WOLFCLU_SUCCESS) { + /* looked up here rather than through wolfCLU_setExtensionFromConf() + * because the "@section" form needs the conf handle to resolve the + * section, which wolfCLU_parseExtension() does not have */ current = wolfSSL_NCONF_get_string(conf, sect, "subjectAltName"); if (current != NULL && current[0] == '@') { - /* the "@section" form needs the conf handle to look the section - * up, which wolfCLU_parseExtension() does not have */ ret = wolfCLU_setAltNames(x509, conf, current + 1); } else if (current != NULL) { - ret = wolfCLU_setExtensionFromConf(x509, conf, sect, + ret = wolfCLU_setExtensionFromValue(x509, current, "subjectAltName", NID_subject_alt_name, issuer); } } @@ -1382,37 +1454,37 @@ int wolfCLU_setExtensions(WOLFSSL_X509* x509, WOLFSSL_CONF* conf, char* sect, /* the extension names -addext accepts, and the nid each one routes to */ typedef struct WOLFCLU_ADDEXT_MAP { - const char* name; - int nid; + const char *name; + int nid; } WOLFCLU_ADDEXT_MAP; static const WOLFCLU_ADDEXT_MAP wolfCLU_addExtMap[] = { - {"basicConstraints", NID_basic_constraints}, - {"subjectKeyIdentifier", NID_subject_key_identifier}, - {"authorityKeyIdentifier", NID_authority_key_identifier}, - {"subjectAltName", NID_subject_alt_name}, - {"issuerAltName", NID_issuer_alt_name}, - {"keyUsage", NID_key_usage}, - {"extendedKeyUsage", NID_ext_key_usage}, - {"nameConstraints", NID_name_constraints}, - {"policyConstraints", NID_policy_constraints}, - {"policyMappings", NID_policy_mappings}, - {"inhibitAnyPolicy", NID_inhibit_any_policy} + { "basicConstraints", NID_basic_constraints }, + { "subjectKeyIdentifier", NID_subject_key_identifier }, + { "authorityKeyIdentifier", NID_authority_key_identifier }, + { "subjectAltName", NID_subject_alt_name }, + { "issuerAltName", NID_issuer_alt_name }, + { "keyUsage", NID_key_usage }, + { "extendedKeyUsage", NID_ext_key_usage }, + { "nameConstraints", NID_name_constraints }, + { "policyConstraints", NID_policy_constraints }, + { "policyMappings", NID_policy_mappings }, + { "inhibitAnyPolicy", NID_inhibit_any_policy } }; /* parse a command line "-addext name=value" and apply it to x509, i.e. * "subjectAltName=DNS:example.com,IP:10.0.0.1". * return WOLFCLU_SUCCESS on success */ -int wolfCLU_parseAddExt(WOLFSSL_X509* x509, char* addExt) +int wolfCLU_parseAddExt(WOLFSSL_X509 *x509, char *addExt) { - int ret; - int len; - int nameSz; - int nid = 0; + int ret; + int len; + int nameSz; + int nid = 0; size_t i; - char* dup; - char* name; - char* value; + char *dup; + char *name; + char *value; if (x509 == NULL || addExt == NULL) { return BAD_FUNC_ARG; @@ -1427,7 +1499,7 @@ int wolfCLU_parseAddExt(WOLFSSL_X509* x509, char* addExt) /* work on a writable copy, the extension parsers tokenize in place and * the original argv string should be left alone */ len = (int)XSTRLEN(addExt); - dup = (char*)XMALLOC(len + 1, NULL, DYNAMIC_TYPE_TMP_BUFFER); + dup = (char *)XMALLOC(len + 1, NULL, DYNAMIC_TYPE_TMP_BUFFER); if (dup == NULL) { return MEMORY_E; } @@ -1439,11 +1511,11 @@ int wolfCLU_parseAddExt(WOLFSSL_X509* x509, char* addExt) * starts with a shorter one, i.e. "keyUsagePeriod", from matching. */ nameSz = (int)(value - addExt); dup[nameSz] = '\0'; - name = wolfCLU_trimToken(dup); + name = wolfCLU_trimToken(dup); value = dup + nameSz + 1; for (i = 0; i < sizeof(wolfCLU_addExtMap) / sizeof(wolfCLU_addExtMap[0]); - i++) { + i++) { if (XSTRCMP(name, wolfCLU_addExtMap[i].name) == 0) { nid = wolfCLU_addExtMap[i].nid; break; @@ -1464,8 +1536,8 @@ int wolfCLU_parseAddExt(WOLFSSL_X509* x509, char* addExt) } #else -int wolfCLU_setExtensions(WOLFSSL_X509* x509, WOLFSSL_CONF* conf, char* sect, - WOLFSSL_X509* issuer) +int wolfCLU_setExtensions(WOLFSSL_X509 *x509, WOLFSSL_CONF *conf, char *sect, + WOLFSSL_X509 *issuer) { (void)x509; (void)conf; @@ -1479,13 +1551,15 @@ int wolfCLU_setExtensions(WOLFSSL_X509* x509, WOLFSSL_CONF* conf, char* sect, /* If not compiled with WOLFSSL_CERT_EXT, fail so certs can be built as * intended by user. */ - wolfCLU_LogError("wolfSSL not compiled with cert extensions " - "(WOLFSSL_CERT_EXT); cannot apply requested x509_extensions " - "section \"%s\"", sect); + wolfCLU_LogError( + "wolfSSL not compiled with cert extensions " + "(WOLFSSL_CERT_EXT); cannot apply requested x509_extensions " + "section \"%s\"", + sect); return NOT_COMPILED_IN; } -int wolfCLU_parseAddExt(WOLFSSL_X509* x509, char* addExt) +int wolfCLU_parseAddExt(WOLFSSL_X509 *x509, char *addExt) { (void)x509; (void)addExt; @@ -1499,24 +1573,24 @@ int wolfCLU_parseAddExt(WOLFSSL_X509* x509, char* addExt) #define DEFAULT_STR_SZ 9 #define MIN_MAX_STR_SZ 5 -static int CheckDisName(WOLFSSL_CONF* conf, char* sect, WOLFSSL_X509_NAME* name, - const char* str, int nid, int strType, int noPrompt) +static int CheckDisName(WOLFSSL_CONF *conf, char *sect, WOLFSSL_X509_NAME *name, + const char *str, int nid, int strType, int noPrompt) { - int ret = WOLFCLU_SUCCESS; + int ret = WOLFCLU_SUCCESS; long mn = 0; long mx = 0; FILE *fout = stdout; FILE *fin = stdin; /* defaulting to stdin but using a fd variable to make it * easy for expanding to other inputs */ - char* curnt = NULL; - char* deflt = NULL; - char *in = NULL; - size_t inSz; - int lineRet; + char *curnt = NULL; + char *deflt = NULL; + char *in = NULL; + size_t inSz; + int lineRet; - char* deflt_str = NULL; - char* mn_str = NULL; - char* mx_str = NULL; + char *deflt_str = NULL; + char *mn_str = NULL; + char *mx_str = NULL; if (noPrompt) { curnt = wolfSSL_NCONF_get_string(conf, sect, str); @@ -1527,8 +1601,8 @@ static int CheckDisName(WOLFSSL_CONF* conf, char* sect, WOLFSSL_X509_NAME* name, } inSz = (int)XSTRLEN(str); - deflt_str = (char*)XMALLOC(inSz + DEFAULT_STR_SZ, NULL, - DYNAMIC_TYPE_TMP_BUFFER); + deflt_str = + (char *)XMALLOC(inSz + DEFAULT_STR_SZ, NULL, DYNAMIC_TYPE_TMP_BUFFER); if (deflt_str == NULL) { ret = WOLFCLU_FATAL_ERROR; } @@ -1538,8 +1612,8 @@ static int CheckDisName(WOLFSSL_CONF* conf, char* sect, WOLFSSL_X509_NAME* name, XSTRNCAT(deflt_str, "_default", inSz + DEFAULT_STR_SZ); } - mn_str = (char*)XMALLOC(inSz + MIN_MAX_STR_SZ, NULL, - DYNAMIC_TYPE_TMP_BUFFER); + mn_str = + (char *)XMALLOC(inSz + MIN_MAX_STR_SZ, NULL, DYNAMIC_TYPE_TMP_BUFFER); if (mn_str == NULL) { ret = WOLFCLU_FATAL_ERROR; } @@ -1549,8 +1623,8 @@ static int CheckDisName(WOLFSSL_CONF* conf, char* sect, WOLFSSL_X509_NAME* name, XSTRNCAT(mn_str, "_min", inSz + MIN_MAX_STR_SZ); } - mx_str = (char*)XMALLOC(inSz + MIN_MAX_STR_SZ, NULL, - DYNAMIC_TYPE_TMP_BUFFER); + mx_str = + (char *)XMALLOC(inSz + MIN_MAX_STR_SZ, NULL, DYNAMIC_TYPE_TMP_BUFFER); if (mx_str == NULL) { ret = WOLFCLU_FATAL_ERROR; } @@ -1564,7 +1638,7 @@ static int CheckDisName(WOLFSSL_CONF* conf, char* sect, WOLFSSL_X509_NAME* name, curnt = wolfSSL_NCONF_get_string(conf, sect, str); if (curnt != NULL) { deflt = wolfSSL_NCONF_get_string(conf, sect, deflt_str); - fprintf(fout, "%s [%s] : ", curnt, (deflt)?deflt:""); + fprintf(fout, "%s [%s] : ", curnt, (deflt) ? deflt : ""); lineRet = wolfCLU_getline(&in, &inSz, fin); if (lineRet == WOLFCLU_FATAL_ERROR) { @@ -1577,16 +1651,20 @@ static int CheckDisName(WOLFSSL_CONF* conf, char* sect, WOLFSSL_X509_NAME* name, if (deflt && XSTRCMP(deflt, ".") != 0) { if (wolfSSL_NCONF_get_number(conf, sect, mx_str, &mx) == - WOLFSSL_SUCCESS && (long)XSTRLEN(deflt) > mx) { + WOLFSSL_SUCCESS && + (long)XSTRLEN(deflt) > mx) { WOLFCLU_LOG(WOLFCLU_E0, - "Name %s is larger than max %ld", deflt, mx); + "Name %s is larger than max %ld", deflt, + mx); ret = WOLFCLU_FATAL_ERROR; } if (wolfSSL_NCONF_get_number(conf, sect, mn_str, &mn) == - WOLFSSL_SUCCESS && (long)XSTRLEN(deflt) < mn) { + WOLFSSL_SUCCESS && + (long)XSTRLEN(deflt) < mn) { WOLFCLU_LOG(WOLFCLU_E0, - "Name %s is smaller than min %ld", deflt, mn); + "Name %s is smaller than min %ld", deflt, + mn); ret = WOLFCLU_FATAL_ERROR; } @@ -1595,7 +1673,8 @@ static int CheckDisName(WOLFSSL_CONF* conf, char* sect, WOLFSSL_X509_NAME* name, } } } - XFREE(in, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); in = NULL; + XFREE(in, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + in = NULL; } } @@ -1608,10 +1687,10 @@ static int CheckDisName(WOLFSSL_CONF* conf, char* sect, WOLFSSL_X509_NAME* name, /* extracts the distinguished names from the conf file and puts them into * the x509 * returns WOLFCLU_SUCCESS on success */ -static int wolfCLU_setDisNames(WOLFSSL_X509* x509, WOLFSSL_CONF* conf, - char* sect, int noPrompt) +static int wolfCLU_setDisNames(WOLFSSL_X509 *x509, WOLFSSL_CONF *conf, + char *sect, int noPrompt) { - int i; + int i; int ret = WOLFCLU_SUCCESS; char buf[MAX_DIST_NAME]; WOLFSSL_X509_NAME *name; @@ -1627,18 +1706,18 @@ static int wolfCLU_setDisNames(WOLFSSL_X509* x509, WOLFSSL_CONF* conf, } fprintf(fout, "Enter '.' will result in the field being " - "skipped.\nExamples of inputs are provided as [*]\n"); + "skipped.\nExamples of inputs are provided as [*]\n"); ret = CheckDisName(conf, sect, name, "countryName", NID_countryName, - CTC_PRINTABLE, noPrompt); + CTC_PRINTABLE, noPrompt); if (ret == WOLFCLU_SUCCESS) { ret = CheckDisName(conf, sect, name, "stateOrProvinceName", - NID_stateOrProvinceName, CTC_UTF8, noPrompt); + NID_stateOrProvinceName, CTC_UTF8, noPrompt); } if (ret == WOLFCLU_SUCCESS) { ret = CheckDisName(conf, sect, name, "localityName", NID_localityName, - CTC_UTF8, noPrompt); + CTC_UTF8, noPrompt); } @@ -1646,14 +1725,14 @@ static int wolfCLU_setDisNames(WOLFSSL_X509* x509, WOLFSSL_CONF* conf, * finding an entry */ if (ret == WOLFCLU_SUCCESS) { ret = CheckDisName(conf, sect, name, "organizationName", - NID_organizationName, CTC_UTF8, noPrompt); + NID_organizationName, CTC_UTF8, noPrompt); } if (ret == WOLFCLU_SUCCESS) { for (i = 0; i < 10; i++) { XSNPRINTF(buf, sizeof(buf), "%d.organizationName", i); ret = CheckDisName(conf, sect, name, buf, NID_organizationName, - CTC_UTF8, noPrompt); + CTC_UTF8, noPrompt); if (ret != WOLFCLU_SUCCESS) { break; } @@ -1662,47 +1741,47 @@ static int wolfCLU_setDisNames(WOLFSSL_X509* x509, WOLFSSL_CONF* conf, if (ret == WOLFCLU_SUCCESS) { ret = CheckDisName(conf, sect, name, "organizationalUnitName", - NID_organizationalUnitName, CTC_UTF8, noPrompt); + NID_organizationalUnitName, CTC_UTF8, noPrompt); } if (ret == WOLFCLU_SUCCESS) { ret = CheckDisName(conf, sect, name, "commonName", NID_commonName, - CTC_UTF8, noPrompt); + CTC_UTF8, noPrompt); } if (ret == WOLFCLU_SUCCESS) { ret = CheckDisName(conf, sect, name, "CN", NID_commonName, CTC_UTF8, - noPrompt); + noPrompt); } if (ret == WOLFCLU_SUCCESS) { ret = CheckDisName(conf, sect, name, "emailAddress", NID_emailAddress, - CTC_UTF8, noPrompt); + CTC_UTF8, noPrompt); } if (ret == WOLFCLU_SUCCESS) { - ret = CheckDisName(conf, sect, name, "name", NID_name, - CTC_UTF8, noPrompt); + ret = CheckDisName(conf, sect, name, "name", NID_name, CTC_UTF8, + noPrompt); } if (ret == WOLFCLU_SUCCESS) { - ret = CheckDisName(conf, sect, name, "surname", NID_surname, - CTC_UTF8, noPrompt); + ret = CheckDisName(conf, sect, name, "surname", NID_surname, CTC_UTF8, + noPrompt); } if (ret == WOLFCLU_SUCCESS) { - ret = CheckDisName(conf, sect, name, "initials", NID_initials, - CTC_UTF8, noPrompt); + ret = CheckDisName(conf, sect, name, "initials", NID_initials, CTC_UTF8, + noPrompt); } if (ret == WOLFCLU_SUCCESS) { ret = CheckDisName(conf, sect, name, "givenName", NID_givenName, - CTC_UTF8, noPrompt); + CTC_UTF8, noPrompt); } if (ret == WOLFCLU_SUCCESS) { ret = CheckDisName(conf, sect, name, "dnQualifier", NID_dnQualifier, - CTC_UTF8, noPrompt); + CTC_UTF8, noPrompt); } if (ret == WOLFCLU_SUCCESS) { @@ -1714,7 +1793,7 @@ static int wolfCLU_setDisNames(WOLFSSL_X509* x509, WOLFSSL_CONF* conf, } /* Make a new WOLFSSL_X509 based off of the config file read */ -int wolfCLU_readConfig(WOLFSSL_X509* x509, char* config, char* sect, char* ext) +int wolfCLU_readConfig(WOLFSSL_X509 *x509, char *config, char *sect, char *ext) { int ret = WOLFCLU_SUCCESS; WOLFSSL_CONF *conf = NULL; @@ -1737,20 +1816,22 @@ int wolfCLU_readConfig(WOLFSSL_X509* x509, char* config, char* sect, char* ext) defaultKey = wolfSSL_NCONF_get_string(conf, sect, "default_keyfile"); wolfCLU_setAttributes(x509, conf, - wolfSSL_NCONF_get_string(conf, sect, "attributes")); + wolfSSL_NCONF_get_string(conf, sect, "attributes")); if (ext == NULL) { /* Note: we capture this return code because the !WOLFSSL_CERT_EXT stub * of wolfCLU_setExtensions gracefully returns SUCCESS when the string * is NULL, but fails loudly if an extension section IS requested and * WOLFSSL_CERT_EXT is disabled. These two behaviors are coupled. */ - ret = wolfCLU_setExtensions(x509, conf, - wolfSSL_NCONF_get_string(conf, sect, "x509_extensions"), NULL); + ret = wolfCLU_setExtensions( + x509, conf, wolfSSL_NCONF_get_string(conf, sect, "x509_extensions"), + NULL); } else { /* extension was specifically set, error out if not found */ if (wolfSSL_NCONF_get_section(conf, ext) == NULL) { wolfCLU_LogError("Unable to find certificate extension " - "section %s", ext); + "section %s", + ext); ret = WOLFCLU_FATAL_ERROR; } else { @@ -1759,7 +1840,8 @@ int wolfCLU_readConfig(WOLFSSL_X509* x509, char* config, char* sect, char* ext) } if (ret == WOLFCLU_SUCCESS) { - ret = wolfCLU_setDisNames(x509, conf, + ret = wolfCLU_setDisNames( + x509, conf, wolfSSL_NCONF_get_string(conf, sect, "distinguished_name"), noPrompt); } @@ -1769,7 +1851,7 @@ int wolfCLU_readConfig(WOLFSSL_X509* x509, char* config, char* sect, char* ext) return ret; } -int wolfCLU_GetTypeFromPKEY(WOLFSSL_EVP_PKEY* key) +int wolfCLU_GetTypeFromPKEY(WOLFSSL_EVP_PKEY *key) { int keyType = 0; diff --git a/src/x509/clu_request_setup.c b/src/x509/clu_request_setup.c index 8246a622..5c6981de 100644 --- a/src/x509/clu_request_setup.c +++ b/src/x509/clu_request_setup.c @@ -41,40 +41,6 @@ #if defined(WOLFSSL_CERT_REQ) && !defined(WOLFCLU_NO_FILESYSTEM) -#ifndef _WIN32 - #include /* for the -keyout / -out same file check */ -#endif - -/* Do 'a' and 'b' name the same file? A plain string compare misses the same - * file spelled two ways ("out.pem" and "./out.pem"), so where stat() is - * available the device and inode decide it. - * returns 1 when both name one file, 0 otherwise */ -static int wolfCLU_isSameFile(const char* a, const char* b) -{ - if (a == NULL || b == NULL) { - return 0; - } - - if (XSTRCMP(a, b) == 0) { - return 1; - } - -#ifndef _WIN32 - { - struct stat sa, sb; - - /* only meaningful once both exist; the caller has already created the - * -keyout file by this point */ - if (stat(a, &sa) == 0 && stat(b, &sb) == 0) { - return sa.st_dev == sb.st_dev && sa.st_ino == sb.st_ino; - } - } -#endif - - return 0; -} - - static void wolfCLU_certgenHelp(void) { WOLFCLU_LOG(WOLFCLU_L0, "Arguments:"); WOLFCLU_LOG(WOLFCLU_L0, "\t-in input file to read from"); @@ -82,13 +48,17 @@ static void wolfCLU_certgenHelp(void) { WOLFCLU_LOG(WOLFCLU_L0, "\t-inform der or pem format for '-in'"); WOLFCLU_LOG(WOLFCLU_L0, "\t-outform der or pem format for '-out'"); WOLFCLU_LOG(WOLFCLU_L0, "\t-config file to parse for certificate configuration"); - WOLFCLU_LOG(WOLFCLU_L0, "\t-days number of days should be valid for (default: 20 days)"); + WOLFCLU_LOG(WOLFCLU_L0, "\t-days number of days should be valid for " + "(default: %u days)", WOLFCLU_DEFAULT_VALIDITY); WOLFCLU_LOG(WOLFCLU_L0, "\t-x509 generate self signed certificate"); WOLFCLU_LOG(WOLFCLU_L0, "\t-CA Parent ca of new cert"); WOLFCLU_LOG(WOLFCLU_L0, "\t-CAkey Ca key for signing new cert"); WOLFCLU_LOG(WOLFCLU_L0, "\t-set_serial Input a serial number for the cert to use if not set one will be generated at random"); WOLFCLU_LOG(WOLFCLU_L0, "\t-extensions overwrite the section to get extensions from"); WOLFCLU_LOG(WOLFCLU_L0, "\t-addext add an extension, ie \"subjectAltName=IP:192.168.1.2,DNS:example.com\""); + WOLFCLU_LOG(WOLFCLU_L0, "\t-copy_extensions none|copy|copyall, whether -CA"); + WOLFCLU_LOG(WOLFCLU_L0, "\t carries the request's extensions into the " + "issued certificate (default none)"); WOLFCLU_LOG(WOLFCLU_L0, "\t-nodes no DES encryption on private key output"); WOLFCLU_LOG(WOLFCLU_L0, "\t-newkey generate the private key to use with " "req, as : i.e. rsa:2048 (rsa 2048/3072/4096 only)"); @@ -118,7 +88,7 @@ static const struct option req_options[] = { {"-in", required_argument, 0, WOLFCLU_INFILE }, {"-out", required_argument, 0, WOLFCLU_OUTFILE }, {"-key", required_argument, 0, WOLFCLU_KEY }, - {"-CA", required_argument, 0, WOLFCLU_CA }, + {"-CA", required_argument, 0, WOLFCLU_CAFILE }, {"-CAkey", required_argument, 0, WOLFCLU_CAKEY }, {"-newkey", required_argument, 0, WOLFCLU_NEWKEY }, {"-inkey", required_argument, 0, WOLFCLU_INKEY }, @@ -137,6 +107,7 @@ static const struct option req_options[] = { {"-noout", no_argument, 0, WOLFCLU_NOOUT }, {"-extensions",required_argument, 0, WOLFCLU_EXTENSIONS}, {"-addext", required_argument, 0, WOLFCLU_ADDEXT }, + {"-copy_extensions", required_argument, 0, WOLFCLU_COPY_EXTENSIONS }, {"-nodes", no_argument, 0, WOLFCLU_NODES }, {"-h", no_argument, 0, WOLFCLU_HELP }, {"-help", no_argument, 0, WOLFCLU_HELP }, @@ -295,11 +266,16 @@ static int _wolfSSL_X509_extensions_print(WOLFSSL_BIO* bio, WOLFSSL_X509* x509, WOLFSSL_X509_EXTENSION* ext = wolfSSL_X509_get_ext(x509, i); if (ext != NULL) { WOLFSSL_ASN1_OBJECT* obj; - char buf[MAX_WIDTH]; + char buf[MAX_WIDTH] = {0}; char* altName; int nid; obj = wolfSSL_X509_EXTENSION_get_object(ext); + if (obj == NULL) { + /* obj2txt leaves 'buf' untouched for a NULL object, and + * the name is what every arm below prints */ + continue; + } wolfSSL_OBJ_obj2txt(buf, MAX_WIDTH, obj, 0); XSNPRINTF(scratch, MAX_WIDTH, "%*s", indent + 4, ""); XSTRLCAT(scratch, buf, MAX_WIDTH); @@ -647,14 +623,18 @@ static int verifyX509(WOLFSSL_BIO* keyBio, WOLFSSL_X509* x509, int isCSR) if (pkey == NULL && keyBio != NULL) { /* the key may already have been read once to sign with, rewind so * this read starts at the beginning of the file again */ - wolfSSL_BIO_reset(keyBio); - - pkey = wolfSSL_PEM_read_bio_PrivateKey(keyBio, NULL, NULL, NULL); - if (pkey == NULL) { - wolfCLU_LogError("Unable to read the key to verify with from the " - "file passed to -key"); + if (wolfSSL_BIO_reset(keyBio) != WOLFSSL_SUCCESS) { + wolfCLU_LogError("Unable to rewind keyBio"); ret = WOLFCLU_FATAL_ERROR; } + else { + pkey = wolfSSL_PEM_read_bio_PrivateKey(keyBio, NULL, NULL, NULL); + if (pkey == NULL) { + wolfCLU_LogError("Unable to read the key to verify with " + "from the file passed to -key"); + ret = WOLFCLU_FATAL_ERROR; + } + } } else if (pkey == NULL) { wolfCLU_LogError("Unable to get public key to verify with from " @@ -681,10 +661,6 @@ static int verifyX509(WOLFSSL_BIO* keyBio, WOLFSSL_X509* x509, int isCSR) } } - /* prepare BIO for future use */ - if (keyBio != NULL) { - wolfSSL_BIO_reset(keyBio); - } wolfSSL_EVP_PKEY_free(pkey); return ret; } @@ -748,7 +724,6 @@ static int writeOutPkey(WOLFSSL_BIO* keyOutBio, WOLFSSL_EVP_PKEY* pkey, return ret; } - /* return WOLFCLU_SUCCESS on success */ /* Write the signed request or certificate out to 'outBio'. * * Kept out of makeReq()/selfSignCert()/caSignCert() so the caller can emit @@ -769,9 +744,24 @@ static int writeOutX509(WOLFSSL_BIO* outBio, WOLFSSL_X509* x509, int outForm, : wolfSSL_PEM_write_bio_X509_REQ(outBio, x509); } + else if (outForm == DER_FORM) { + /* NOTE: not wolfSSL_i2d_X509_bio(): that rebuilds the TBS from the + * struct fields and staples the stored signature onto it, which need + * not be the bytes that were signed. wolfSSL_i2d_X509() hands back the + * cached DER, which is what the PEM path writes too. */ + byte* der = NULL; + int derSz = wolfSSL_i2d_X509(x509, &der); + + if (derSz <= 0 || der == NULL) { + wolfCLU_LogError("Error getting the encoded x509 cert"); + return WOLFCLU_FATAL_ERROR; + } + ret = (wolfSSL_BIO_write(outBio, der, derSz) == derSz) ? + WOLFSSL_SUCCESS : WOLFSSL_FAILURE; + XFREE(der, NULL, DYNAMIC_TYPE_OPENSSL); + } else { - ret = (outForm == DER_FORM) ? wolfSSL_i2d_X509_bio(outBio, x509) - : wolfSSL_PEM_write_bio_X509(outBio, x509); + ret = wolfSSL_PEM_write_bio_X509(outBio, x509); } if (ret != WOLFSSL_SUCCESS) { @@ -815,9 +805,79 @@ static int makeReq(WOLFSSL_X509* x509, WOLFSSL_EVP_PKEY* pkey, return ret; } - /* return WOLFCLU_SUCCESS on success */ +/* Build the serial number a new certificate is issued with. + * + * 'set' is the value -set_serial carried, or negative when the option was not + * given, in which case one is drawn at random. + * + * The draw is a fixed WOLFCLU_SERIAL_SIZE bytes rather than sizeof(long): on + * LLP64 and ILP32 targets a long is 32 bits, which would silently halve the + * entropy and collide after ~65k certificates from one CA. That means going + * through a BIGNUM, since wolfSSL_ASN1_INTEGER_set() only takes a long. + * + * returns the new serial on success, NULL on failure */ +static WOLFSSL_ASN1_INTEGER* makeSerial(long set) +{ + WOLFSSL_ASN1_INTEGER* serial = NULL; + + if (set >= 0) { + serial = wolfSSL_ASN1_INTEGER_new(); + if (serial != NULL && wolfSSL_ASN1_INTEGER_set(serial, set) + != WOLFSSL_SUCCESS) { + wolfSSL_ASN1_INTEGER_free(serial); + serial = NULL; + } + if (serial == NULL) { + wolfCLU_LogError("Unable to create the serial number"); + } + } + else { + WC_RNG rng; + byte randBytes[WOLFCLU_SERIAL_SIZE]; + + /* wolfCrypt returns 0 on success, not WOLFSSL_SUCCESS */ + if (wc_InitRng(&rng) != 0) { + wolfCLU_LogError("Unable to initialize RNG for serial number"); + return NULL; + } + + if (wc_RNG_GenerateBlock(&rng, randBytes, (word32)sizeof(randBytes)) + != 0) { + wolfCLU_LogError("Unable to generate serial number"); + } + else { + WOLFSSL_BIGNUM* bn; + + /* A serial has to be a positive integer (RFC 5280 4.1.2.2), so + * clear the sign bit. Setting the next one down keeps the drawn + * width constant, so every serial is WOLFCLU_SERIAL_SIZE bytes + * and can never come out zero. */ + randBytes[0] &= 0x7F; + randBytes[0] |= 0x40; + + bn = wolfSSL_BN_bin2bn(randBytes, (int)sizeof(randBytes), NULL); + if (bn != NULL) { + serial = wolfSSL_BN_to_ASN1_INTEGER(bn, NULL); + wolfSSL_BN_free(bn); + } + if (serial == NULL) { + wolfCLU_LogError("Unable to create the serial number"); + } + } + + wc_FreeRng(&rng); + wolfCLU_ForceZero(randBytes, (unsigned int)sizeof(randBytes)); + } + + return serial; +} + +/* Turn 'x509' into a self-signed certificate: issuer name, validity window, + * serial number and the default extensions, signed with its own key. + * + * return WOLFCLU_SUCCESS on success */ static int selfSignCert(WOLFSSL_X509* x509, WOLFSSL_EVP_PKEY* pkey, - const WOLFSSL_EVP_MD* md, long days, long serial) + const WOLFSSL_EVP_MD* md, long days, WOLFSSL_ASN1_INTEGER* serial) { int ret = WOLFCLU_SUCCESS; @@ -872,24 +932,13 @@ static int selfSignCert(WOLFSSL_X509* x509, WOLFSSL_EVP_PKEY* pkey, } /* Set the serial number. */ - if (ret == WOLFCLU_SUCCESS && serial > 0) { - WOLFSSL_ASN1_INTEGER* asn1SerialNum = wolfSSL_ASN1_INTEGER_new(); - if (asn1SerialNum != NULL) { - /* wolfSSL statuses stay out of 'ret', which carries the WOLFCLU - * status; the two only happen to agree on success */ - if (wolfSSL_ASN1_INTEGER_set(asn1SerialNum, serial) - != WOLFSSL_SUCCESS || - wolfSSL_X509_set_serialNumber(x509, asn1SerialNum) - != WOLFSSL_SUCCESS) { - wolfCLU_LogError("Unable to set serial number"); - ret = WOLFCLU_FATAL_ERROR; - } - } - else { + if (ret == WOLFCLU_SUCCESS && serial != NULL) { + /* the wolfSSL status stays out of 'ret', which carries the WOLFCLU + * status; the two only happen to agree on success */ + if (wolfSSL_X509_set_serialNumber(x509, serial) != WOLFSSL_SUCCESS) { wolfCLU_LogError("Unable to set serial number"); ret = WOLFCLU_FATAL_ERROR; } - wolfSSL_ASN1_INTEGER_free(asn1SerialNum); } #if defined(WOLFSSL_CERT_EXT) && !defined(NO_SHA) @@ -919,7 +968,7 @@ static int selfSignCert(WOLFSSL_X509* x509, WOLFSSL_EVP_PKEY* pkey, #else if (ret == WOLFCLU_SUCCESS) { - WOLFCLU_LOG(WOLFCLU_L0, "Skipping basicConstaints " + WOLFCLU_LOG(WOLFCLU_L0, "Skipping basicConstraints, " "WOLFSSL_CERT_EXT or SHA-1 disabled"); } #endif @@ -940,47 +989,81 @@ static int selfSignCert(WOLFSSL_X509* x509, WOLFSSL_EVP_PKEY* pkey, return ret; } - /* return WOLFCLU_SUCCESS on success */ -static int caSignCert(WOLFSSL_X509* x509, WOLFSSL_BIO* caBio, +/* Issue a certificate for the request '*x509' under the CA in 'caBio'. + * + * On success '*x509' is replaced by the issued certificate and the request is + * freed, so the caller must not hold another reference to it. + * + * A request states what the requester WANTS; only the issuer decides what the + * certificate SAYS. So by default the leaf is built from scratch and takes + * just the subject name and the public key out of the request -- every + * extension on it is the CA's own. Otherwise a requester could mint itself a + * sub-CA, a keyCertSign key or a certificate for a name it does not own just + * by putting it in the CSR. 'copyExt' set carries the request's extensions + * over instead, the opt-in OpenSSL spells -copy_extensions. + * + * return WOLFCLU_SUCCESS on success */ +static int caSignCert(WOLFSSL_X509** x509, WOLFSSL_BIO* caBio, WOLFSSL_BIO* caKeyBio, const WOLFSSL_EVP_MD* md, long days, - long serial, int doVerify) + WOLFSSL_ASN1_INTEGER* serial, int doVerify, int copyExt) { int ret = WOLFCLU_SUCCESS; WOLFSSL_EVP_PKEY* caKey = NULL; WOLFSSL_X509* caCert = NULL; + WOLFSSL_X509* req = NULL; + WOLFSSL_X509* leaf = NULL; /* Load the CA material */ - if (caBio == NULL || caKeyBio == NULL) + if (x509 == NULL || *x509 == NULL || caBio == NULL || caKeyBio == NULL) return WOLFCLU_FATAL_ERROR; - caCert = wolfSSL_PEM_read_bio_X509(caBio, NULL, NULL, NULL); - if (caCert == NULL) { - wolfCLU_LogError("Unable to read ca cert passed to -CA"); - ret = WOLFCLU_FATAL_ERROR; - } + req = *x509; + + caCert = wolfSSL_PEM_read_bio_X509(caBio, NULL, NULL, NULL); + if (caCert == NULL) { + /* the PEM attempt above read the BIO to EOF, and the DER read sizes + * the input from the current offset, so rewind before retrying. A + * failed rewind is reported on its own: the DER read would otherwise + * start at the wrong offset and blame the file's contents. */ + if (wolfSSL_BIO_reset(caBio) != WOLFSSL_SUCCESS) { + wolfCLU_LogError("Unable to rewind the file passed to -CA"); + ret = WOLFCLU_FATAL_ERROR; + } + else { + caCert = wolfSSL_d2i_X509_bio(caBio, NULL); + if (caCert == NULL) { + wolfCLU_LogError("Unable to read ca cert passed to -CA " + "tried to parse as PEM and DER"); + ret = WOLFCLU_FATAL_ERROR; + } + } + } - if (ret == WOLFCLU_SUCCESS) { - caKey = wolfSSL_PEM_read_bio_PrivateKey(caKeyBio, NULL, NULL, NULL); + if (ret == WOLFCLU_SUCCESS) { + caKey = wolfSSL_PEM_read_bio_PrivateKey(caKeyBio, NULL, NULL, NULL); if (caKey == NULL) { - wolfCLU_LogError("Unable to read ca key passed to -CAkey"); - ret = WOLFCLU_FATAL_ERROR; + wolfCLU_LogError("Unable to read ca key passed to -CAkey"); + ret = WOLFCLU_FATAL_ERROR; } } /* Confirm the CA cert can issue. wolfSSL_X509_check_ca() answers 1 for * CA:TRUE but also 4 for a leaf that merely carries a critical * extendedKeyUsage, so only the CA bit may be accepted here. */ - if (ret == WOLFCLU_SUCCESS) { + if (ret == WOLFCLU_SUCCESS) { if (wolfSSL_X509_check_ca(caCert) != 1) { wolfCLU_LogError("The certificate passed to -CA is not a CA " "(basicConstraints CA:TRUE) and cannot issue"); ret = WOLFCLU_FATAL_ERROR; } - } + } /* A published keyUsage has to include keyCertSign (RFC 5280 4.2.1.3). * wolfSSL_X509_get_key_usage() returns all bits set when the extension is - * absent, so an unrestricted CA needs no special case. */ + * absent, so an unrestricted CA needs no special case. Guarded on the + * wolfSSL version the same way the extension printer above is: the getter + * does not exist in older releases. */ +#if LIBWOLFSSL_VERSION_HEX > 0x05001000 if (ret == WOLFCLU_SUCCESS) { if ((wolfSSL_X509_get_key_usage(caCert) & KEYUSE_KEY_CERT_SIGN) == 0) { wolfCLU_LogError("The certificate passed to -CA has a keyUsage " @@ -988,6 +1071,7 @@ static int caSignCert(WOLFSSL_X509* x509, WOLFSSL_BIO* caBio, ret = WOLFCLU_FATAL_ERROR; } } +#endif /* -CAkey has to be the key -CA was issued under, otherwise the * certificate would carry the CA's issuer name over a signature that @@ -1001,26 +1085,49 @@ static int caSignCert(WOLFSSL_X509* x509, WOLFSSL_BIO* caBio, } } - /* Verify the incoming request before certifying it */ - if (ret == WOLFCLU_SUCCESS) { - WOLFSSL_EVP_PKEY* reqPub = wolfSSL_X509_get_pubkey(x509); + /* Verify the incoming request before certifying it, and build the leaf + * that will be signed. + * + * Without -copy_extensions the leaf is a fresh certificate carrying only + * the two things the request gets to decide -- its subject name and its + * public key. Everything else the request object holds (subjectAltName, + * keyUsage, extendedKeyUsage, certificate policies, nsCertType, custom + * extensions) is left behind, because wolfSSL_X509_sign() re-encodes all + * of it straight out of the object it is handed. With -copy_extensions + * the request itself is signed, so those extensions do carry over. */ + if (ret == WOLFCLU_SUCCESS) { + WOLFSSL_EVP_PKEY* reqPub = wolfSSL_X509_get_pubkey(req); if (reqPub == NULL) { wolfCLU_LogError("Req did not have a public key to verify it with"); ret = WOLFCLU_FATAL_ERROR; } else { - if (wolfSSL_X509_REQ_verify(x509, reqPub) < 1) { + if (wolfSSL_X509_REQ_verify(req, reqPub) < 1) { wolfCLU_LogError("Req Failed verification"); ret = WOLFCLU_FATAL_ERROR; } + else if (copyExt) { + leaf = req; + } + else if ((leaf = wolfSSL_X509_new()) == NULL) { + wolfCLU_LogError("Unable to create the certificate to issue"); + ret = WOLFCLU_FATAL_ERROR; + } + else if (wolfSSL_X509_set_subject_name(leaf, + wolfSSL_X509_get_subject_name(req)) + != WOLFSSL_SUCCESS || + wolfSSL_X509_set_pubkey(leaf, reqPub) != WOLFSSL_SUCCESS) { + wolfCLU_LogError("Unable to copy the subject name and public " + "key out of the request"); + ret = WOLFCLU_FATAL_ERROR; + } } wolfSSL_EVP_PKEY_free(reqPub); - } - + } /* Bump to v3 */ if (ret == WOLFCLU_SUCCESS) { - if (wolfSSL_X509_set_version(x509, WOLFSSL_X509_V3) != + if (wolfSSL_X509_set_version(leaf, WOLFSSL_X509_V3) != WOLFSSL_SUCCESS) { wolfCLU_LogError("Error setting CSR version"); ret = WOLFCLU_FATAL_ERROR; @@ -1029,7 +1136,7 @@ static int caSignCert(WOLFSSL_X509* x509, WOLFSSL_BIO* caBio, /* Issuer == the CA's subject */ if (ret == WOLFCLU_SUCCESS) { - if (wolfSSL_X509_set_issuer_name(x509, + if (wolfSSL_X509_set_issuer_name(leaf, wolfSSL_X509_get_subject_name(caCert)) != WOLFSSL_SUCCESS) { wolfCLU_LogError("Error setting issuer name"); ret = WOLFCLU_FATAL_ERROR; @@ -1053,8 +1160,8 @@ static int caSignCert(WOLFSSL_X509* x509, WOLFSSL_BIO* caBio, ret = WOLFCLU_FATAL_ERROR; } else { - wolfSSL_X509_set_notBefore(x509, notBefore); - wolfSSL_X509_set_notAfter(x509, notAfter); + wolfSSL_X509_set_notBefore(leaf, notBefore); + wolfSSL_X509_set_notAfter(leaf, notAfter); } wolfSSL_ASN1_TIME_free(notBefore); @@ -1063,24 +1170,13 @@ static int caSignCert(WOLFSSL_X509* x509, WOLFSSL_BIO* caBio, } /* Assign the CA-chosen serial */ - if (ret == WOLFCLU_SUCCESS && serial > 0) { - WOLFSSL_ASN1_INTEGER* asn1SerialNum = wolfSSL_ASN1_INTEGER_new(); - if (asn1SerialNum != NULL) { - /* wolfSSL statuses are kept out of 'ret', which carries the - * WOLFCLU status; the two only happen to agree on success */ - if (wolfSSL_ASN1_INTEGER_set(asn1SerialNum, serial) - != WOLFSSL_SUCCESS || - wolfSSL_X509_set_serialNumber(x509, asn1SerialNum) - != WOLFSSL_SUCCESS) { - wolfCLU_LogError("Unable to set serial number"); - ret = WOLFCLU_FATAL_ERROR; - } - } - else { + if (ret == WOLFCLU_SUCCESS && serial != NULL) { + /* the wolfSSL status is kept out of 'ret', which carries the WOLFCLU + * status; the two only happen to agree on success */ + if (wolfSSL_X509_set_serialNumber(leaf, serial) != WOLFSSL_SUCCESS) { wolfCLU_LogError("Unable to set serial number"); ret = WOLFCLU_FATAL_ERROR; } - wolfSSL_ASN1_INTEGER_free(asn1SerialNum); } @@ -1089,12 +1185,20 @@ static int caSignCert(WOLFSSL_X509* x509, WOLFSSL_BIO* caBio, * - Subject Key Id derived from this cert's own public key * - Authority Key Id copied from the CA cert's Subject Key Id */ if (ret == WOLFCLU_SUCCESS && - wolfSSL_X509_ext_isSet_by_NID(x509, NID_basic_constraints) && - wolfSSL_X509_check_ca(x509) == 1) { + wolfSSL_X509_ext_isSet_by_NID(req, NID_basic_constraints) && + wolfSSL_X509_check_ca(req) == 1) { WOLFCLU_LOG(WOLFCLU_L0, "Warning: request asked for Basic Constraints " "CA:TRUE; issuing a leaf with CA:FALSE"); } + /* Say so rather than handing back a certificate quietly missing the + * subjectAltName the operator believed the request would supply. */ + if (ret == WOLFCLU_SUCCESS && !copyExt && + wolfSSL_X509_get_ext_count(req) > 0) { + WOLFCLU_LOG(WOLFCLU_L0, "Ignoring the extensions in the request; pass " + "-copy_extensions copy to carry them over"); + } + #if defined(WOLFSSL_CERT_EXT) && !defined(NO_SHA) if (ret == WOLFCLU_SUCCESS) { @@ -1109,7 +1213,7 @@ static int caSignCert(WOLFSSL_X509* x509, WOLFSSL_BIO* caBio, } else { obj->ca = 0; /* CA:FALSE -- this is a leaf, not a CA */ - if (wolfSSL_X509_add_ext(x509, ext, -1) != WOLFSSL_SUCCESS) { + if (wolfSSL_X509_add_ext(leaf, ext, -1) != WOLFSSL_SUCCESS) { WOLFCLU_LOG(WOLFCLU_E0, "error adding Basic Constraints extension"); ret = WOLFCLU_FATAL_ERROR; @@ -1119,17 +1223,35 @@ static int caSignCert(WOLFSSL_X509* x509, WOLFSSL_BIO* caBio, } - /* Subject Key Id from this cert's own public key */ + /* Subject Key Id from this cert's own public key. + * + * Derived on the request and copied over, rather than derived on the leaf. + * wolfSSL hashes whatever WOLFSSL_X509.pubKey holds, and that is the bare + * public key bits on a parsed request but a whole SubjectPublicKeyInfo on + * a key installed with wolfSSL_X509_set_pubkey(). Only the former is the + * value RFC 5280 4.2.1.2 describes and every other tool computes, and the + * two certify the same key, so the request's is the one to use. */ if (ret == WOLFCLU_SUCCESS) { - if (wolfSSL_X509_set_subject_key_id_ex(x509) != WOLFSSL_SUCCESS) { + if (wolfSSL_X509_set_subject_key_id_ex(req) != WOLFSSL_SUCCESS) { wolfCLU_LogError("Error setting Subject Key Identifier"); ret = WOLFCLU_FATAL_ERROR; } + else if (leaf != req) { + byte skid[WC_SHA_DIGEST_SIZE]; + int skidSz = (int)sizeof(skid); + + if (wolfSSL_X509_get_subjectKeyID(req, skid, &skidSz) == NULL || + wolfSSL_X509_set_subject_key_id(leaf, skid, skidSz) + != WOLFSSL_SUCCESS) { + wolfCLU_LogError("Error setting Subject Key Identifier"); + ret = WOLFCLU_FATAL_ERROR; + } + } } /* Authority Key Id = the CA cert's Subject Key Id (links the chain) */ if (ret == WOLFCLU_SUCCESS) { - if (wolfSSL_X509_set_authority_key_id_ex(x509, caCert) != + if (wolfSSL_X509_set_authority_key_id_ex(leaf, caCert) != WOLFSSL_SUCCESS) { wolfCLU_LogError("Error setting Authority Key Identifier"); ret = WOLFCLU_FATAL_ERROR; @@ -1137,7 +1259,7 @@ static int caSignCert(WOLFSSL_X509* x509, WOLFSSL_BIO* caBio, } #else if (ret == WOLFCLU_SUCCESS) { - WOLFCLU_LOG(WOLFCLU_L0, "Skipping basicConstaints AKI and SKI " + WOLFCLU_LOG(WOLFCLU_L0, "Skipping basicConstraints, AKI and SKI: " "WOLFSSL_CERT_EXT or SHA-1 disabled"); } #endif @@ -1145,7 +1267,7 @@ static int caSignCert(WOLFSSL_X509* x509, WOLFSSL_BIO* caBio, /* Sign with the CA key. wolfSSL_X509_sign() hands back the cert length * on success rather than WOLFSSL_SUCCESS, so it is kept in a local. */ if (ret == WOLFCLU_SUCCESS) { - int signSz = wolfSSL_X509_sign(x509, caKey, md); + int signSz = wolfSSL_X509_sign(leaf, caKey, md); if (signSz <= 0) { wolfCLU_LogError("Error signing certificate"); @@ -1165,7 +1287,7 @@ static int caSignCert(WOLFSSL_X509* x509, WOLFSSL_BIO* caBio, ret = WOLFCLU_FATAL_ERROR; } else { - if (wolfSSL_X509_verify(x509, pubKey) != 1) { + if (wolfSSL_X509_verify(leaf, pubKey) != 1) { wolfCLU_LogError("New x509 ca signed cert could not be " "verified"); ret = WOLFCLU_FATAL_ERROR; @@ -1178,6 +1300,18 @@ static int caSignCert(WOLFSSL_X509* x509, WOLFSSL_BIO* caBio, } /* the caller writes the encoded form out, after -text / -verify */ + /* Hand the issued certificate back in place of the request. On failure + * the caller still owns the request, so only the half-built leaf goes. */ + if (leaf != req) { + if (ret == WOLFCLU_SUCCESS) { + wolfSSL_X509_free(req); + *x509 = leaf; + } + else { + wolfSSL_X509_free(leaf); + } + } + wolfSSL_X509_free(caCert); wolfSSL_EVP_PKEY_free(caKey); @@ -1254,13 +1388,13 @@ int wolfCLU_requestSetup(int argc, char** argv) { #ifndef WOLFSSL_CERT_REQ wolfCLU_LogError("wolfSSL not compiled with --enable-certreq"); - /* silence unused variable warnings */ + /* silence unused variable warnings */ (void) argc; (void) argv; return NOT_COMPILED_IN; #elif defined(WOLFCLU_NO_FILESYSTEM) WOLFCLU_LOG(WOLFCLU_E0, "No Filesystem Support."); - /* silence unused variable warnings */ + /* silence unused variable warnings */ (void) argc; (void) argv; return NOT_COMPILED_IN; @@ -1277,6 +1411,7 @@ int wolfCLU_requestSetup(int argc, char** argv) const WOLFSSL_EVP_MD *md = wolfSSL_EVP_sha256(); long serialNumber = -1; + WOLFSSL_ASN1_INTEGER* serial = NULL; int ret = WOLFCLU_SUCCESS; char* subj = NULL; @@ -1295,9 +1430,9 @@ int wolfCLU_requestSetup(int argc, char** argv) int mdSet = 0; char password[MAX_PASSWORD_SIZE] = {0}; - /* the length wolfCLU_GetPassword() parsed, not the buffer capacity; - * writeOutPkey() is handed sizeof(password) for that */ - int passwordLen = MAX_PASSWORD_SIZE; + /* capacity in, parsed length out; the out value goes unread because + * writeOutPkey() is handed sizeof(password) and recomputes the length */ + int passwordLen = (int)sizeof(password); int passoutSet = 0; byte doVerify = 0; @@ -1309,6 +1444,9 @@ int wolfCLU_requestSetup(int argc, char** argv) /* cleared once the run has produced a certificate rather than a request, * so -text prints the right object regardless of which printer is used */ byte isCSR = 1; + /* -copy_extensions: off, so a requester cannot pick its own extensions */ + int copyExt = 0; + byte copyExtSet = 0; /* Multiple -addext is not yet supported. Detect it up front and fail * instead of silently dropping the extension and exiting success. */ @@ -1340,6 +1478,31 @@ int wolfCLU_requestSetup(int argc, char** argv) addExt = optarg; break; + /* OpenSSL's spelling: "copy" skips extensions the issuer already + * set and "copyall" does not, but wolfCLU sets Basic Constraints, + * SKID and AKID after the copy either way, so the two land in the + * same place and are accepted as one. */ + case WOLFCLU_COPY_EXTENSIONS: + if (optarg == NULL) { + wolfCLU_LogError("-copy_extensions has no arg"); + ret = WOLFCLU_FATAL_ERROR; + } + else if (XSTRCMP(optarg, "none") == 0) { + copyExt = 0; + copyExtSet = 1; + } + else if (XSTRCMP(optarg, "copy") == 0 || + XSTRCMP(optarg, "copyall") == 0) { + copyExt = 1; + copyExtSet = 1; + } + else { + wolfCLU_LogError("-copy_extensions expects none, copy or " + "copyall, got %s", optarg); + ret = WOLFCLU_FATAL_ERROR; + } + break; + case WOLFCLU_NODES: useDes = 0; break; @@ -1369,7 +1532,7 @@ int wolfCLU_requestSetup(int argc, char** argv) * is not accepted as rsa */ keyType = ((split - optarg) == 3 && XSTRNCMP("rsa", optarg, 3) == 0) ? - EVP_PKEY_RSA : 0; + WC_EVP_PKEY_RSA : 0; if (keyType == 0) { wolfCLU_LogError("-newkey only supports rsa generation " "saw request for %.*s, " @@ -1431,7 +1594,7 @@ int wolfCLU_requestSetup(int argc, char** argv) outKeyFile = optarg; break; - case WOLFCLU_CA: + case WOLFCLU_CAFILE: caFile = optarg; break; @@ -1544,6 +1707,11 @@ int wolfCLU_requestSetup(int argc, char** argv) break; case WOLFCLU_PASSWORD_OUT: + if (optarg == NULL) { + ret = WOLFCLU_FATAL_ERROR; + wolfCLU_LogError("-passout requires and argument"); + break; + } ret = wolfCLU_GetPassword(password, &passwordLen, optarg); passoutSet = 1; break; @@ -1570,6 +1738,7 @@ int wolfCLU_requestSetup(int argc, char** argv) break; case ARG_FOUND_TWICE: + wolfCLU_LogError("Found duplicate argument"); ret = WOLFCLU_FATAL_ERROR; break; @@ -1641,8 +1810,17 @@ int wolfCLU_requestSetup(int argc, char** argv) ret = WOLFCLU_FATAL_ERROR; } - /* -CA passes the request through untouched, so reject anything that would - * alter it. Done here so -newkey cannot truncate -keyout before failing. */ + /* and the mirror image: -CA names the issuing certificate, -CAkey the key + * it was issued under, so neither option is usable on its own */ + if (ret == WOLFCLU_SUCCESS && caFile != NULL && caKeyFile == NULL) { + wolfCLU_LogError("-CAkey was not set but -CA was passed; -CAkey names " + "the key for the certificate given to -CA"); + ret = WOLFCLU_FATAL_ERROR; + } + + /* -CA certifies the request as it stands rather than editing it, so + * reject anything that would alter it. Done here so -newkey cannot + * truncate -keyout before failing. */ if (ret == WOLFCLU_SUCCESS && caFile != NULL && (subj != NULL || configFile != NULL || addExt != NULL || keyFile != NULL || keyType != 0)) { @@ -1653,6 +1831,12 @@ int wolfCLU_requestSetup(int argc, char** argv) ret = WOLFCLU_FATAL_ERROR; } + /* -copy_extensions only steers what -CA carries out of the request; the + * CSR and -x509 paths already build from the options given here. */ + if (ret == WOLFCLU_SUCCESS && copyExtSet && caFile == NULL) { + WOLFCLU_LOG(WOLFCLU_L0, "Ignoring -copy_extensions, it applies to -CA"); + } + /* A PKCS#10 request has neither field, so say so rather than dropping * the value silently, matching the cross-option checks above. */ if (ret == WOLFCLU_SUCCESS && caFile == NULL && !genX509 && @@ -1663,51 +1847,13 @@ int wolfCLU_requestSetup(int argc, char** argv) (serialNumber >= 0 ? "-set_serial" : "-days")); } - if (ret == WOLFCLU_SUCCESS && serialNumber < 0 && (caFile != NULL - || genX509)) { - WC_RNG rng = {0}; - if (wc_InitRng(&rng) != 0) { - wolfCLU_LogError("Unable to initialize RNG for serial number"); + /* Only the certificate paths carry a serial; makeSerial() draws a random + * one when -set_serial was not given. */ + if (ret == WOLFCLU_SUCCESS && (caFile != NULL || genX509)) { + serial = makeSerial(serialNumber); + if (serial == NULL) { ret = WOLFCLU_FATAL_ERROR; } - else { - /* Fill a whole long with random bytes, accumulated separately - * because serialNumber still holds the all-ones -1 sentinel that - * OR-ing into would be a no-op. 'unsigned long' is the unsigned - * twin of the long the setters take, so the byte count, mask and - * final cast are one width on every target -- word64 matches long - * only on LP64 and does not exist without a 64-bit type. */ - word32 index = 0; - unsigned long serial = 0; - byte randBytes[sizeof(unsigned long)]; - - /* wolfCrypt returns 0 on success, not WOLFSSL_SUCCESS */ - if (wc_RNG_GenerateBlock(&rng, randBytes, (word32)sizeof(randBytes)) - != 0) { - wolfCLU_LogError("Unable to generate serial number"); - ret = WOLFCLU_FATAL_ERROR; - } - else { - for (; index < (word32)sizeof(randBytes); index++) { - serial = (serial << 8) | randBytes[index]; - } - } - wc_FreeRng(&rng); - - if (ret == WOLFCLU_SUCCESS) { - /* Clear the sign bit: a serial has to be a positive integer - * (RFC 5280 4.1.2.2), and the setters below take a signed - * long. Zero is then steered away from because both signing - * helpers gate on "serial > 0" and would otherwise skip - * wolfSSL_X509_set_serialNumber entirely, silently emitting - * wolfSSL's default serial instead of the one drawn here. */ - serial &= ~0UL >> 1; - if (serial == 0) { - serial = 1; - } - serialNumber = (long)serial; - } - } } if (ret == WOLFCLU_SUCCESS) { @@ -1843,7 +1989,19 @@ int wolfCLU_requestSetup(int argc, char** argv) } if (ret == WOLFCLU_SUCCESS && pkey != NULL) { - if (wolfSSL_X509_set_pubkey(x509, pkey) + /* Installing an unrelated -key over an -in request would + * silently write out a different request than the one the + * user pointed at, and would leave -verify checking a + * signature wolfCLU just created. */ + if (reqBio != NULL && keyBio != NULL && + wolfSSL_X509_check_private_key(x509, pkey) + != WOLFSSL_SUCCESS) { + wolfCLU_LogError("The key passed to -key does not " + "match the public key of the request passed " + "to -in"); + ret = WOLFCLU_FATAL_ERROR; + } + else if (wolfSSL_X509_set_pubkey(x509, pkey) != WOLFSSL_SUCCESS) { ret = WOLFCLU_FATAL_ERROR; } @@ -1935,24 +2093,20 @@ int wolfCLU_requestSetup(int argc, char** argv) if (ret == WOLFCLU_SUCCESS) { if (caBio != NULL) { - /* -CA: issue a CA-signed cert. caSignCert reads */ - if (caKeyFile == NULL) { - wolfCLU_LogError("-CAkey was not set but -ca " - "was passed"); - ret = WOLFCLU_FATAL_ERROR; - } - if (ret == WOLFCLU_SUCCESS) { - ret = caSignCert(x509, caBio, caKeyBio, md, - days == 0 ? WOLFCLU_DEFAULT_VALIDITY : days, - serialNumber, doVerify); - isCSR = 0; - } + /* -CA: issue a CA-signed cert. caSignCert reads the CA + * material off the BIOs and replaces 'x509' with the + * certificate it issues. The -CAkey pairing was checked + * with the rest of the option validation above. */ + ret = caSignCert(&x509, caBio, caKeyBio, md, + days == 0 ? WOLFCLU_DEFAULT_VALIDITY : days, + serial, doVerify, copyExt); + isCSR = 0; } else if (genX509) { /* -x509: self-signed cert, own key is issuer + signer */ ret = selfSignCert(x509, pkey, md, days == 0 ? WOLFCLU_DEFAULT_VALIDITY : days, - serialNumber); + serial); isCSR = 0; } else if (reqBio == NULL || reSign) { @@ -1980,15 +2134,15 @@ int wolfCLU_requestSetup(int argc, char** argv) ret = verifyX509(keyBio, x509, isCSR); } - /* Nothing is opened when there is nothing to write: "wb" truncates, - * so "-noout -out f" used to leave f an empty file. */ + /* Nothing is opened when there is nothing to write: "wb" truncates, + * so "-noout -out f" used to leave f an empty file. */ if (ret == WOLFCLU_SUCCESS && (!noOut || doTextOut)) { if (outFile != NULL) { /* "-keyout f -out f" appends both objects to one file, as * OpenSSL's req does. Reopening with "wb" would truncate the * key and leave both BIOs flushing from offset 0. */ if (outKeyBio != NULL && - wolfCLU_isSameFile(outKeyFile, outFile)) { + XSTRCMP(outKeyFile, outFile) == 0) { outBio = outKeyBio; sharedOutBio = 1; } @@ -2049,6 +2203,7 @@ int wolfCLU_requestSetup(int argc, char** argv) wolfSSL_X509_free(x509); } + wolfSSL_ASN1_INTEGER_free(serial); wolfCLU_ForceZero(password, sizeof(password)); return ret; diff --git a/tests/x509/x509-ca-test.py b/tests/x509/x509-ca-test.py index 2613d371..dc2d437b 100644 --- a/tests/x509/x509-ca-test.py +++ b/tests/x509/x509-ca-test.py @@ -397,6 +397,7 @@ def test_days_bounds(self): The bound is WOLFCLU_MAX_VALIDITY, the largest day count that still fits an int once converted to seconds; ca used to accept up to INT_MAX and only fail later, from inside the time formatter.""" + self._clean(_tmp("test_ca_days_bad.pem")) for bad in ("0", "-1", "24856", "2147483647", "abc", "10.5"): with self.subTest(days=bad): r = run_wolfssl("ca", "-config", self.conf, diff --git a/tests/x509/x509-req-test.py b/tests/x509/x509-req-test.py index 8dfd52c5..a3802ad7 100644 --- a/tests/x509/x509-req-test.py +++ b/tests/x509/x509-req-test.py @@ -899,9 +899,28 @@ def setUpClass(cls): "-out", cls.ca_false_csr) assert r.returncode == 0, "setup CA:FALSE CSR failed: " + r.stderr + # Requests carrying extensions the issuer never agreed to. Only one + # -addext is supported per run, so each gets its own request. + cls.san_csr = _tmp("test_reqca_san.csr") + r = run_wolfssl("req", "-new", + "-key", os.path.join(CERTS_DIR, "server-key.pem"), + "-subj", cls.RSA_SUBJ, + "-addext", "subjectAltName=DNS:notmine.example.com", + "-out", cls.san_csr) + assert r.returncode == 0, "setup SAN CSR failed: " + r.stderr + + cls.ku_csr = _tmp("test_reqca_ku.csr") + r = run_wolfssl("req", "-new", + "-key", os.path.join(CERTS_DIR, "server-key.pem"), + "-subj", cls.RSA_SUBJ, + "-addext", "keyUsage=digitalSignature,keyEncipherment", + "-out", cls.ku_csr) + assert r.returncode == 0, "setup keyUsage CSR failed: " + r.stderr + @classmethod def tearDownClass(cls): - _cleanup(cls.rsa_csr, cls.ecc_csr, cls.ca_true_csr, cls.ca_false_csr) + _cleanup(cls.rsa_csr, cls.ecc_csr, cls.ca_true_csr, cls.ca_false_csr, + cls.san_csr, cls.ku_csr) def _clean(self, *files): for f in files: @@ -1079,9 +1098,10 @@ def test_ca_sign_default_serial_is_wide_and_unique(self): It used to be drawn from sizeof(int)-1 bytes, i.e. 24 bits, which collides with about 50% probability after ~4100 certificates from the - same CA. A zero draw was worse: both signing helpers gate on - "serial > 0", so it skipped set_serialNumber entirely and the cert - carried wolfSSL's default instead.""" + same CA, and then from sizeof(long), which is only 8 bytes on LP64 -- + half that on the Windows and 32-bit builds. A zero draw was worse: both + signing helpers gated on "serial > 0", so it skipped set_serialNumber + entirely and the cert carried wolfSSL's default instead.""" seen = set() for i in range(4): r, out = self._ca_sign("reqca_defserial{}.pem".format(i)) @@ -1093,9 +1113,11 @@ def test_ca_sign_default_serial_is_wide_and_unique(self): value = int(hexval, 16) self.assertGreater(value, 0, "serial must be positive and non zero") - # 24 bits of entropy fits in 6 hex digits; require more than that - self.assertGreater(len(hexval), 8, - "serial {!r} is too narrow".format(hexval)) + # WOLFCLU_SERIAL_SIZE is 8 bytes, and the draw forces bit 0x40 of + # the top one, so the width is the same on every target rather + # than following sizeof(long). + self.assertEqual(len(hexval), 16, + "serial {!r} is not 8 bytes wide".format(hexval)) # No high-bit check on the first printed octet: -serial prints the # magnitude, like OpenSSL does, not the DER content octets. A # positive INTEGER whose top magnitude byte has the high bit set is @@ -1122,6 +1144,34 @@ def test_ca_sign_outform_der(self): self.assertEqual(subj.returncode, 0, subj.stderr) self.assertIn("leaf.example.com", subj.stdout) + def test_ca_sign_outform_der_verifies(self): + """The DER output must carry the signature that was made over it. + + Reading the subject back only proves the file parses; a DER encoding + rebuilt from the struct rather than written out as signed would still + parse but no longer verify.""" + r, out = self._ca_sign("reqca_verify.der", "-outform", "DER") + self.assertEqual(r.returncode, 0, r.stderr) + + pem = _tmp("reqca_verify_fromder.pem") + self._clean(pem) + c = run_wolfssl("x509", "-inform", "DER", "-in", out, + "-outform", "PEM", "-out", pem) + self.assertEqual(c.returncode, 0, c.stderr) + + v = run_wolfssl("verify", "-CAfile", + os.path.join(CERTS_DIR, "ca-cert.pem"), pem) + self.assertEqual(v.returncode, 0, v.stderr) + + def test_ca_sign_der_ca_cert(self): + """-CA accepts a DER encoded CA certificate.""" + r, out = self._ca_sign("reqca_derca.pem", ca="ca-cert.der") + self.assertEqual(r.returncode, 0, r.stderr) + + v = run_wolfssl("verify", "-CAfile", + os.path.join(CERTS_DIR, "ca-cert.pem"), out) + self.assertEqual(v.returncode, 0, v.stderr) + def test_ca_without_cakey_fails(self): """-CA without -CAkey has no signing key and must fail.""" out = _tmp("reqca_nokey.pem") @@ -1364,6 +1414,174 @@ def test_ca_sign_subject_key_id_is_leafs_own(self): "leaf SKID should come from its own key, not the " "CA's") + def test_ca_sign_drops_request_subject_alt_name(self): + """A subjectAltName the requester asked for is not issued. + + The name in a SAN is the one a TLS client actually matches, so a + requester that could plant one would get a certificate for a host it + does not own out of any CA that signs its CSRs. OpenSSL drops request + extensions for the same reason unless -copy_extensions says otherwise. + """ + req = run_wolfssl("req", "-in", self.san_csr, "-text", "-noout") + self.assertEqual(req.returncode, 0, req.stderr) + self.assertIn("notmine.example.com", req.stdout + req.stderr, + "the request under test must actually carry a SAN") + + r, out = self._ca_sign("reqca_dropsan.pem", csr=self.san_csr) + self.assertEqual(r.returncode, 0, r.stderr) + + text = self._text(out) + self.assertNotIn("notmine.example.com", text, + "the requested SAN must not reach the issued cert") + self.assertNotIn("Subject Alternative Name", text) + + def test_ca_sign_drops_request_key_usage(self): + """Nor is a keyUsage the requester asked for issued. + + Guards the same rule on a second extension: the fix is to build the + leaf from the request's subject and key alone, not to special case + the one extension that was noticed first.""" + req = run_wolfssl("req", "-in", self.ku_csr, "-text", "-noout") + self.assertEqual(req.returncode, 0, req.stderr) + self.assertIn("Key Usage", req.stdout + req.stderr, + "the request under test must actually carry a keyUsage") + + r, out = self._ca_sign("reqca_dropku.pem", csr=self.ku_csr) + self.assertEqual(r.returncode, 0, r.stderr) + self.assertNotIn("Key Usage", self._text(out)) + + def test_ca_sign_says_it_is_dropping_the_request_extensions(self): + """Dropping them is reported, so the omission is not a surprise. + + An operator who put the subjectAltName in the CSR would otherwise get + a certificate quietly missing it, at exit 0, with nothing pointing at + -copy_extensions.""" + r, out = self._ca_sign("reqca_dropnotice.pem", csr=self.san_csr) + self.assertEqual(r.returncode, 0, r.stderr) + self.assertIn("-copy_extensions", r.stdout + r.stderr) + + def test_ca_sign_no_drop_notice_when_copying(self): + """The notice is about a drop, so copying must not print it.""" + r, out = self._ca_sign("reqca_nodropnotice.pem", + "-copy_extensions", "copy", csr=self.san_csr) + self.assertEqual(r.returncode, 0, r.stderr) + self.assertNotIn("Ignoring the extensions", r.stdout + r.stderr) + + def test_ca_sign_copy_extensions_carries_the_request_san(self): + """-copy_extensions copy is the opt-in that does carry them over.""" + r, out = self._ca_sign("reqca_copysan.pem", "-copy_extensions", "copy", + csr=self.san_csr) + self.assertEqual(r.returncode, 0, r.stderr) + + text = self._text(out) + self.assertIn("Subject Alternative Name", text) + self.assertIn("notmine.example.com", text) + + def test_ca_sign_copy_extensions_copyall_carries_the_request_san(self): + """copyall is accepted as a synonym for copy. + + OpenSSL separates the two by whether extensions the issuer also sets + are skipped, but wolfCLU applies its own Basic Constraints, SKID and + AKID after the copy either way, so they land in the same place.""" + r, out = self._ca_sign("reqca_copyallsan.pem", + "-copy_extensions", "copyall", csr=self.san_csr) + self.assertEqual(r.returncode, 0, r.stderr) + self.assertIn("notmine.example.com", self._text(out)) + + def test_ca_sign_copy_extensions_none_matches_the_default(self): + """Spelling the default out explicitly drops them just the same.""" + r, out = self._ca_sign("reqca_copynone.pem", "-copy_extensions", "none", + csr=self.san_csr) + self.assertEqual(r.returncode, 0, r.stderr) + self.assertNotIn("notmine.example.com", self._text(out)) + + def test_ca_sign_copy_extensions_still_forces_ca_false(self): + """Even with -copy_extensions the requester does not get CA:TRUE. + + The copy is an opt-in to the requester's *leaf* extensions; Basic + Constraints stays the issuer's call, or the opt-in would quietly hand + out the sub-CA the CA:TRUE downgrade exists to refuse.""" + r, out = self._ca_sign("reqca_copycatrue.pem", + "-copy_extensions", "copy", csr=self.ca_true_csr) + self.assertEqual(r.returncode, 0, r.stderr) + + text = self._text(out) + self.assertIn("CA:FALSE", text) + self.assertNotIn("CA:TRUE", text) + self.assertIn("Warning", r.stderr) + + def test_ca_sign_copy_extensions_rejects_an_unknown_value(self): + """A misspelled value fails instead of silently meaning "none". + + Treating an unrecognized value as the default would issue a + certificate missing the extensions the operator asked to carry, at + exit 0.""" + r, out = self._ca_sign("reqca_copybogus.pem", + "-copy_extensions", "sure", csr=self.san_csr) + self.assertNotEqual(r.returncode, 0) + self.assertFalse(os.path.exists(out), + "no certificate should be written on a bad value") + + def test_ca_sign_preserves_the_request_public_key(self): + """The issued cert certifies the key the request carried. + + The leaf is built fresh rather than signed in place, so the public key + has to be carried across deliberately; getting the CA's key here, or + an empty one, would be certifying the wrong subject entirely.""" + r, out = self._ca_sign("reqca_pubkey.pem", csr=self.san_csr) + self.assertEqual(r.returncode, 0, r.stderr) + + req_key = run_wolfssl("req", "-in", self.san_csr, "-text", "-noout") + self.assertEqual(req_key.returncode, 0, req_key.stderr) + + def modulus(text): + """The first line of the printed modulus, signature bytes look + the same but do not follow a 'Modulus:' header.""" + m = re.search(r"Modulus:\s*\n\s*([0-9a-f:]+)", text) + return m.group(1) if m else None + + req_mod = modulus(req_key.stdout) + self.assertIsNotNone(req_mod, "no modulus found in the request") + self.assertEqual(req_mod, modulus(self._text(out)), + "issued cert should carry the request's public key") + + def test_ca_sign_subject_key_id_does_not_depend_on_copy_extensions(self): + """The leaf's SKID is the same whether or not extensions are copied. + + wolfSSL derives it from WOLFSSL_X509.pubKey, which holds the bare key + bits on a parsed request but a whole SubjectPublicKeyInfo on a key + installed with X509_set_pubkey(). Only the former is the RFC 5280 + 4.2.1.2 value every other tool computes, so the two paths drifting + apart here means the fresh-built leaf took the wrong hash.""" + plain, out_plain = self._ca_sign("reqca_skid_plain.pem", + csr=self.san_csr) + self.assertEqual(plain.returncode, 0, plain.stderr) + copied, out_copied = self._ca_sign("reqca_skid_copy.pem", + "-copy_extensions", "copy", + csr=self.san_csr) + self.assertEqual(copied.returncode, 0, copied.stderr) + + skid_plain = _ext_value(self._text(out_plain), + "X509v3 Subject Key Identifier") + skid_copied = _ext_value(self._text(out_copied), + "X509v3 Subject Key Identifier") + self.assertIsNotNone(skid_plain, "leaf has no Subject Key Identifier") + self.assertEqual(skid_plain, skid_copied) + + def test_copy_extensions_without_ca_is_reported_as_ignored(self): + """-copy_extensions outside the -CA path says it is doing nothing. + + A CSR is built from the options given here, so there is nothing to + copy; dropping the option silently would read as having applied it.""" + out = _tmp("reqca_copy_noca.csr") + self._clean(out) + r = run_wolfssl("req", "-new", + "-key", os.path.join(CERTS_DIR, "server-key.pem"), + "-subj", self.RSA_SUBJ, + "-copy_extensions", "copy", "-out", out) + self.assertEqual(r.returncode, 0, r.stderr) + self.assertIn("Ignoring -copy_extensions", r.stdout + r.stderr) + def test_ca_sign_produces_v3_certificate(self): """The issued cert is X.509 v3. @@ -1510,8 +1728,8 @@ def test_x509_default_serial_is_wide_and_unique(self): """The default serial on the self-signed path is wide and non zero. Same reasoning as test_ca_sign_default_serial_is_wide_and_unique: a - 24-bit draw collides after a few thousand certs, and a zero draw would - skip set_serialNumber entirely because this path also gates on + narrow draw collides after a few thousand certs, and a zero draw would + have skipped set_serialNumber entirely back when this path gated on "serial > 0".""" seen = set() for i in range(4): @@ -1524,9 +1742,9 @@ def test_x509_default_serial_is_wide_and_unique(self): value = int(hexval, 16) self.assertGreater(value, 0, "serial must be positive and non zero") - # 24 bits of entropy fits in 6 hex digits; require more than that - self.assertGreater(len(hexval), 8, - "serial {!r} is too narrow".format(hexval)) + # same fixed 8 byte width as the -CA path above + self.assertEqual(len(hexval), 16, + "serial {!r} is not 8 bytes wide".format(hexval)) seen.add(hexval) self.assertEqual(len(seen), 4, "serials repeated: {}".format(seen)) @@ -2252,8 +2470,9 @@ def test_addext_subj_alt_name_critical_prefix_token_passes(self): text = self._addext_text( ["subjectAltName=critical,DNS:mixed.example.com"], "test_addext_subjan_crit_prefix_pass.crt") + # only acceptance is checked: wolfSSL cannot mark a subjectAltName + # critical, which wolfCLU warns about rather than applying self.assertIn("DNS:mixed.example.com", text) - self.assertIn("critical", text) def test_addext_authority_key_identifier_always_suffix(self): """The OpenSSL ":always" suffix on keyid is accepted.""" @@ -2450,6 +2669,52 @@ def test_addext_truncated_name_fails(self): self._addext_fails("keyUsag=digitalSignature", "test_addext_truncated.crt") + def test_addext_extended_key_usage_any_with_named_fails(self): + """"any" beside a named purpose must be rejected, not half applied. + + wolfSSL's SetExtKeyUsage() short circuits on EXTKEYUSE_ANY and emits + anyExtendedKeyUsage alone, so serverAuth here would be accepted and + then dropped from the certificate at exit 0. OpenSSL emits both.""" + self._addext_fails("extendedKeyUsage=any,serverAuth", + "test_addext_eku_any_named.crt") + + def test_addext_extended_key_usage_any_alone_is_accepted(self): + """"any" on its own is still a valid extendedKeyUsage.""" + text = self._addext_text(["extendedKeyUsage=any"], + "test_addext_eku_any.crt") + self.assertIsNotNone(_ext_value(text, "X509v3 Extended Key Usage"), + "extended key usage not found in output") + + def test_addext_subject_alt_name_empty_fails(self): + """A subjectAltName naming nothing is reported, not dropped. + + Every sibling parser fails on an empty value; this one used to return + success having added no names at all.""" + self._addext_fails("subjectAltName=", + "test_addext_san_empty.crt") + + def test_addext_subject_alt_name_critical_only_fails(self): + """"critical" is a flag, so it cannot be the whole value.""" + self._addext_fails("subjectAltName=critical", + "test_addext_san_crit_only.crt") + + def test_addext_key_usage_is_always_emitted_critical(self): + """wolfSSL always marks keyUsage critical, with or without the flag. + + EncodeExtensions() sets the criticality flag whenever a key usage is + present and CopyX509ToCert() never carries keyUsageCrit across, so the + two spellings have to produce the same extension. Pinned here because + the diagnostic wolfCLU prints about it is easy to invert.""" + for i, val in enumerate(("keyCertSign,cRLSign", + "critical,keyCertSign,cRLSign")): + with self.subTest(keyUsage=val): + text = self._addext_text( + ["keyUsage=" + val], + "test_addext_ku_crit{}.crt".format(i)) + self.assertIn("X509v3 Key Usage: critical", text, + "keyUsage was not emitted critical") + + EXT_SECTION_CONF = """\ [ ext_all ] basicConstraints = CA:TRUE @@ -2468,6 +2733,9 @@ def test_addext_truncated_name_fails(self): [ ext_bad_eku ] extendedKeyUsage = serverAuth,bogusUsage + +[ ext_missing_san_section ] +subjectAltName = @no_such_section """ @@ -2594,6 +2862,25 @@ def test_extfile_subject_alt_name_section(self): self.assertIn("DNS:section.example.com", san) self.assertIn("IP Address:192.0.2.7", san) + def test_extfile_subject_alt_name_missing_section_fails(self): + """"subjectAltName = @section" naming a section that is not there + fails the command. + + It used to leave ret at success, so the certificate was issued with no + alt names at all and the operator was never told the section name was + wrong.""" + crt = _tmp("test_extfile_san_missing_sect.crt") + self._clean(crt) + r = run_wolfssl("x509", "-req", "-in", self.csr, "-days", "3650", + "-extfile", self.conf, + "-extensions", "ext_missing_san_section", + "-signkey", + os.path.join(CERTS_DIR, "server-key.pem"), + "-out", crt) + self.assertNotEqual(r.returncode, 0, + "a missing alt name section must fail the command") + self.assertIn("no_such_section", r.stdout + r.stderr) + class TestReqOptionHandling(unittest.TestCase): """Option level behaviour: file aliasing, diagnostics, dropped values.""" @@ -2679,26 +2966,6 @@ def test_addext_akid_without_a_key_id_is_reported(self): # is covered by TestReqAddExtNames. # test_addext_authority_key_identifier_issuer_only_is_skipped - def test_keyout_same_file_spelled_differently_keeps_both(self): - """The shared-stream case is decided by which file the paths resolve - to, not by the two option strings being byte-identical.""" - both = _tmp("test_req_keyout_alias.pem") - self._clean(both) - # name the same file two ways: absolute, and via an explicit "." - aliased = os.path.join(os.path.dirname(both), ".", - os.path.basename(both)).replace("\\", "/") - - r = run_wolfssl("req", "-new", "-newkey", "rsa:2048", "-nodes", - "-subj", "/C=US/CN=test", - "-keyout", both, "-out", aliased) - self.assertEqual(r.returncode, 0, r.stdout + r.stderr) - - with open(both) as f: - text = f.read() - self.assertIn("PRIVATE KEY", text, - "the key was truncated by the -out open") - self.assertIn("BEGIN CERTIFICATE REQUEST", text) - def test_newkey_algorithm_conflict_either_order(self): """The -newkey/-ecc agreement check must not depend on argv order.""" for args in (("-ecc", "-newkey", "rsa:2048"), diff --git a/wolfclu/clu_optargs.h b/wolfclu/clu_optargs.h index 41562e01..dc71f1e6 100644 --- a/wolfclu/clu_optargs.h +++ b/wolfclu/clu_optargs.h @@ -98,6 +98,7 @@ enum { WOLFCLU_CONFIG, WOLFCLU_EXTENSIONS, WOLFCLU_ADDEXT, + WOLFCLU_COPY_EXTENSIONS, WOLFCLU_SERIAL, WOLFCLU_CAKEY, WOLFCLU_CURVE_NAME, diff --git a/wolfclu/x509/clu_cert.h b/wolfclu/x509/clu_cert.h index 6dd9ef71..6358014b 100644 --- a/wolfclu/x509/clu_cert.h +++ b/wolfclu/x509/clu_cert.h @@ -27,10 +27,15 @@ #define RAW_FORM 3 /* Default validity, in days, for a cert */ -#define WOLFCLU_DEFAULT_VALIDITY 20 +#define WOLFCLU_DEFAULT_VALIDITY 30 /* Max number of days that when converted to seconds will not overflow an int */ #define WOLFCLU_MAX_VALIDITY 24855 +/* Width, in bytes, of a randomly generated certificate serial number. Fixed + * rather than tied to sizeof(long) so the entropy is the same on every + * target, including the ones where a long is 32 bits. */ +#define WOLFCLU_SERIAL_SIZE 8 + /* handles incoming arguments for certificate generation */ int wolfCLU_certSetup(int argc, char** argv); From 466704f644841d7fce15b2673cb08ecc11a21a0b Mon Sep 17 00:00:00 2001 From: Aidan Keefe Date: Mon, 24 Aug 2026 16:54:46 -0600 Subject: [PATCH 3/3] Fix compile error for clang --- src/x509/clu_request_setup.c | 4 ++-- src/x509/clu_x509_sign.c | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/x509/clu_request_setup.c b/src/x509/clu_request_setup.c index 5c6981de..107d6499 100644 --- a/src/x509/clu_request_setup.c +++ b/src/x509/clu_request_setup.c @@ -916,7 +916,7 @@ static int selfSignCert(WOLFSSL_X509* x509, WOLFSSL_EVP_PKEY* pkey, } else { notBefore = wolfSSL_ASN1_TIME_adj(NULL, t, 0, 0); - notAfter = wolfSSL_ASN1_TIME_adj(NULL, t, days, 0); + notAfter = wolfSSL_ASN1_TIME_adj(NULL, t, (int)days, 0); if (notBefore == NULL || notAfter == NULL) { wolfCLU_LogError("Error creating not before/after dates"); ret = WOLFCLU_FATAL_ERROR; @@ -1154,7 +1154,7 @@ static int caSignCert(WOLFSSL_X509** x509, WOLFSSL_BIO* caBio, } else { notBefore = wolfSSL_ASN1_TIME_adj(NULL, t, 0, 0); - notAfter = wolfSSL_ASN1_TIME_adj(NULL, t, days, 0); + notAfter = wolfSSL_ASN1_TIME_adj(NULL, t, (int)days, 0); if (notBefore == NULL || notAfter == NULL) { wolfCLU_LogError("Error creating not before/after dates"); ret = WOLFCLU_FATAL_ERROR; diff --git a/src/x509/clu_x509_sign.c b/src/x509/clu_x509_sign.c index c7219402..3e455cb3 100644 --- a/src/x509/clu_x509_sign.c +++ b/src/x509/clu_x509_sign.c @@ -1026,7 +1026,7 @@ int wolfCLU_CertSetDate(WOLFSSL_X509* x509, int days) } notBefore = wolfSSL_ASN1_TIME_adj(NULL, t, 0, 0); - notAfter = wolfSSL_ASN1_TIME_adj(NULL, t, days, 0); + notAfter = wolfSSL_ASN1_TIME_adj(NULL, t, (int)days, 0); if (notBefore == NULL || notAfter == NULL) { wolfCLU_LogError("Error creating not before/after dates"); ret = WOLFCLU_FATAL_ERROR;