diff --git a/examples/crypto_policies/README.md b/examples/crypto_policies/README.md new file mode 100644 index 00000000000..3f6deb9f74e --- /dev/null +++ b/examples/crypto_policies/README.md @@ -0,0 +1,140 @@ +# wolfSSL crypto-policy files + +This directory ships two kinds of policy files, both consumed by +`wolfSSL_crypto_policy_enable(path)` (or +`wolfSSL_crypto_policy_enable_buffer(buf)`): + +| File | Format | Code path | +|---|---|---| +| `/wolfssl.txt` | Legacy single-line `@SECLEVEL=N:...` cipher string | `crypto_policy_parse()` in `src/ssl.c` | +| `/wolfssl-allowlist.txt` | Granular sectioned allowlist | `wolfSSL_crypto_policy_parse_granular()` in `src/crypto_policy_granular.c` | + +`wolfSSL_crypto_policy_enable()` sniffs the file header (first non-blank +non-comment line) and dispatches to the matching parser. The two +formats coexist; existing deployments that point at a legacy file keep +working unchanged. + +## Why two formats? + +The legacy `@SECLEVEL=N:EECDH:kRSA:...` format was rejected by the +Fedora crypto-policies maintainers as +[insufficient](https://gitlab.com/redhat-crypto/fedora-crypto-policies/-/issues/60) +because it inherits the OpenSSL cipher-string DSL: opaque family +aliases, a coarse `@SECLEVEL` integer that bundles unrelated decisions +together, and no granular control over signature schemes, named +groups, or per-version protocol enablement. + +The allowlist format mirrors the GnuTLS back-end that crypto-policies +already endorses as granular: explicit primitive names, one directive +per primitive, grouped by category. The vocabulary is owned by +crypto-policies; the wolfSSL-side mapping tables live in +`src/crypto_policy_granular.c`. + +## Allowlist file format + +```ini +# Header — mandatory. +version = 1 +override-mode = allowlist + +[protocols] +enabled-version = TLS1.2 # one directive per enabled value +enabled-version = TLS1.3 +enabled-version = DTLS1.2 + +[ciphers] +enabled-cipher = AES-256-GCM +enabled-cipher = AES-128-GCM +enabled-cipher = CHACHA20-POLY1305 + +[key-exchange] +enabled-kx = ECDHE +enabled-kx = DHE-RSA + +[macs] +enabled-mac = AEAD +enabled-mac = HMAC-SHA2-256 +enabled-mac = HMAC-SHA2-384 + +[hashes] +enabled-hash = SHA2-256 +enabled-hash = SHA2-384 +enabled-hash = SHA2-512 + +[groups] +enabled-group = X25519 +enabled-group = SECP256R1 +enabled-group = SECP384R1 + +[signatures] +enabled-sig = ECDSA-SHA2-256 +enabled-sig = ECDSA-SHA2-384 +enabled-sig = RSA-SHA2-256 + +[constraints] +min-rsa-bits = 2048 +min-dh-bits = 2048 +min-dsa-bits = 2048 +security-level = 2 +``` + +Rules: + +* `version = 1` is the only format this build understands. A higher + version is rejected outright (`WOLFSSL_BAD_FILE`) rather than parsed + under wrong semantics. +* `override-mode = allowlist` is mandatory. +* Section headers (`[protocols]`, …) are cosmetic; only `key = value` + lines drive parsing. +* `#` introduces a line comment. +* Unknown tokens (for instance, post-quantum primitives a given + wolfSSL build does not implement) are tolerated silently. The + intersection of "policy-enabled" ∩ "build-supported" is what gets + applied to every `WOLFSSL_CTX`. +* Per-category limit: 64 tokens, 48 bytes each. +* File size limit: 1 MiB. + +## What the apply step drives + +For every `WOLFSSL_CTX` created after the policy is enabled, the +applier calls (in order): + +1. `wolfSSL_CTX_SetMinVersion` from the lowest `enabled-version`. +2. `wolfSSL_CTX_set_cipher_list` from the cross-product + `cipher × kx × mac × version` against the build's known TLS suites. +3. `wolfSSL_CTX_UseSupportedCurve` for each mapped `enabled-group`. +4. `wolfSSL_CTX_set1_sigalgs_list` from the mapped `enabled-sig` set. +5. `wolfSSL_CTX_SetMinRsaKey_Sz` / `SetMinDhKey_Sz` / + `SetMinEccKey_Sz` from `min-rsa-bits` / `min-dh-bits` + (ECC floor derived from RSA-equivalent strength). + +Steps 1, 3 and 4 are best-effort: if a build lacks the primitive (no +TLS 1.0 support, no `rsa_pss_*`), the applier logs and continues +rather than tearing down the CTX — the remaining steps still enforce +the policy. + +## The five fixtures shipped here + +`legacy/`, `default/`, `future/`, `fips/`, `bsi/` are unmodified +outputs of the Fedora crypto-policies generator. They are checked into +this tree so the wolfSSL unit tests can exercise the parser end-to-end +against the same files a Fedora install would produce. Regenerate +with: + +```sh +python3 build-crypto-policies.py --flat --policy DEFAULT policies out +cp out/DEFAULT-wolfssl.txt \ + examples/crypto_policies/default/wolfssl-allowlist.txt +``` + +## Related upstream issues + +* wolfSSL [#9802](https://github.com/wolfSSL/wolfssl/issues/9802) — full + Fedora crypto-policies support tracking issue. +* fedora-crypto-policies + [work item #60](https://gitlab.com/redhat-crypto/fedora-crypto-policies/-/issues/60) + — file format coordination. +* The OpenSSL [`opensslcnf.config`](https://gitlab.com/redhat-crypto/fedora-crypto-policies/-/blob/main/python/policygenerators/openssl.py) + and GnuTLS + [`gnutls.config`](https://gitlab.com/redhat-crypto/fedora-crypto-policies/-/blob/main/python/policygenerators/gnutls.py) + generators are the precedents this allowlist format follows. diff --git a/examples/crypto_policies/default/wolfssl-allowlist-dtls.txt b/examples/crypto_policies/default/wolfssl-allowlist-dtls.txt new file mode 100644 index 00000000000..e9f89966e7a --- /dev/null +++ b/examples/crypto_policies/default/wolfssl-allowlist-dtls.txt @@ -0,0 +1,42 @@ +# wolfSSL system-wide cryptographic policy. +# Test fixture: DTLS-only (drives the WOLFSSL_DTLSV* min-version path). +# Generated to match the DTLS slice of the Fedora DEFAULT policy. + +version = 1 +override-mode = allowlist + +[protocols] +enabled-version = DTLS1.2 + +[ciphers] +enabled-cipher = AES-256-GCM +enabled-cipher = AES-128-GCM +enabled-cipher = CHACHA20-POLY1305 + +[key-exchange] +enabled-kx = ECDHE +enabled-kx = DHE-RSA + +[macs] +enabled-mac = AEAD + +[hashes] +enabled-hash = SHA2-256 +enabled-hash = SHA2-384 + +[groups] +enabled-group = X25519 +enabled-group = SECP256R1 +enabled-group = SECP384R1 + +[signatures] +enabled-sig = ECDSA-SHA2-256 +enabled-sig = ECDSA-SHA2-384 +enabled-sig = RSA-SHA2-256 +enabled-sig = RSA-SHA2-384 + +[constraints] +min-rsa-bits = 2048 +min-dh-bits = 2048 +min-dsa-bits = 2048 +security-level = 2 diff --git a/examples/crypto_policies/default/wolfssl-allowlist.txt b/examples/crypto_policies/default/wolfssl-allowlist.txt new file mode 100644 index 00000000000..c6535552394 --- /dev/null +++ b/examples/crypto_policies/default/wolfssl-allowlist.txt @@ -0,0 +1,113 @@ +# wolfSSL system-wide cryptographic policy. +# Auto-generated by Fedora crypto-policies -- do not edit. +# Consumed at runtime by wolfSSL_crypto_policy_enable(). + +version = 1 +override-mode = allowlist + +[protocols] +enabled-version = TLS1.3 +enabled-version = TLS1.2 +enabled-version = DTLS1.2 + +[ciphers] +enabled-cipher = AES-256-GCM +enabled-cipher = AES-256-CCM +enabled-cipher = CHACHA20-POLY1305 +enabled-cipher = AES-256-CBC +enabled-cipher = AES-128-GCM +enabled-cipher = AES-128-CCM +enabled-cipher = AES-128-CBC + +[key-exchange] +enabled-kx = KEM-ECDH +enabled-kx = ECDHE +enabled-kx = RSA +enabled-kx = DHE +enabled-kx = DHE-RSA +enabled-kx = PSK +enabled-kx = DHE-PSK +enabled-kx = ECDHE-PSK +enabled-kx = RSA-PSK +enabled-kx = ECDHE-GSS +enabled-kx = DHE-GSS + +[macs] +enabled-mac = AEAD +enabled-mac = HMAC-SHA2-256 +enabled-mac = HMAC-SHA1 +enabled-mac = UMAC-128 +enabled-mac = HMAC-SHA2-384 +enabled-mac = HMAC-SHA2-512 + +[hashes] +enabled-hash = SHA2-256 +enabled-hash = SHA2-384 +enabled-hash = SHA2-512 +enabled-hash = SHA3-256 +enabled-hash = SHA3-384 +enabled-hash = SHA3-512 +enabled-hash = SHA2-224 +enabled-hash = SHA3-224 +enabled-hash = SHAKE-256 + +[groups] +enabled-group = MLKEM768-X25519 +enabled-group = P256-MLKEM768 +enabled-group = P384-MLKEM1024 +enabled-group = MLKEM1024-X448 +enabled-group = X25519 +enabled-group = SECP256R1 +enabled-group = X448 +enabled-group = SECP521R1 +enabled-group = SECP384R1 +enabled-group = FFDHE-2048 +enabled-group = FFDHE-3072 +enabled-group = FFDHE-4096 +enabled-group = FFDHE-6144 +enabled-group = FFDHE-8192 + +[signatures] +enabled-sig = MLDSA44 +enabled-sig = MLDSA65 +enabled-sig = MLDSA87 +enabled-sig = ECDSA-SHA3-256 +enabled-sig = ECDSA-SHA2-256 +enabled-sig = ECDSA-SHA2-256-FIDO +enabled-sig = ECDSA-SHA3-384 +enabled-sig = ECDSA-SHA2-384 +enabled-sig = ECDSA-SHA3-512 +enabled-sig = ECDSA-SHA2-512 +enabled-sig = EDDSA-ED25519 +enabled-sig = EDDSA-ED25519-FIDO +enabled-sig = EDDSA-ED448 +enabled-sig = RSA-PSS-SHA3-256 +enabled-sig = RSA-PSS-SHA2-256 +enabled-sig = RSA-PSS-SHA3-384 +enabled-sig = RSA-PSS-SHA2-384 +enabled-sig = RSA-PSS-SHA3-512 +enabled-sig = RSA-PSS-SHA2-512 +enabled-sig = RSA-PSS-RSAE-SHA3-256 +enabled-sig = RSA-PSS-RSAE-SHA2-256 +enabled-sig = RSA-PSS-RSAE-SHA3-384 +enabled-sig = RSA-PSS-RSAE-SHA2-384 +enabled-sig = RSA-PSS-RSAE-SHA3-512 +enabled-sig = RSA-PSS-RSAE-SHA2-512 +enabled-sig = RSA-SHA3-256 +enabled-sig = RSA-SHA2-256 +enabled-sig = RSA-SHA3-384 +enabled-sig = RSA-SHA2-384 +enabled-sig = RSA-SHA3-512 +enabled-sig = RSA-SHA2-512 +enabled-sig = ECDSA-SHA2-224 +enabled-sig = RSA-PSS-SHA2-224 +enabled-sig = RSA-SHA2-224 +enabled-sig = ECDSA-SHA3-224 +enabled-sig = RSA-PSS-SHA3-224 +enabled-sig = RSA-SHA3-224 + +[constraints] +min-rsa-bits = 2048 +min-dh-bits = 2048 +min-dsa-bits = 2048 +security-level = 2 diff --git a/examples/crypto_policies/future/wolfssl-allowlist.txt b/examples/crypto_policies/future/wolfssl-allowlist.txt new file mode 100644 index 00000000000..c46f21bda85 --- /dev/null +++ b/examples/crypto_policies/future/wolfssl-allowlist.txt @@ -0,0 +1,97 @@ +# wolfSSL system-wide cryptographic policy. +# Auto-generated by Fedora crypto-policies -- do not edit. +# Consumed at runtime by wolfSSL_crypto_policy_enable(). + +version = 1 +override-mode = allowlist + +[protocols] +enabled-version = TLS1.3 +enabled-version = TLS1.2 +enabled-version = DTLS1.2 + +[ciphers] +enabled-cipher = AES-256-GCM +enabled-cipher = AES-256-CCM +enabled-cipher = CHACHA20-POLY1305 + +[key-exchange] +enabled-kx = KEM-ECDH +enabled-kx = ECDHE +enabled-kx = DHE +enabled-kx = DHE-RSA +enabled-kx = PSK +enabled-kx = DHE-PSK +enabled-kx = ECDHE-PSK +enabled-kx = ECDHE-GSS +enabled-kx = DHE-GSS + +[macs] +enabled-mac = AEAD +enabled-mac = HMAC-SHA2-256 +enabled-mac = UMAC-128 +enabled-mac = HMAC-SHA2-384 +enabled-mac = HMAC-SHA2-512 + +[hashes] +enabled-hash = SHA2-256 +enabled-hash = SHA2-384 +enabled-hash = SHA2-512 +enabled-hash = SHA3-256 +enabled-hash = SHA3-384 +enabled-hash = SHA3-512 +enabled-hash = SHAKE-256 + +[groups] +enabled-group = MLKEM768-X25519 +enabled-group = P256-MLKEM768 +enabled-group = P384-MLKEM1024 +enabled-group = MLKEM1024-X448 +enabled-group = X25519 +enabled-group = SECP256R1 +enabled-group = X448 +enabled-group = SECP521R1 +enabled-group = SECP384R1 +enabled-group = FFDHE-3072 +enabled-group = FFDHE-4096 +enabled-group = FFDHE-6144 +enabled-group = FFDHE-8192 + +[signatures] +enabled-sig = MLDSA44 +enabled-sig = MLDSA65 +enabled-sig = MLDSA87 +enabled-sig = ECDSA-SHA3-256 +enabled-sig = ECDSA-SHA2-256 +enabled-sig = ECDSA-SHA2-256-FIDO +enabled-sig = ECDSA-SHA3-384 +enabled-sig = ECDSA-SHA2-384 +enabled-sig = ECDSA-SHA3-512 +enabled-sig = ECDSA-SHA2-512 +enabled-sig = EDDSA-ED25519 +enabled-sig = EDDSA-ED25519-FIDO +enabled-sig = EDDSA-ED448 +enabled-sig = RSA-PSS-SHA3-256 +enabled-sig = RSA-PSS-SHA2-256 +enabled-sig = RSA-PSS-SHA3-384 +enabled-sig = RSA-PSS-SHA2-384 +enabled-sig = RSA-PSS-SHA3-512 +enabled-sig = RSA-PSS-SHA2-512 +enabled-sig = RSA-PSS-RSAE-SHA3-256 +enabled-sig = RSA-PSS-RSAE-SHA2-256 +enabled-sig = RSA-PSS-RSAE-SHA3-384 +enabled-sig = RSA-PSS-RSAE-SHA2-384 +enabled-sig = RSA-PSS-RSAE-SHA3-512 +enabled-sig = RSA-PSS-RSAE-SHA2-512 +enabled-sig = RSA-SHA3-256 +enabled-sig = RSA-SHA2-256 +enabled-sig = RSA-SHA3-384 +enabled-sig = RSA-SHA2-384 +enabled-sig = RSA-SHA3-512 +enabled-sig = RSA-SHA2-512 + +[constraints] +min-rsa-bits = 3072 +min-dh-bits = 3072 +min-dsa-bits = 3072 +security-level = 3 diff --git a/examples/crypto_policies/legacy/wolfssl-allowlist.txt b/examples/crypto_policies/legacy/wolfssl-allowlist.txt new file mode 100644 index 00000000000..336fa2a75f6 --- /dev/null +++ b/examples/crypto_policies/legacy/wolfssl-allowlist.txt @@ -0,0 +1,132 @@ +# wolfSSL system-wide cryptographic policy. +# Auto-generated by Fedora crypto-policies -- do not edit. +# Consumed at runtime by wolfSSL_crypto_policy_enable(). + +version = 1 +override-mode = allowlist + +[protocols] +enabled-version = TLS1.3 +enabled-version = TLS1.2 +enabled-version = TLS1.1 +enabled-version = TLS1.0 +enabled-version = DTLS1.2 +enabled-version = DTLS1.0 + +[ciphers] +enabled-cipher = AES-256-GCM +enabled-cipher = AES-256-CCM +enabled-cipher = CHACHA20-POLY1305 +enabled-cipher = AES-256-CBC +enabled-cipher = AES-128-GCM +enabled-cipher = AES-128-CCM +enabled-cipher = AES-128-CBC +enabled-cipher = 3DES-CBC + +[key-exchange] +enabled-kx = KEM-ECDH +enabled-kx = ECDHE +enabled-kx = RSA +enabled-kx = DHE +enabled-kx = DHE-RSA +enabled-kx = DHE-DSS +enabled-kx = PSK +enabled-kx = DHE-PSK +enabled-kx = ECDHE-PSK +enabled-kx = RSA-PSK +enabled-kx = ECDHE-GSS +enabled-kx = DHE-GSS + +[macs] +enabled-mac = AEAD +enabled-mac = HMAC-SHA2-256 +enabled-mac = HMAC-SHA1 +enabled-mac = UMAC-128 +enabled-mac = HMAC-SHA2-384 +enabled-mac = HMAC-SHA2-512 + +[hashes] +enabled-hash = SHA2-256 +enabled-hash = SHA2-384 +enabled-hash = SHA2-512 +enabled-hash = SHA3-256 +enabled-hash = SHA3-384 +enabled-hash = SHA3-512 +enabled-hash = SHA2-224 +enabled-hash = SHA3-224 +enabled-hash = SHAKE-256 +enabled-hash = SHAKE-128 +enabled-hash = SHA1 + +[groups] +enabled-group = MLKEM768-X25519 +enabled-group = P256-MLKEM768 +enabled-group = P384-MLKEM1024 +enabled-group = MLKEM1024-X448 +enabled-group = X25519 +enabled-group = SECP256R1 +enabled-group = X448 +enabled-group = SECP521R1 +enabled-group = SECP384R1 +enabled-group = FFDHE-2048 +enabled-group = FFDHE-3072 +enabled-group = FFDHE-4096 +enabled-group = FFDHE-6144 +enabled-group = FFDHE-8192 +enabled-group = FFDHE-1536 + +[signatures] +enabled-sig = MLDSA44 +enabled-sig = MLDSA65 +enabled-sig = MLDSA87 +enabled-sig = ECDSA-SHA3-256 +enabled-sig = ECDSA-SHA2-256 +enabled-sig = ECDSA-SHA2-256-FIDO +enabled-sig = ECDSA-SHA3-384 +enabled-sig = ECDSA-SHA2-384 +enabled-sig = ECDSA-SHA3-512 +enabled-sig = ECDSA-SHA2-512 +enabled-sig = EDDSA-ED25519 +enabled-sig = EDDSA-ED25519-FIDO +enabled-sig = EDDSA-ED448 +enabled-sig = RSA-PSS-SHA3-256 +enabled-sig = RSA-PSS-SHA2-256 +enabled-sig = RSA-PSS-SHA3-384 +enabled-sig = RSA-PSS-SHA2-384 +enabled-sig = RSA-PSS-SHA3-512 +enabled-sig = RSA-PSS-SHA2-512 +enabled-sig = RSA-PSS-RSAE-SHA3-256 +enabled-sig = RSA-PSS-RSAE-SHA2-256 +enabled-sig = RSA-PSS-RSAE-SHA3-384 +enabled-sig = RSA-PSS-RSAE-SHA2-384 +enabled-sig = RSA-PSS-RSAE-SHA3-512 +enabled-sig = RSA-PSS-RSAE-SHA2-512 +enabled-sig = RSA-SHA3-256 +enabled-sig = RSA-SHA2-256 +enabled-sig = RSA-SHA3-384 +enabled-sig = RSA-SHA2-384 +enabled-sig = RSA-SHA3-512 +enabled-sig = RSA-SHA2-512 +enabled-sig = ECDSA-SHA2-224 +enabled-sig = RSA-PSS-SHA2-224 +enabled-sig = RSA-SHA2-224 +enabled-sig = ECDSA-SHA3-224 +enabled-sig = RSA-PSS-SHA3-224 +enabled-sig = RSA-SHA3-224 +enabled-sig = DSA-SHA2-256 +enabled-sig = DSA-SHA2-384 +enabled-sig = DSA-SHA2-512 +enabled-sig = DSA-SHA2-224 +enabled-sig = DSA-SHA3-256 +enabled-sig = DSA-SHA3-384 +enabled-sig = DSA-SHA3-512 +enabled-sig = ECDSA-SHA1 +enabled-sig = RSA-PSS-SHA1 +enabled-sig = RSA-SHA1 +enabled-sig = DSA-SHA1 + +[constraints] +min-rsa-bits = 1024 +min-dh-bits = 1024 +min-dsa-bits = 1024 +security-level = 1 diff --git a/src/crypto_policy_granular.c b/src/crypto_policy_granular.c new file mode 100644 index 00000000000..41d8bec8ce1 --- /dev/null +++ b/src/crypto_policy_granular.c @@ -0,0 +1,697 @@ +/* crypto_policy_granular.c + * + * Granular allowlist crypto-policy parser and applier for wolfSSL. + * + * Consumes the file emitted by the Fedora `crypto-policies` wolfSSL + * back-end generator (a sectioned allowlist with explicit primitive + * names) and drives the wolfSSL public API to configure a WOLFSSL_CTX + * accordingly. Coexists with the legacy single-line `@SECLEVEL=N:...` + * parser in src/ssl.c; the routing decision lives in + * wolfSSL_crypto_policy_enable*(). + * + * Vocabulary owned by crypto-policies. Mapping tables owned by wolfSSL. + * + * Reentrancy / threading: the helpers in this file are pure (no global + * state). The applier calls back into the wolfSSL public API and + * temporarily lifts the `wolfSSL_CTX_SetMinVersion` policy guard via + * the `crypto_policy_applying` flag in src/ssl.c -- that is the only + * coupling. Like the legacy crypto-policy parser, the apply step is + * documented as init-time only and is not thread-safe. + * + * Forward compatibility: unknown vocabulary tokens are tolerated + * silently so a wolfSSL build can consume a newer Fedora file without + * upgrading; the intersection of "policy-enabled intersect build-supported" + * is what actually reaches a WOLFSSL_CTX. The file *format* version + * (`version = 1`) is conversely strict: a higher version is rejected + * outright because it may redefine the meaning of existing keys. + * + * Best-effort apply: SetMinVersion / set1_sigalgs_list failures (a + * build that lacks TLS 1.0 support, or rejects an rsa_pss sigalg the + * policy lists) downgrade to a logged warning instead of tearing + * down the CTX. The cipher list and key-size floors still enforce + * the essential security level, so a partial apply is safer than + * none. + */ + +#include + +#if defined(WOLFSSL_SYS_CRYPTO_POLICY) + +#include +#include + +#include +#include +#include + +#include "crypto_policy_granular.h" + +/* -------------------------------------------------------------------- */ +/* small helpers */ +/* -------------------------------------------------------------------- */ + +static char *wcp_trim(char *s) +{ + char *end; + while (*s && isspace((unsigned char)*s)) { + s++; + } + end = s + XSTRLEN(s); + while (end > s && isspace((unsigned char)end[-1])) { + *--end = '\0'; + } + return s; +} + +static int wcp_list_add(WolfCPList *l, const char *tok) +{ + if (l->count >= WOLF_CP_MAX_TOKENS) { + return WOLF_CP_ERR_OVERFLOW; + } + if (XSTRLEN(tok) >= WOLF_CP_MAX_TOKEN_LEN) { + return WOLF_CP_ERR_OVERFLOW; + } + XSTRNCPY(l->tok[l->count], tok, WOLF_CP_MAX_TOKEN_LEN - 1); + l->tok[l->count][WOLF_CP_MAX_TOKEN_LEN - 1] = '\0'; + l->count++; + return WOLF_CP_OK; +} + +static int wcp_has(const WolfCPList *l, const char *tok) +{ + int i; + for (i = 0; i < l->count; i++) { + if (XSTRCMP(l->tok[i], tok) == 0) { + return 1; + } + } + return 0; +} + +/* -------------------------------------------------------------------- */ +/* header sniff: is this an allowlist file? */ +/* -------------------------------------------------------------------- */ + +/* A minimal, cheap test. We do not parse fully here; we just look at + * the first non-blank, non-comment line. The legacy format starts with + * `@SECLEVEL=`. The granular format starts with `version = 1`. */ +int wolfSSL_crypto_policy_is_granular(const char *buf) +{ + const char *p = buf; + + if (buf == NULL) { + return 0; + } + + while (*p != '\0') { + const char *line_start = p; + size_t n = 0; + const char *cursor; + + while (*p != '\0' && *p != '\n') { + p++; + } + + cursor = line_start; + while (cursor < p && isspace((unsigned char)*cursor)) { + cursor++; + n++; + } + + if (cursor == p) { + /* blank line */ + } + else if (*cursor == '#') { + /* comment */ + } + else { + /* first real line */ + if (XSTRNCMP(cursor, "version", 7) == 0 + || XSTRNCMP(cursor, "override-mode", 13) == 0 + || *cursor == '[') { + return 1; + } + return 0; + } + + if (*p == '\n') { + p++; + } + (void)n; + } + + return 0; +} + +/* -------------------------------------------------------------------- */ +/* parser */ +/* -------------------------------------------------------------------- */ + +int wolfSSL_crypto_policy_parse_granular(const char *buf, + WolfGranularPolicy *out, + char *err, size_t errlen) +{ + char line[WOLF_CP_MAX_LINE]; + const char *p = buf; + int lineno = 0; + int directives = 0; + + if (buf == NULL || out == NULL) { + if (err && errlen) XSNPRINTF(err, errlen, "null argument"); + return WOLF_CP_ERR_SYNTAX; + } + XMEMSET(out, 0, sizeof(*out)); + out->security_level = -1; + + while (*p != '\0') { + const char *nl = strchr(p, '\n'); + size_t len = nl ? (size_t)(nl - p) : XSTRLEN(p); + char *key, *val, *eq, *content; + + lineno++; + if (len >= sizeof(line)) { + if (err && errlen) { + XSNPRINTF(err, errlen, "line %d too long", lineno); + } + return WOLF_CP_ERR_SYNTAX; + } + XMEMCPY(line, p, len); + line[len] = '\0'; + p += len + (nl ? 1 : 0); + + content = strchr(line, '#'); + if (content) { + *content = '\0'; + } + content = wcp_trim(line); + if (*content == '\0') { + continue; /* blank / comment-only */ + } + if (*content == '[') { + continue; /* section header, cosmetic */ + } + + eq = strchr(content, '='); + if (eq == NULL) { + if (err && errlen) { + XSNPRINTF(err, errlen, + "line %d: expected 'key = value'", lineno); + } + return WOLF_CP_ERR_SYNTAX; + } + *eq = '\0'; + key = wcp_trim(content); + val = wcp_trim(eq + 1); + if (*key == '\0' || *val == '\0') { + if (err && errlen) { + XSNPRINTF(err, errlen, + "line %d: empty key or value", lineno); + } + return WOLF_CP_ERR_SYNTAX; + } + directives++; + + if (XSTRCMP(key, "version") == 0) { + out->version = XATOI(val); + } else if (XSTRCMP(key, "override-mode") == 0) { + out->allowlist = (XSTRCMP(val, "allowlist") == 0); + } else if (XSTRCMP(key, "enabled-version") == 0) { + if (wcp_list_add(&out->protocols, val)) goto overflow; + } else if (XSTRCMP(key, "enabled-cipher") == 0) { + if (wcp_list_add(&out->ciphers, val)) goto overflow; + } else if (XSTRCMP(key, "enabled-kx") == 0) { + if (wcp_list_add(&out->kx, val)) goto overflow; + } else if (XSTRCMP(key, "enabled-mac") == 0) { + if (wcp_list_add(&out->macs, val)) goto overflow; + } else if (XSTRCMP(key, "enabled-hash") == 0) { + if (wcp_list_add(&out->hashes, val)) goto overflow; + } else if (XSTRCMP(key, "enabled-group") == 0) { + if (wcp_list_add(&out->groups, val)) goto overflow; + } else if (XSTRCMP(key, "enabled-sig") == 0) { + if (wcp_list_add(&out->sigs, val)) goto overflow; + } else if (XSTRCMP(key, "min-rsa-bits") == 0) { + out->min_rsa_bits = XATOI(val); + } else if (XSTRCMP(key, "min-dh-bits") == 0) { + out->min_dh_bits = XATOI(val); + } else if (XSTRCMP(key, "min-dsa-bits") == 0) { + out->min_dsa_bits = XATOI(val); + } else if (XSTRCMP(key, "security-level") == 0) { + out->security_level = XATOI(val); + } + /* Unknown key: tolerate for forward compatibility. */ + continue; + +overflow: + if (err && errlen) { + XSNPRINTF(err, errlen, + "line %d: too many '%s' entries", lineno, key); + } + return WOLF_CP_ERR_OVERFLOW; + } + + if (!out->allowlist) { + if (err && errlen) { + XSNPRINTF(err, errlen, "override-mode is not 'allowlist'"); + } + return WOLF_CP_ERR_NOT_ALLOWLIST; + } + /* `version = 1` is the only format this parser knows. A newer file + * may add directives that change the *meaning* of existing keys -- + * silently consuming them would be unsafe, so we refuse the file + * outright. Forward compatibility is the file-format author's job + * (bump the version) and ours (ship a parser that handles it). */ + if (out->version != 1) { + if (err && errlen) { + XSNPRINTF(err, errlen, + "unsupported policy file version: %d (expect 1)", + out->version); + } + return WOLF_CP_ERR_SYNTAX; + } + if (directives < 2) { + if (err && errlen) { + XSNPRINTF(err, errlen, "policy has no usable directives"); + } + return WOLF_CP_ERR_EMPTY; + } + if (err && errlen) err[0] = '\0'; + return WOLF_CP_OK; +} + +/* -------------------------------------------------------------------- */ +/* mapping tables: crypto-policies vocabulary -> wolfSSL */ +/* -------------------------------------------------------------------- */ + +struct wcp_kv_int { const char *cp; int wolf; }; +struct wcp_kv_str { const char *cp; const char *wolf; }; + +/* TLS named groups (wolfSSL_CTX_UseSupportedCurve). */ +static const struct wcp_kv_int wcp_group_map[] = { + { "X25519", WOLFSSL_ECC_X25519 }, + { "X448", WOLFSSL_ECC_X448 }, + { "SECP256R1", WOLFSSL_ECC_SECP256R1 }, + { "SECP384R1", WOLFSSL_ECC_SECP384R1 }, + { "SECP521R1", WOLFSSL_ECC_SECP521R1 }, +#ifdef HAVE_FFDHE_2048 + { "FFDHE-2048", WOLFSSL_FFDHE_2048 }, +#endif +#ifdef HAVE_FFDHE_3072 + { "FFDHE-3072", WOLFSSL_FFDHE_3072 }, +#endif +#ifdef HAVE_FFDHE_4096 + { "FFDHE-4096", WOLFSSL_FFDHE_4096 }, +#endif +#ifdef HAVE_FFDHE_6144 + { "FFDHE-6144", WOLFSSL_FFDHE_6144 }, +#endif +#ifdef HAVE_FFDHE_8192 + { "FFDHE-8192", WOLFSSL_FFDHE_8192 }, +#endif + { NULL, 0 } +}; + +/* TLS protocol versions for SetMinVersion / max-version pin. */ +static const struct wcp_kv_int wcp_version_map[] = { + { "TLS1.0", WOLFSSL_TLSV1 }, + { "TLS1.1", WOLFSSL_TLSV1_1 }, + { "TLS1.2", WOLFSSL_TLSV1_2 }, + { "TLS1.3", WOLFSSL_TLSV1_3 }, + { "DTLS1.0", WOLFSSL_DTLSV1 }, + { "DTLS1.2", WOLFSSL_DTLSV1_2 }, + { "DTLS1.3", WOLFSSL_DTLSV1_3 }, + { NULL, 0 } +}; + +/* TLS signature schemes (wolfSSL_CTX_set1_sigalgs_list). */ +static const struct wcp_kv_str wcp_sig_map[] = { + { "ECDSA-SHA2-256", "ECDSA+SHA256" }, + { "ECDSA-SHA2-384", "ECDSA+SHA384" }, + { "ECDSA-SHA2-512", "ECDSA+SHA512" }, + { "RSA-PSS-SHA2-256", "rsa_pss_pss_sha256" }, + { "RSA-PSS-SHA2-384", "rsa_pss_pss_sha384" }, + { "RSA-PSS-SHA2-512", "rsa_pss_pss_sha512" }, + { "RSA-PSS-RSAE-SHA2-256", "rsa_pss_rsae_sha256" }, + { "RSA-PSS-RSAE-SHA2-384", "rsa_pss_rsae_sha384" }, + { "RSA-PSS-RSAE-SHA2-512", "rsa_pss_rsae_sha512" }, + { "RSA-SHA2-256", "RSA+SHA256" }, + { "RSA-SHA2-384", "RSA+SHA384" }, + { "RSA-SHA2-512", "RSA+SHA512" }, + { "EDDSA-ED25519", "ed25519" }, + { "EDDSA-ED448", "ed448" }, + { NULL, NULL } +}; + +/* A TLS cipher suite is emitted only if every component it needs is + * allowlisted. kx == "" marks a TLS 1.3 suite. mac == "AEAD" for AEAD + * suites; an HMAC token otherwise. */ +struct wcp_suite { + const char *name; + const char *cipher; + const char *kx; + const char *mac; + const char *version; +}; +static const struct wcp_suite wcp_suite_table[] = { + /* TLS 1.3 */ + { "TLS13-AES256-GCM-SHA384", "AES-256-GCM", "", "AEAD", "TLS1.3" }, + { "TLS13-CHACHA20-POLY1305-SHA256", "CHACHA20-POLY1305", "", "AEAD", "TLS1.3" }, + { "TLS13-AES128-GCM-SHA256", "AES-128-GCM", "", "AEAD", "TLS1.3" }, + { "TLS13-AES128-CCM-SHA256", "AES-128-CCM", "", "AEAD", "TLS1.3" }, + /* TLS 1.2 AEAD */ + { "ECDHE-ECDSA-AES256-GCM-SHA384", "AES-256-GCM", "ECDHE", "AEAD", "TLS1.2" }, + { "ECDHE-RSA-AES256-GCM-SHA384", "AES-256-GCM", "ECDHE", "AEAD", "TLS1.2" }, + { "DHE-RSA-AES256-GCM-SHA384", "AES-256-GCM", "DHE-RSA", "AEAD", "TLS1.2" }, + { "ECDHE-ECDSA-CHACHA20-POLY1305", "CHACHA20-POLY1305", "ECDHE", "AEAD", "TLS1.2" }, + { "ECDHE-RSA-CHACHA20-POLY1305", "CHACHA20-POLY1305", "ECDHE", "AEAD", "TLS1.2" }, + { "DHE-RSA-CHACHA20-POLY1305", "CHACHA20-POLY1305", "DHE-RSA", "AEAD", "TLS1.2" }, + { "ECDHE-ECDSA-AES128-GCM-SHA256", "AES-128-GCM", "ECDHE", "AEAD", "TLS1.2" }, + { "ECDHE-RSA-AES128-GCM-SHA256", "AES-128-GCM", "ECDHE", "AEAD", "TLS1.2" }, + { "DHE-RSA-AES128-GCM-SHA256", "AES-128-GCM", "DHE-RSA", "AEAD", "TLS1.2" }, + /* TLS 1.2 CBC (HMAC) */ + { "ECDHE-ECDSA-AES256-SHA384", "AES-256-CBC", "ECDHE", "HMAC-SHA2-384", "TLS1.2" }, + { "ECDHE-RSA-AES256-SHA384", "AES-256-CBC", "ECDHE", "HMAC-SHA2-384", "TLS1.2" }, + { "ECDHE-ECDSA-AES128-SHA256", "AES-128-CBC", "ECDHE", "HMAC-SHA2-256", "TLS1.2" }, + { "ECDHE-RSA-AES128-SHA256", "AES-128-CBC", "ECDHE", "HMAC-SHA2-256", "TLS1.2" }, + { "AES256-GCM-SHA384", "AES-256-GCM", "RSA", "AEAD", "TLS1.2" }, + { "AES128-GCM-SHA256", "AES-128-GCM", "RSA", "AEAD", "TLS1.2" }, + { NULL, NULL, NULL, NULL, NULL } +}; + +static int wcp_lookup_int(const struct wcp_kv_int *m, const char *cp) +{ + int i; + for (i = 0; m[i].cp != NULL; i++) { + if (XSTRCMP(m[i].cp, cp) == 0) { + return m[i].wolf; + } + } + return -1; +} + +static const char *wcp_lookup_str(const struct wcp_kv_str *m, const char *cp) +{ + int i; + for (i = 0; m[i].cp != NULL; i++) { + if (XSTRCMP(m[i].cp, cp) == 0) { + return m[i].wolf; + } + } + return NULL; +} + +/* -------------------------------------------------------------------- */ +/* derive cipher list */ +/* -------------------------------------------------------------------- */ + +/* The IANA cipher suites used by TLS 1.x and DTLS 1.x at the same minor + * version are identical (the DTLS variant is encoded as an alias of the + * TLS code-point). The suite table tags each row with its TLS label, so + * a DTLS-only allowlist (e.g. enabled-version = DTLS1.2) must still + * enable every TLS 1.2 row that survives the other constraints -- and + * vice versa. Treat the protocol token as "TLS 1.x family" rather than + * exact string match. */ +static int wcp_protocol_family_enabled(const WolfCPList *protocols, + const char *suite_version) +{ + static const struct { const char *tls; const char *dtls; } pair[] = { + { "TLS1.2", "DTLS1.2" }, + { "TLS1.3", "DTLS1.3" }, + { NULL, NULL } + }; + int i; + + if (wcp_has(protocols, suite_version)) { + return 1; + } + for (i = 0; pair[i].tls != NULL; i++) { + if (XSTRCMP(suite_version, pair[i].tls) == 0 + && wcp_has(protocols, pair[i].dtls)) { + return 1; + } + if (XSTRCMP(suite_version, pair[i].dtls) == 0 + && wcp_has(protocols, pair[i].tls)) { + return 1; + } + } + return 0; +} + +int wolfSSL_crypto_policy_derive_cipher_list(const WolfGranularPolicy *p, + char *out, size_t outlen) +{ + int i; + size_t off = 0; + int first = 1; + + if (p == NULL || out == NULL || outlen == 0) { + return WOLF_CP_ERR_SYNTAX; + } + + out[0] = '\0'; + + for (i = 0; wcp_suite_table[i].name != NULL; i++) { + const struct wcp_suite *s = &wcp_suite_table[i]; + size_t need; + + if (!wcp_has(&p->ciphers, s->cipher)) continue; + if (!wcp_protocol_family_enabled(&p->protocols, s->version)) continue; + if (s->kx[0] != '\0' && !wcp_has(&p->kx, s->kx)) continue; + if (!wcp_has(&p->macs, s->mac)) continue; + + need = XSTRLEN(s->name) + (first ? 0 : 1); + if (off + need + 1 >= outlen) { + return WOLF_CP_ERR_OVERFLOW; + } + if (!first) { + out[off++] = ':'; + } + XMEMCPY(out + off, s->name, XSTRLEN(s->name)); + off += XSTRLEN(s->name); + out[off] = '\0'; + first = 0; + } + + return WOLF_CP_OK; +} + +/* -------------------------------------------------------------------- */ +/* derive sigalgs list */ +/* -------------------------------------------------------------------- */ + +int wolfSSL_crypto_policy_derive_sigalgs_list(const WolfGranularPolicy *p, + char *out, size_t outlen) +{ + int i; + size_t off = 0; + int first = 1; + + if (p == NULL || out == NULL || outlen == 0) { + return WOLF_CP_ERR_SYNTAX; + } + + out[0] = '\0'; + + for (i = 0; i < p->sigs.count; i++) { + const char *w = wcp_lookup_str(wcp_sig_map, p->sigs.tok[i]); + size_t need; + + if (w == NULL) continue; + + need = XSTRLEN(w) + (first ? 0 : 1); + if (off + need + 1 >= outlen) { + return WOLF_CP_ERR_OVERFLOW; + } + if (!first) { + out[off++] = ':'; + } + XMEMCPY(out + off, w, XSTRLEN(w)); + off += XSTRLEN(w); + out[off] = '\0'; + first = 0; + } + + return WOLF_CP_OK; +} + +/* -------------------------------------------------------------------- */ +/* lowest enabled TLS/DTLS version */ +/* -------------------------------------------------------------------- */ + +/* `wolfSSL_CTX_SetMinVersion` rejects a TLS constant on a DTLS CTX (and + * vice versa). The min-version computation must therefore be scoped to + * the CTX's protocol family, otherwise a policy that enables both TLS + * and DTLS would pass a TLS constant into a DTLS CTX and the floor + * would be silently dropped. is_dtls != 0 restricts the search to DTLS + * tokens, is_dtls == 0 restricts it to TLS tokens. */ +int wolfSSL_crypto_policy_min_version(const WolfGranularPolicy *p, + int is_dtls) +{ + int i; + int best = -1; + int best_pri = 1 << 30; + + if (p == NULL) return -1; + + for (i = 0; i < p->protocols.count; i++) { + const char *tok = p->protocols.tok[i]; + int v; + int tok_is_dtls = (XSTRNCMP(tok, "DTLS", 4) == 0); + if (tok_is_dtls != (is_dtls != 0)) { + continue; + } + v = wcp_lookup_int(wcp_version_map, tok); + if (v < 0) continue; + /* Pin "min" to numerically lowest within the family. The + * wolfSSL public enum is monotonic per family: TLSV1=1 < + * TLSV1_1=2 < TLSV1_2=3 < TLSV1_3=4, and DTLSV1=5 < + * DTLSV1_2=6 < DTLSV1_3=7. We've already filtered by family + * above, so the smallest value is the oldest version in that + * family -- the right floor. */ + if (v < best_pri) { + best_pri = v; + best = v; + } + } + return best; +} + +/* -------------------------------------------------------------------- */ +/* apply: drive real wolfSSL public API on a WOLFSSL_CTX */ +/* -------------------------------------------------------------------- */ + +int wolfSSL_crypto_policy_apply_granular(WOLFSSL_CTX *ctx, + const WolfGranularPolicy *p) +{ + int rc; + int i; + char buf[2048]; + int min_ver; + int is_dtls; + + if (ctx == NULL || p == NULL) { + return BAD_FUNC_ARG; + } + + WOLFSSL_ENTER("wolfSSL_crypto_policy_apply_granular"); + + is_dtls = (ctx->method != NULL + && ctx->method->version.major == DTLS_MAJOR); + + /* 1. Protocol min version. Best-effort: a TLS 1.0 floor against a + * build that lacks WOLFSSL_ALLOW_TLSV10 must not tear down the CTX + * -- we keep the wolfSSL-default downgrade floor and let the cipher + * list + key-size floors carry the policy. The caller will still + * negotiate within the build's supported version range. + * + * We resolve the floor inside the CTX's protocol family. Passing a + * TLS constant to a DTLS CTX (or vice versa) makes SetMinVersion + * fail and the floor would be silently dropped. */ + min_ver = wolfSSL_crypto_policy_min_version(p, is_dtls); + if (min_ver >= 0) { + rc = wolfSSL_CTX_SetMinVersion(ctx, min_ver); + if (rc != WOLFSSL_SUCCESS) { + WOLFSSL_MSG_EX("granular policy: SetMinVersion(%d) rejected by " + "build: %d (continuing)", min_ver, rc); + } + } + + /* 2. Cipher list. An allowlist is authoritative: if the + * intersection of policy-enabled cipher suites and the suite table + * is empty, the CTX would silently keep its default cipher list + * and the policy would not actually constrain anything. Refuse + * outright in that case. */ + rc = wolfSSL_crypto_policy_derive_cipher_list(p, buf, sizeof(buf)); + if (rc != WOLF_CP_OK) { + WOLFSSL_MSG("granular policy: cipher list derivation failed"); + return WOLFSSL_FAILURE; + } + if (buf[0] == '\0') { + WOLFSSL_MSG("granular policy: derived cipher list is empty -- " + "policy enables no suites this build can serve"); + return WOLFSSL_FAILURE; + } + rc = wolfSSL_CTX_set_cipher_list(ctx, buf); + if (rc != WOLFSSL_SUCCESS) { + WOLFSSL_MSG_EX("granular policy: set_cipher_list failed: %d", rc); + return rc; + } + + /* 3. Supported groups (TLS named groups). */ + for (i = 0; i < p->groups.count; i++) { + int g = wcp_lookup_int(wcp_group_map, p->groups.tok[i]); + if (g < 0) { + WOLFSSL_MSG_EX("granular policy: group not in wolfSSL map: %s", + p->groups.tok[i]); + continue; + } + rc = wolfSSL_CTX_UseSupportedCurve(ctx, (word16)g); + if (rc != WOLFSSL_SUCCESS) { + WOLFSSL_MSG_EX("granular policy: UseSupportedCurve(%s=%d) " + "failed: %d", p->groups.tok[i], g, rc); + /* Non-fatal: a group not supported by this build should not + * tear down the entire policy. */ + } + } + + /* 4. Signature algorithms. Best-effort: if wolfSSL rejects the + * derived list (for instance because the build lacks rsa_pss + * support), keep the policy applied without sigalg pinning rather + * than tearing the CTX down. The cipher list and key-size floors + * already enforce the essential security level. */ + rc = wolfSSL_crypto_policy_derive_sigalgs_list(p, buf, sizeof(buf)); + if (rc != WOLF_CP_OK) { + WOLFSSL_MSG("granular policy: sigalgs list derivation failed"); + } + else if (buf[0] != '\0') { + rc = wolfSSL_CTX_set1_sigalgs_list(ctx, buf); + if (rc != WOLFSSL_SUCCESS) { + WOLFSSL_MSG_EX("granular policy: set1_sigalgs_list rejected by " + "build: %d (continuing)", rc); + } + } + + /* 5. Asymmetric key-size floors. */ +#if !defined(NO_RSA) + if (p->min_rsa_bits > 0) { + rc = wolfSSL_CTX_SetMinRsaKey_Sz(ctx, (short)p->min_rsa_bits); + if (rc != WOLFSSL_SUCCESS) { + WOLFSSL_MSG_EX("granular policy: SetMinRsaKey_Sz(%ld) failed: %d", + p->min_rsa_bits, rc); + return rc; + } + } +#endif +#if !defined(NO_DH) + if (p->min_dh_bits > 0) { + rc = wolfSSL_CTX_SetMinDhKey_Sz(ctx, (word16)p->min_dh_bits); + if (rc != WOLFSSL_SUCCESS) { + WOLFSSL_MSG_EX("granular policy: SetMinDhKey_Sz(%ld) failed: %d", + p->min_dh_bits, rc); + return rc; + } + } +#endif +#ifdef HAVE_ECC + { + /* Map RSA-equivalent strength to ECC bits: 2048->224, 3072->256, + * 4096->384, 7680->384, 15360->521. Conservative. */ + short ecc_bits = 0; + if (p->min_rsa_bits >= 15360) ecc_bits = 521; + else if (p->min_rsa_bits >= 7680) ecc_bits = 384; + else if (p->min_rsa_bits >= 3072) ecc_bits = 256; + else if (p->min_rsa_bits >= 2048) ecc_bits = 224; + if (ecc_bits > 0) { + rc = wolfSSL_CTX_SetMinEccKey_Sz(ctx, ecc_bits); + if (rc != WOLFSSL_SUCCESS) { + WOLFSSL_MSG_EX("granular policy: SetMinEccKey_Sz(%d) failed: " + "%d", ecc_bits, rc); + return rc; + } + } + } +#endif + + return WOLFSSL_SUCCESS; +} + +#endif /* WOLFSSL_SYS_CRYPTO_POLICY */ diff --git a/src/crypto_policy_granular.h b/src/crypto_policy_granular.h new file mode 100644 index 00000000000..1061b8e1afb --- /dev/null +++ b/src/crypto_policy_granular.h @@ -0,0 +1,85 @@ +/* crypto_policy_granular.h + * + * Internal header for the granular allowlist crypto-policy back-end. + * Not part of the wolfSSL public API. See src/crypto_policy_granular.c. + */ +#ifndef WOLFSSL_CRYPTO_POLICY_GRANULAR_H +#define WOLFSSL_CRYPTO_POLICY_GRANULAR_H + +#include + +#if defined(WOLFSSL_SYS_CRYPTO_POLICY) + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define WOLF_CP_MAX_TOKENS 64 +#define WOLF_CP_MAX_TOKEN_LEN 48 +#define WOLF_CP_MAX_LINE 256 + +#define WOLF_CP_OK 0 +#define WOLF_CP_ERR_SYNTAX -1 +#define WOLF_CP_ERR_NOT_ALLOWLIST -2 +#define WOLF_CP_ERR_OVERFLOW -3 +#define WOLF_CP_ERR_EMPTY -4 + +typedef struct { + char tok[WOLF_CP_MAX_TOKENS][WOLF_CP_MAX_TOKEN_LEN]; + int count; +} WolfCPList; + +typedef struct { + int version; + int allowlist; + WolfCPList protocols; + WolfCPList ciphers; + WolfCPList kx; + WolfCPList macs; + WolfCPList hashes; + WolfCPList groups; + WolfCPList sigs; + long min_rsa_bits; + long min_dh_bits; + long min_dsa_bits; + int security_level; +} WolfGranularPolicy; + +/* Header sniff: 1 if buffer looks like a granular allowlist file, + * 0 if legacy single-line @SECLEVEL= format. */ +WOLFSSL_LOCAL int wolfSSL_crypto_policy_is_granular(const char *buf); + +/* Parse a granular allowlist buffer into a WolfGranularPolicy. */ +WOLFSSL_LOCAL int wolfSSL_crypto_policy_parse_granular( + const char *buf, WolfGranularPolicy *out, char *err, size_t errlen); + +/* Derive a wolfSSL-style cipher list string from the parsed policy. */ +WOLFSSL_LOCAL int wolfSSL_crypto_policy_derive_cipher_list( + const WolfGranularPolicy *p, char *out, size_t outlen); + +/* Derive a wolfSSL sigalgs list string from the parsed policy. */ +WOLFSSL_LOCAL int wolfSSL_crypto_policy_derive_sigalgs_list( + const WolfGranularPolicy *p, char *out, size_t outlen); + +/* Lowest enabled version inside the requested protocol family. + * is_dtls != 0 considers only DTLS tokens, is_dtls == 0 considers + * only TLS tokens. Returns -1 if no token of that family is enabled. */ +WOLFSSL_LOCAL int wolfSSL_crypto_policy_min_version( + const WolfGranularPolicy *p, int is_dtls); + +/* Apply the parsed policy to a CTX: drive SetMinVersion, + * set_cipher_list, UseSupportedCurve, set1_sigalgs_list and + * SetMin{Rsa,Dh,Ecc}Key_Sz. */ +WOLFSSL_LOCAL int wolfSSL_crypto_policy_apply_granular( + WOLFSSL_CTX *ctx, const WolfGranularPolicy *p); + +#ifdef __cplusplus +} +#endif + +#endif /* WOLFSSL_SYS_CRYPTO_POLICY */ + +#endif /* WOLFSSL_CRYPTO_POLICY_GRANULAR_H */ diff --git a/src/include.am b/src/include.am index 563a6fa3e8d..cc92fbac547 100644 --- a/src/include.am +++ b/src/include.am @@ -2004,8 +2004,11 @@ src_libwolfssl@LIBSUFFIX@_la_SOURCES += \ src/wolfio.c \ src/keys.c \ src/ssl.c \ + src/crypto_policy_granular.c \ src/tls.c +EXTRA_DIST += src/crypto_policy_granular.h + if BUILD_TLS13 src_libwolfssl@LIBSUFFIX@_la_SOURCES += src/tls13.c endif diff --git a/src/ssl.c b/src/ssl.c index 416f9a2f220..cac6507881f 100644 --- a/src/ssl.c +++ b/src/ssl.c @@ -319,9 +319,19 @@ int wc_OBJ_sn2nid(const char *sn) #if defined(WOLFSSL_SYS_CRYPTO_POLICY) +#include "crypto_policy_granular.h" /* The system wide crypto-policy. Configured by wolfSSL_crypto_policy_enable. * */ static struct SystemCryptoPolicy crypto_policy; +/* Optional granular (allowlist) policy. Activated when the file uses the + * sectioned crypto-policies vocabulary instead of the legacy + * `@SECLEVEL=N:...` cipher-string. */ +static WolfGranularPolicy crypto_policy_gran; +static int crypto_policy_gran_enabled = 0; +/* Internal flag: while the granular applier is driving the CTX, the + * per-setter guards (SetMinVersion, SetMinRsaKey_Sz, …) must let our + * calls through; otherwise the policy could never install itself. */ +static int crypto_policy_applying = 0; #endif /* WOLFSSL_SYS_CRYPTO_POLICY */ #if !defined(NO_RSA) || !defined(NO_DH) || defined(HAVE_ECC) || \ @@ -620,22 +630,40 @@ WOLFSSL_CTX* wolfSSL_CTX_new_ex(WOLFSSL_METHOD* method, void* heap) #if defined(WOLFSSL_SYS_CRYPTO_POLICY) /* Load the crypto-policy ciphers if configured. */ if (ctx && wolfSSL_crypto_policy_is_enabled()) { - const char * list = wolfSSL_crypto_policy_get_ciphers(); - int ret = 0; - - if (list != NULL && *list != '\0') { - if (AllocateCtxSuites(ctx) != 0) { - WOLFSSL_MSG("allocate ctx suites failed"); + if (crypto_policy_gran_enabled) { + /* Granular allowlist: drive the wolfSSL public API directly. + * The per-setter policy guards are temporarily disarmed so + * our own apply step can install the policy values. */ + int ret; + crypto_policy_applying = 1; + ret = wolfSSL_crypto_policy_apply_granular( + ctx, &crypto_policy_gran); + crypto_policy_applying = 0; + if (ret != WOLFSSL_SUCCESS) { + WOLFSSL_MSG("granular crypto policy apply failed"); wolfSSL_CTX_free(ctx); ctx = NULL; } - else { - ret = wolfSSL_parse_cipher_list(ctx, NULL, ctx->suites, list); - if (ret != WOLFSSL_SUCCESS) { - WOLFSSL_MSG("parse cipher list failed"); + } + else { + const char * list = wolfSSL_crypto_policy_get_ciphers(); + int ret = 0; + + if (list != NULL && *list != '\0') { + if (AllocateCtxSuites(ctx) != 0) { + WOLFSSL_MSG("allocate ctx suites failed"); wolfSSL_CTX_free(ctx); ctx = NULL; } + else { + ret = wolfSSL_parse_cipher_list(ctx, NULL, ctx->suites, + list); + if (ret != WOLFSSL_SUCCESS) { + WOLFSSL_MSG("parse cipher list failed"); + wolfSSL_CTX_free(ctx); + ctx = NULL; + } + } } } } @@ -4898,7 +4926,7 @@ int wolfSSL_CTX_SetMinVersion(WOLFSSL_CTX* ctx, int version) } #if defined(WOLFSSL_SYS_CRYPTO_POLICY) - if (crypto_policy.enabled) { + if (crypto_policy.enabled && !crypto_policy_applying) { return CRYPTO_POLICY_FORBIDDEN; } #endif /* WOLFSSL_SYS_CRYPTO_POLICY */ @@ -5345,6 +5373,7 @@ int wolfSSL_crypto_policy_enable(const char * policy_file) XFILE file; long sz = 0; size_t n_read = 0; + char * gran_buf = NULL; WOLFSSL_ENTER("wolfSSL_crypto_policy_enable"); @@ -5365,6 +5394,8 @@ int wolfSSL_crypto_policy_enable(const char * policy_file) } XMEMSET(&crypto_policy, 0, sizeof(crypto_policy)); + XMEMSET(&crypto_policy_gran, 0, sizeof(crypto_policy_gran)); + crypto_policy_gran_enabled = 0; file = XFOPEN(policy_file, "rb"); @@ -5390,23 +5421,84 @@ int wolfSSL_crypto_policy_enable(const char * policy_file) return WOLFSSL_BAD_FILE; } - if (sz <= 0 || sz > MAX_WOLFSSL_CRYPTO_POLICY_SIZE) { + /* Granular allowlist files can exceed MAX_WOLFSSL_CRYPTO_POLICY_SIZE + * (the legacy single-line cap). Allocate a heap buffer for the sniff + * pass; we fall back to the legacy in-place buffer when the file + * fits and turns out to be legacy format. */ + if (sz <= 0 || sz > (long)(1L << 20)) { WOLFSSL_MSG_EX("error: crypto policy file %s, invalid size: %ld", policy_file, sz); XFCLOSE(file); return WOLFSSL_BAD_FILE; } - n_read = XFREAD(crypto_policy.str, 1, sz, file); + gran_buf = (char *)XMALLOC((size_t)sz + 1, NULL, + DYNAMIC_TYPE_TMP_BUFFER); + if (gran_buf == NULL) { + XFCLOSE(file); + WOLFSSL_MSG("error: crypto policy: out of memory"); + return MEMORY_E; + } + + n_read = XFREAD(gran_buf, 1, (size_t)sz, file); XFCLOSE(file); if (n_read != (size_t) sz) { WOLFSSL_MSG_EX("error: crypto policy file %s: read %zu, " "expected %ld", policy_file, n_read, sz); + XFREE(gran_buf, NULL, DYNAMIC_TYPE_TMP_BUFFER); return WOLFSSL_BAD_FILE; } + gran_buf[sz] = '\0'; + + /* Route on header sniff. */ + if (wolfSSL_crypto_policy_is_granular(gran_buf)) { + char err[128]; + int rc = wolfSSL_crypto_policy_parse_granular( + gran_buf, &crypto_policy_gran, err, sizeof(err)); + XFREE(gran_buf, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (rc != WOLF_CP_OK) { + WOLFSSL_MSG_EX("granular crypto policy parse failed: %s", err); + XMEMSET(&crypto_policy_gran, 0, sizeof(crypto_policy_gran)); + return WOLFSSL_BAD_FILE; + } + /* Mirror the coarse level into the legacy struct so that + * wolfSSL_crypto_policy_init_ctx() and security_level getters + * continue to behave. */ + crypto_policy.secLevel = crypto_policy_gran.security_level > 0 + ? crypto_policy_gran.security_level + : 0; + /* Mirror the derived cipher list into the legacy str buffer so + * wolfSSL_crypto_policy_get_ciphers() returns the actual list + * the policy enables (and not the empty string while + * `enabled == 1`). The cipher set is the same for the TLS and + * DTLS protocol families at a given minor version, so a single + * derivation feeds both CTX families. Truncation is safe: the + * string is informational, the authoritative apply happens per + * CTX in wolfSSL_crypto_policy_apply_granular(). */ + rc = wolfSSL_crypto_policy_derive_cipher_list( + &crypto_policy_gran, + crypto_policy.str, + sizeof(crypto_policy.str)); + if (rc != WOLF_CP_OK) { + crypto_policy.str[0] = '\0'; + } + crypto_policy.enabled = 1; + crypto_policy_gran_enabled = 1; + return WOLFSSL_SUCCESS; + } - crypto_policy.str[n_read] = '\0'; + /* Legacy single-line @SECLEVEL=... format. Honour the legacy size + * ceiling for those files only. */ + if (sz > MAX_WOLFSSL_CRYPTO_POLICY_SIZE) { + XFREE(gran_buf, NULL, DYNAMIC_TYPE_TMP_BUFFER); + WOLFSSL_MSG_EX("error: legacy crypto policy file %s, too large: %ld", + policy_file, sz); + return WOLFSSL_BAD_FILE; + } + XMEMCPY(crypto_policy.str, gran_buf, (size_t)sz); + crypto_policy.str[sz] = '\0'; + XFREE(gran_buf, NULL, DYNAMIC_TYPE_TMP_BUFFER); return crypto_policy_parse(); } @@ -5436,11 +5528,39 @@ int wolfSSL_crypto_policy_enable_buffer(const char * buf) sz = XSTRLEN(buf); + XMEMSET(&crypto_policy, 0, sizeof(crypto_policy)); + XMEMSET(&crypto_policy_gran, 0, sizeof(crypto_policy_gran)); + crypto_policy_gran_enabled = 0; + + if (wolfSSL_crypto_policy_is_granular(buf)) { + char err[128]; + int rc = wolfSSL_crypto_policy_parse_granular( + buf, &crypto_policy_gran, err, sizeof(err)); + if (rc != WOLF_CP_OK) { + WOLFSSL_MSG_EX("granular crypto policy parse failed: %s", err); + XMEMSET(&crypto_policy_gran, 0, sizeof(crypto_policy_gran)); + return BAD_FUNC_ARG; + } + crypto_policy.secLevel = crypto_policy_gran.security_level > 0 + ? crypto_policy_gran.security_level + : 0; + /* Mirror the derived cipher list — see comment in + * wolfSSL_crypto_policy_enable() above. */ + rc = wolfSSL_crypto_policy_derive_cipher_list( + &crypto_policy_gran, + crypto_policy.str, + sizeof(crypto_policy.str)); + if (rc != WOLF_CP_OK) { + crypto_policy.str[0] = '\0'; + } + crypto_policy.enabled = 1; + crypto_policy_gran_enabled = 1; + return WOLFSSL_SUCCESS; + } + if (sz == 0 || sz > MAX_WOLFSSL_CRYPTO_POLICY_SIZE) { return BAD_FUNC_ARG; } - - XMEMSET(&crypto_policy, 0, sizeof(crypto_policy)); XMEMCPY(crypto_policy.str, buf, sz); return crypto_policy_parse(); @@ -5468,6 +5588,8 @@ void wolfSSL_crypto_policy_disable(void) WOLFSSL_ENTER("wolfSSL_crypto_policy_disable"); crypto_policy.enabled = 0; XMEMSET(&crypto_policy, 0, sizeof(crypto_policy)); + crypto_policy_gran_enabled = 0; + XMEMSET(&crypto_policy_gran, 0, sizeof(crypto_policy_gran)); return; } diff --git a/tests/api.c b/tests/api.c index db7f271815e..bd15c71b532 100644 --- a/tests/api.c +++ b/tests/api.c @@ -30040,6 +30040,224 @@ static int test_wolfSSL_crypto_policy_ciphers(void) return EXPECT_RESULT(); } +/* Helper: count how many ciphers are exposed by `ssl`. */ +#if defined(WOLFSSL_SYS_CRYPTO_POLICY) && !defined(NO_TLS) +static int crypto_policy_cipher_count(const WOLFSSL * ssl) +{ + WOLF_STACK_OF(WOLFSSL_CIPHER) * sk = NULL; + + if (ssl == NULL) { + return -1; + } + + sk = wolfSSL_get_ciphers_compat(ssl); + + if (sk == NULL) { + return 0; + } + + return wolfSSL_sk_SSL_CIPHER_num(sk); +} +#endif + +/* System wide crypto-policy test: granular allowlist mode. + * + * Drives the new granular code path: wolfSSL_crypto_policy_enable() must + * detect the allowlist header, parse the sectioned file, and the CTX + * created afterwards must reflect the policy's primitives -- cipher + * count, suite membership, security level, and DTLS support. + * */ +static int test_wolfSSL_crypto_policy_granular(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_SYS_CRYPTO_POLICY) && !defined(NO_TLS) + int rc = WC_NO_ERR_TRACE(WOLFSSL_FAILURE); + const char * policy_list[] = { + "examples/crypto_policies/legacy/wolfssl-allowlist.txt", + "examples/crypto_policies/default/wolfssl-allowlist.txt", + "examples/crypto_policies/future/wolfssl-allowlist.txt", + }; + int seclevel_list[] = { 1, 2, 3 }; + int i = 0; + int legacy_suites = 0; + int future_suites = 0; + + for (i = 0; i < 3; ++i) { + WOLFSSL_CTX * ctx = NULL; + WOLFSSL * ssl = NULL; + int is_future = (XSTRSTR(policy_list[i], "future") != NULL); + int is_legacy = (XSTRSTR(policy_list[i], "legacy") != NULL); + int found_tls13 = 0; + int found_aes128 = 0; + int n_suites = 0; + + rc = wolfSSL_crypto_policy_enable(policy_list[i]); + ExpectIntEQ(rc, WOLFSSL_SUCCESS); + + rc = wolfSSL_crypto_policy_is_enabled(); + ExpectIntEQ(rc, 1); + + rc = wolfSSL_crypto_policy_get_level(); + ExpectIntEQ(rc, seclevel_list[i]); + + ctx = wolfSSL_CTX_new(TLS_method()); + ExpectNotNull(ctx); + + ssl = SSL_new(ctx); + ExpectNotNull(ssl); + + n_suites = crypto_policy_cipher_count(ssl); + ExpectIntGT(n_suites, 0); + + /* Every fixture enables at least one TLS 1.3 AES-GCM suite, so + * the resolved cipher list is never empty. Different wolfSSL + * builds use different suite-name conventions ("AES_256" vs + * "AES256" vs "TLS13-AES256-GCM-SHA384"), so we accept any of + * the three. */ + if (crypto_policy_cipher_found(ssl, "AES_256", 0) == 1 + || crypto_policy_cipher_found(ssl, "AES256", 0) == 1 + || crypto_policy_cipher_found(ssl, "TLS13", 0) == 1) { + found_tls13 = 1; + } + ExpectIntEQ(found_tls13, 1); + + /* AES-128 cipher (any name form) must be absent from FUTURE and + * present in LEGACY/DEFAULT (the FUTURE fixture enables only + * AES-256-GCM, AES-256-CCM and CHACHA20-POLY1305). */ + if (crypto_policy_cipher_found(ssl, "AES_128", 0) == 1 + || crypto_policy_cipher_found(ssl, "AES128", 0) == 1) { + found_aes128 = 1; + } + ExpectIntEQ(found_aes128, !is_future); + + if (is_legacy) legacy_suites = n_suites; + if (is_future) future_suites = n_suites; + + if (ssl != NULL) { + SSL_free(ssl); + ssl = NULL; + } + if (ctx != NULL) { + wolfSSL_CTX_free(ctx); + ctx = NULL; + } + + wolfSSL_crypto_policy_disable(); + } + + /* Monotonicity: stricter policies must derive fewer (or equal) TLS + * cipher suites. FUTURE excludes AES-128, so its suite count must + * be strictly less than LEGACY's. */ + ExpectIntGT(legacy_suites, future_suites); + + /* DTLS-only fixture: the parser/applier must accept a policy whose + * sole protocol is DTLS 1.2 and produce a usable CTX whose cipher + * list reflects the policy. A previous version of the granular + * back-end only matched the exact "TLS1.x" token against suites, + * so a DTLS-only policy derived an empty cipher list and the + * applier silently kept the CTX's default suites -- i.e. the + * allowlist did not constrain anything. + * + * We assert: + * (a) get_ciphers() returns a non-empty derived list, proving + * DTLS1.2 protocol tokens enable their TLS1.2 cipher peers + * in the derivation (fix for the DTLS suite-match gap); + * (b) the DTLS CTX is created, proving apply_granular ran to + * success -- empty derive now returns WOLFSSL_FAILURE and + * the CTX would be torn down before reaching this point; + * (c) the derived list round-trips through set_cipher_list on + * a fresh DTLS CTX (the CTX-level cipher store accepts the + * suite names the granular derivation produced). + * + * We deliberately do NOT assert on wolfSSL_get_ciphers_compat() + * for a DTLS SSL here: its sslCipherMinMaxCheck() compares the + * TLS minor of each suite (e.g. TLSv1_2_MINOR = 3) against the + * SSL's options.minDowngrade, which on a DTLS CTX holds a DTLS + * minor (e.g. DTLSv1_2_MINOR = 0xfd). The comparison is a + * pre-existing wolfSSL TLS-vs-DTLS minor quirk, unrelated to + * the granular back-end, so we leave it alone. */ +#ifdef WOLFSSL_DTLS + { + WOLFSSL_CTX * dtls_ctx = NULL; + WOLFSSL_CTX * dtls_ctx_rt = NULL; + const char * gran_str = NULL; + + rc = wolfSSL_crypto_policy_enable( + "examples/crypto_policies/default/wolfssl-allowlist-dtls.txt"); + ExpectIntEQ(rc, WOLFSSL_SUCCESS); + + rc = wolfSSL_crypto_policy_is_enabled(); + ExpectIntEQ(rc, 1); + + /* (a) Derived cipher list mirrored to the legacy str buffer. */ + gran_str = wolfSSL_crypto_policy_get_ciphers(); + ExpectNotNull(gran_str); + if (gran_str != NULL) { + ExpectIntGT((int)XSTRLEN(gran_str), 0); + } + + /* (b) DTLS CTX survives apply_granular. */ + dtls_ctx = wolfSSL_CTX_new(wolfDTLS_method()); + ExpectNotNull(dtls_ctx); + if (dtls_ctx != NULL) { + wolfSSL_CTX_free(dtls_ctx); + } + + /* (c) Round-trip: the derived list is a string set_cipher_list + * accepts on a DTLS CTX. We disable the policy first so the + * CTX is built without the apply step, then drive the API + * directly with the previously-captured string. */ + if (gran_str != NULL && gran_str[0] != '\0') { + char gran_copy[MAX_WOLFSSL_CRYPTO_POLICY_SIZE + 1]; + XSTRNCPY(gran_copy, gran_str, sizeof(gran_copy) - 1); + gran_copy[sizeof(gran_copy) - 1] = '\0'; + + wolfSSL_crypto_policy_disable(); + + dtls_ctx_rt = wolfSSL_CTX_new(wolfDTLS_method()); + ExpectNotNull(dtls_ctx_rt); + if (dtls_ctx_rt != NULL) { + ExpectIntEQ(wolfSSL_CTX_set_cipher_list(dtls_ctx_rt, + gran_copy), + WOLFSSL_SUCCESS); + wolfSSL_CTX_free(dtls_ctx_rt); + } + } + wolfSSL_crypto_policy_disable(); + } +#endif /* WOLFSSL_DTLS */ + + /* Forward-compat guard: a `version = 2` file must be rejected + * outright rather than parsed under our v1 semantics. */ + { + const char * v2_buf = + "version = 2\n" + "override-mode = allowlist\n" + "[protocols]\n" + "enabled-version = TLS1.3\n"; + rc = wolfSSL_crypto_policy_enable_buffer(v2_buf); + ExpectIntLT(rc, 0); + + rc = wolfSSL_crypto_policy_is_enabled(); + ExpectIntEQ(rc, 0); + } + + /* Override-mode != allowlist must be rejected. */ + { + const char * bad_buf = + "version = 1\n" + "override-mode = blocklist\n" + "[protocols]\n" + "enabled-version = TLS1.3\n"; + rc = wolfSSL_crypto_policy_enable_buffer(bad_buf); + ExpectIntLT(rc, 0); + } + + wolfSSL_crypto_policy_disable(); +#endif /* WOLFSSL_SYS_CRYPTO_POLICY && !NO_TLS */ + return EXPECT_RESULT(); +} + static int test_wolfSSL_SSL_in_init(void) { EXPECT_DECLS; @@ -40564,6 +40782,7 @@ TEST_CASE testCases[] = { TEST_DECL(test_wolfSSL_crypto_policy_certs_and_keys), TEST_DECL(test_wolfSSL_crypto_policy_tls_methods), TEST_DECL(test_wolfSSL_crypto_policy_ciphers), + TEST_DECL(test_wolfSSL_crypto_policy_granular), TEST_DECL(test_wolfSSL_SSL_in_init), TEST_DECL(test_wolfSSL_CTX_set_timeout), TEST_DECL(test_wolfSSL_set_psk_use_session_callback),