From c4e13ebadef845d954a4ff2dca397463f0b2f31f Mon Sep 17 00:00:00 2001 From: max-qlab Date: Wed, 27 May 2026 11:38:28 -0400 Subject: [PATCH 1/5] crypto-policies: add granular allowlist parser module Introduces src/crypto_policy_granular.{c,h}, a self-contained parser for the sectioned crypto-policies allowlist format. The vocabulary is the one Fedora crypto-policies emits (the same shape the GnuTLS back-end already uses, endorsed by @asosedkin in fedora-crypto-policies #60 as the granular alternative to the single-line @SECLEVEL=N:... cipher string the wolfSSL back-end was rejected for in wolfSSL #8205). The module is split in three responsibilities: 1. Header sniff (wolfSSL_crypto_policy_is_granular): looks at the first non-blank, non-comment line. `version = `, `override-mode = `, or a `[section]` header => granular file. Anything else => legacy. 2. Parser (wolfSSL_crypto_policy_parse_granular): fills a WolfGranularPolicy struct from a NUL-terminated buffer. Bounded: 64 tokens per category, 48 bytes per token, 256-byte line length, 1 MiB file ceiling. Strict on file format version (only version=1 accepted); forward-tolerant on unknown vocabulary tokens. 3. Apply (wolfSSL_crypto_policy_apply_granular): drives the wolfSSL public API on a WOLFSSL_CTX from the parsed struct. Five sites, in order: - wolfSSL_CTX_SetMinVersion - wolfSSL_CTX_set_cipher_list (cipher x kx x mac x version cross-product against the known TLS suite table) - wolfSSL_CTX_UseSupportedCurve (per enabled-group) - wolfSSL_CTX_set1_sigalgs_list - wolfSSL_CTX_SetMin{Rsa,Dh,Ecc}Key_Sz SetMinVersion and set1_sigalgs_list are best-effort: a build that lacks TLS 1.0 support or rejects an rsa_pss sigalg the policy lists logs a warning and the remaining steps still enforce the policy. Cipher list and key-size floors are authoritative. Standalone helpers (derive_cipher_list, derive_sigalgs_list, min_version) are exposed WOLFSSL_LOCAL so the existing ssl.c parser can be wired to them in the next commit without source-level copy-paste. src/include.am is updated to compile the new file under the existing !BUILD_CRYPTONLY guard; the file's own contents are gated by WOLFSSL_SYS_CRYPTO_POLICY so the build is a no-op when that flag is not defined. Mapping tables (versions, groups, sig schemes, suite table) follow the OpenSSL/GnuTLS back-end naming for primitives; the cross-product is restricted to the Fedora-relevant TLS suites and can be extended incrementally as further policies enable additional primitives. Co-Authored-By: Dominik Blain --- src/crypto_policy_granular.c | 639 +++++++++++++++++++++++++++++++++++ src/crypto_policy_granular.h | 83 +++++ src/include.am | 3 + 3 files changed, 725 insertions(+) create mode 100644 src/crypto_policy_granular.c create mode 100644 src/crypto_policy_granular.h diff --git a/src/crypto_policy_granular.c b/src/crypto_policy_granular.c new file mode 100644 index 0000000000..32a2a61cf6 --- /dev/null +++ b/src/crypto_policy_granular.c @@ -0,0 +1,639 @@ +/* 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 ∩ 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. + */ + +#ifdef HAVE_CONFIG_H + #include +#endif + +#include + +#if defined(WOLFSSL_SYS_CRYPTO_POLICY) + +#include +#include +#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 */ +/* -------------------------------------------------------------------- */ + +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_has(&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 */ +/* -------------------------------------------------------------------- */ + +int wolfSSL_crypto_policy_min_version(const WolfGranularPolicy *p) +{ + int i; + int best = -1; + int best_pri = 1 << 30; + + if (p == NULL) return -1; + + for (i = 0; i < p->protocols.count; i++) { + int v = wcp_lookup_int(wcp_version_map, p->protocols.tok[i]); + int pri; + if (v < 0) continue; + /* Pin "min" to numerically lowest (TLSV1=1, TLSV1_3=4). */ + pri = v; + if (pri < best_pri) { + best_pri = pri; + 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; + + if (ctx == NULL || p == NULL) { + return BAD_FUNC_ARG; + } + + WOLFSSL_ENTER("wolfSSL_crypto_policy_apply_granular"); + + /* 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. */ + min_ver = wolfSSL_crypto_policy_min_version(p); + 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. */ + 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') { + 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 0000000000..6b0b628922 --- /dev/null +++ b/src/crypto_policy_granular.h @@ -0,0 +1,83 @@ +/* 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 TLS/DTLS version enabled. Returns -1 if none. */ +WOLFSSL_LOCAL int wolfSSL_crypto_policy_min_version( + const WolfGranularPolicy *p); + +/* 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 563a6fa3e8..cc92fbac54 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 From 5555fcf252fa0d2f84ab740b6a30d57120226379 Mon Sep 17 00:00:00 2001 From: max-qlab Date: Wed, 27 May 2026 11:39:03 -0400 Subject: [PATCH 2/5] crypto-policies: route enable() through granular parser on header sniff src/ssl.c gains four mechanical changes plus one safety guard so the granular allowlist module from the previous commit installs itself on every WOLFSSL_CTX. The legacy `@SECLEVEL=N:...` single-line code path is preserved unchanged for existing deployments. 1. State extension Two file-scope globals next to the existing `static struct SystemCryptoPolicy crypto_policy;`: static WolfGranularPolicy crypto_policy_gran; static int crypto_policy_gran_enabled = 0; static int crypto_policy_applying = 0; `_gran` stores the parsed allowlist; `_gran_enabled` marks which parser owns the active policy; `_applying` is the temporary bypass used during the apply step (see point 5). 2. wolfSSL_crypto_policy_enable(file) The file is read into a heap buffer (legacy 1024-byte ceiling replaced by a 1 MiB ceiling; the legacy size check is re-asserted only on the legacy code path). The header sniff decides: granular header => parse with wolfSSL_crypto_policy_parse_granular() into crypto_policy_gran, mirror security_level into crypto_policy.secLevel (so existing getters keep returning the right number), set enabled = 1 on both, return. Legacy header => existing crypto_policy_parse() path, unchanged. 3. wolfSSL_crypto_policy_enable_buffer(buf) Same dispatch. The legacy MAX_WOLFSSL_CRYPTO_POLICY_SIZE check now only applies to legacy-format buffers. 4. wolfSSL_crypto_policy_disable() Zeroes both legacy and granular state. 5. wolfSSL_CTX_new_ex() The block that loads the crypto-policy cipher list on a freshly allocated CTX now branches on crypto_policy_gran_enabled: - granular: call wolfSSL_crypto_policy_apply_granular(ctx, &gran), wrapped in `crypto_policy_applying = 1; ... ; = 0;` so the per-setter policy guards (point 6) let the applier through. - legacy: unchanged AllocateCtxSuites + wolfSSL_parse_cipher_list. 6. wolfSSL_CTX_SetMinVersion() guard The existing `if (crypto_policy.enabled) return CRYPTO_POLICY_FORBIDDEN` guard at the top of wolfSSL_CTX_SetMinVersion() prevented the granular applier from installing the policy's protocol floor. The guard is widened to `crypto_policy.enabled && !crypto_policy_applying` so application code is still blocked, but our apply step is not. No other setter needs this treatment: the key-size guards already let larger requested floors through (they only reject weakening), and set_cipher_list / UseSupportedCurve / set1_sigalgs_list have no policy guard. After this change the build is still cipher-list compatible with the legacy format and `make check` is green (see the test commit). Co-Authored-By: Dominik Blain --- src/ssl.c | 130 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 114 insertions(+), 16 deletions(-) diff --git a/src/ssl.c b/src/ssl.c index 416f9a2f22..6604d95296 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,69 @@ 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; + 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 +5513,30 @@ 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; + 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 +5564,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; } From 0e8ed3c1304eaca8dadbd986419309882ba19aaa Mon Sep 17 00:00:00 2001 From: max-qlab Date: Wed, 27 May 2026 11:39:26 -0400 Subject: [PATCH 3/5] tests: granular crypto-policy fixtures, format spec, end-to-end test Adds end-to-end coverage of the granular allowlist code path introduced in the previous two commits. Test (tests/api.c, test_wolfSSL_crypto_policy_granular): For LEGACY / DEFAULT / FUTURE the test: - enables the policy via wolfSSL_crypto_policy_enable(file), - asserts wolfSSL_crypto_policy_is_enabled() == 1, - asserts wolfSSL_crypto_policy_get_level() returns the policy's security-level (1 / 2 / 3), - creates a WOLFSSL_CTX (must not be NULL, which would mean the applier tore it down), - counts the resolved cipher suites (must be > 0), - asserts presence of TLS 1.3 / AES-256 family suites, - asserts AES-128 is absent from FUTURE and present elsewhere. Then two cross-policy invariants: - suite-count monotonicity: LEGACY > FUTURE (FUTURE excludes AES-128 so it must derive strictly fewer suites), - a DTLS-only fixture must yield a usable DTLS CTX (guarded by WOLFSSL_DTLS). Then two format-validation cases via _enable_buffer: - `version = 2` must be rejected outright, - `override-mode = blocklist` must be rejected. Fixtures (examples/crypto_policies/{legacy,default,future}/ wolfssl-allowlist.txt): These are unmodified outputs of the Fedora crypto-policies generator (the WolfSSLGenerator class). Checked in so the test exercises the parser against the same bytes a Fedora install would produce, not a hand-rolled approximation. default/wolfssl-allowlist-dtls.txt is hand-crafted (the DTLS-only slice of DEFAULT) since the upstream generator does not currently emit a pure-DTLS file. Format spec (examples/crypto_policies/README.md): Documents the allowlist syntax, the relationship with the legacy format, the five API sites the apply step drives, the forward-compat rules (version=1 strict, vocabulary tokens tolerant), and the relevant upstream tracking issues (wolfSSL #9802, fedora-crypto-policies #60). `make check` is green on three build configurations: default (--enable-opensslextra --enable-tls13 --enable-arc4), production no-debug, and --disable-dtls (the DTLS section of the test auto-skips via #ifdef WOLFSSL_DTLS). Co-Authored-By: Dominik Blain --- examples/crypto_policies/README.md | 140 ++++++++++++++ .../default/wolfssl-allowlist-dtls.txt | 42 +++++ .../default/wolfssl-allowlist.txt | 113 ++++++++++++ .../future/wolfssl-allowlist.txt | 97 ++++++++++ .../legacy/wolfssl-allowlist.txt | 132 +++++++++++++ tests/api.c | 174 ++++++++++++++++++ 6 files changed, 698 insertions(+) create mode 100644 examples/crypto_policies/README.md create mode 100644 examples/crypto_policies/default/wolfssl-allowlist-dtls.txt create mode 100644 examples/crypto_policies/default/wolfssl-allowlist.txt create mode 100644 examples/crypto_policies/future/wolfssl-allowlist.txt create mode 100644 examples/crypto_policies/legacy/wolfssl-allowlist.txt diff --git a/examples/crypto_policies/README.md b/examples/crypto_policies/README.md new file mode 100644 index 0000000000..3f6deb9f74 --- /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 0000000000..e9f89966e7 --- /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 0000000000..c653555239 --- /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 0000000000..c46f21bda8 --- /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 0000000000..336fa2a75f --- /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/tests/api.c b/tests/api.c index db7f271815..a4fcd931e7 100644 --- a/tests/api.c +++ b/tests/api.c @@ -30040,6 +30040,179 @@ 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; + WOLFSSL_CIPHER * current = NULL; + int i = 0; + + if (ssl == NULL) { + return -1; + } + + sk = wolfSSL_get_ciphers_compat(ssl); + + if (sk == NULL) { + return 0; + } + + do { + current = wolfSSL_sk_SSL_CIPHER_value(sk, i); + if (current) { + i++; + } + } while (current); + + return i; +} +#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. */ +#ifdef WOLFSSL_DTLS + { + WOLFSSL_CTX * dtls_ctx = 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); + + dtls_ctx = wolfSSL_CTX_new(wolfDTLS_method()); + ExpectNotNull(dtls_ctx); + + if (dtls_ctx != NULL) { + wolfSSL_CTX_free(dtls_ctx); + } + 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 +40737,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), From 0b95179dac133096053cc1a677e463c9931ab487 Mon Sep 17 00:00:00 2001 From: max-qlab Date: Wed, 27 May 2026 16:27:20 -0400 Subject: [PATCH 4/5] crypto-policies: address granular allowlist review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six fixes for the granular allowlist back-end, in response to the GitHub Copilot pull-request review and the os-check.yml CI matrix failure on the initial PR (#10541). 1. src/crypto_policy_granular.c top includes. The file was using the standalone `#include ` + `#include ` pattern, which trips the include-order assertion in settings.h whenever the build defines TEST_LIBWOLFSSL_SOURCES_INCLUSION_SEQUENCE — that is, on the upstream `make check linux` matrix (75 jobs failed, first observed on CPPFLAGS=-DNO_VERIFY_OID). When the module's content is gated behind WOLFSSL_SYS_CRYPTO_POLICY and the macro is undefined, the same configuration also hit ISO C's empty-translation-unit rule with -Werror=pedantic. Switch to `#include ` (the canonical pattern used by src/ssl.c, src/internal.c, src/tls.c) which both establishes the correct include order and pulls in types.h / error-crypt.h / logging.h so the TU is never empty. The downstream includes of logging.h and types.h become redundant and are removed. 2. DTLS-only allowlist no longer derives an empty cipher list (Copilot review, src/crypto_policy_granular.c:432). The previous derivation matched a suite's `version` field (e.g. "TLS1.2") byte-for-byte against the policy's enabled protocol tokens. A DTLS-only file (enabled-version = DTLS1.2) therefore produced zero cipher rows even though the IANA suite identifiers used by TLS 1.x and DTLS 1.x at the same minor version are identical. The new helper `wcp_protocol_family_enabled()` treats "TLS1.x" and "DTLS1.x" as suite-equivalent at the same minor version, in both directions. 3. min_version is now scoped to the CTX's protocol family (Copilot review, src/crypto_policy_granular.c:545). `wolfSSL_crypto_policy_min_version()` used to pick the numerically smallest enabled version across both families. On a DTLS CTX, that would feed a TLS constant (e.g. WOLFSSL_TLSV1_2 = 3) into `wolfSSL_CTX_SetMinVersion()`, which rejects it for the DTLS major version and the floor was silently dropped. The function now takes an `is_dtls` argument and only considers tokens of that family; apply_granular() derives `is_dtls` from `ctx->method->version.major == DTLS_MAJOR`. 4. Empty derived cipher list is now a hard failure (Copilot review, src/crypto_policy_granular.c:559). apply_granular() used to skip `wolfSSL_CTX_set_cipher_list()` when the derived list was empty and still return success, leaving the CTX's default cipher list in place — i.e. an allowlist that didn't constrain anything. It now returns WOLFSSL_FAILURE in that case, which tears the CTX down in the WOLFSSL_CTX_new_ex granular-apply branch of src/ssl.c. 5. wolfSSL_crypto_policy_get_ciphers() reflects the granular derivation (Copilot review, src/ssl.c:5473 and :5535). Both enable paths mirror the derived cipher list into `crypto_policy.str` after a successful granular parse, so the public getter no longer returns an empty string while `crypto_policy.enabled == 1`. The cipher set is identical for the TLS and DTLS families at a given minor version, so one derivation feeds both CTX families. Truncation is safe: the string is informational; the authoritative apply happens per CTX. 6. Cipher-count test helper uses the public stack accessor (Copilot review, tests/api.c:30068). `crypto_policy_cipher_count()` is replaced by a single call to `wolfSSL_sk_SSL_CIPHER_num()`. The manual loop relied on out-of-range behavior of `wolfSSL_sk_SSL_CIPHER_value()`. The DTLS section of test_wolfSSL_crypto_policy_granular() is extended to assert (a) the global policy cipher list is non-empty for a DTLS-only fixture (regression guard for fix #2), (b) the DTLS CTX survives apply_granular (regression guard for fixes #3 and #4), (c) the derived list round-trips through `wolfSSL_CTX_set_cipher_list()` on a fresh DTLS CTX. The test explicitly does not assert on `wolfSSL_get_ciphers_compat()` for a DTLS SSL: its `sslCipherMinMaxCheck()` compares TLS minor numbers with DTLS minor numbers, a pre-existing TLS-vs-DTLS quirk unrelated to the granular back-end. Verified against five local build configurations on Ubuntu 24.04 + gcc 13: (1) `CPPFLAGS=-DNO_VERIFY_OID` (the original failing CI config) — clean; (2) `--enable-all --enable-dtls --enable-dtls13 --with-sys-crypto-policy=...` — make check 17/17 pass; (3) default `./configure` — make check 5/5 pass; (4) `--enable-all` + `-pedantic -Werror -fsanitize=undefined -O1` — build clean (a pre-existing UBSan finding in src/ssl_crypto.c DES code is unrelated); (5) `--enable-dtls --enable-dtls13` + DTLS-only policy — our test passes. Co-Authored-By: Dominik Blain --- src/crypto_policy_granular.c | 106 +++++++++++++++++++++++++++-------- src/crypto_policy_granular.h | 6 +- src/ssl.c | 24 ++++++++ tests/api.c | 71 ++++++++++++++++++----- 4 files changed, 168 insertions(+), 39 deletions(-) diff --git a/src/crypto_policy_granular.c b/src/crypto_policy_granular.c index 32a2a61cf6..8e2ab3e25e 100644 --- a/src/crypto_policy_granular.c +++ b/src/crypto_policy_granular.c @@ -33,18 +33,12 @@ * none. */ -#ifdef HAVE_CONFIG_H - #include -#endif - -#include +#include #if defined(WOLFSSL_SYS_CRYPTO_POLICY) #include #include -#include -#include #include #include @@ -409,6 +403,39 @@ static const char *wcp_lookup_str(const struct wcp_kv_str *m, const char *cp) /* 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) { @@ -427,7 +454,7 @@ int wolfSSL_crypto_policy_derive_cipher_list(const WolfGranularPolicy *p, size_t need; if (!wcp_has(&p->ciphers, s->cipher)) continue; - if (!wcp_has(&p->protocols, s->version)) 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; @@ -490,7 +517,14 @@ int wolfSSL_crypto_policy_derive_sigalgs_list(const WolfGranularPolicy *p, /* lowest enabled TLS/DTLS version */ /* -------------------------------------------------------------------- */ -int wolfSSL_crypto_policy_min_version(const WolfGranularPolicy *p) +/* `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; @@ -499,13 +533,22 @@ int wolfSSL_crypto_policy_min_version(const WolfGranularPolicy *p) if (p == NULL) return -1; for (i = 0; i < p->protocols.count; i++) { - int v = wcp_lookup_int(wcp_version_map, p->protocols.tok[i]); - int pri; + 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 (TLSV1=1, TLSV1_3=4). */ - pri = v; - if (pri < best_pri) { - best_pri = pri; + /* 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; } } @@ -523,6 +566,7 @@ int wolfSSL_crypto_policy_apply_granular(WOLFSSL_CTX *ctx, int i; char buf[2048]; int min_ver; + int is_dtls; if (ctx == NULL || p == NULL) { return BAD_FUNC_ARG; @@ -530,12 +574,19 @@ int wolfSSL_crypto_policy_apply_granular(WOLFSSL_CTX *ctx, 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. */ - min_ver = wolfSSL_crypto_policy_min_version(p); + * 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) { @@ -544,18 +595,25 @@ int wolfSSL_crypto_policy_apply_granular(WOLFSSL_CTX *ctx, } } - /* 2. Cipher list. */ + /* 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') { - 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; - } + 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). */ diff --git a/src/crypto_policy_granular.h b/src/crypto_policy_granular.h index 6b0b628922..1061b8e1af 100644 --- a/src/crypto_policy_granular.h +++ b/src/crypto_policy_granular.h @@ -64,9 +64,11 @@ WOLFSSL_LOCAL int wolfSSL_crypto_policy_derive_cipher_list( WOLFSSL_LOCAL int wolfSSL_crypto_policy_derive_sigalgs_list( const WolfGranularPolicy *p, char *out, size_t outlen); -/* Lowest TLS/DTLS version enabled. Returns -1 if none. */ +/* 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); + const WolfGranularPolicy *p, int is_dtls); /* Apply the parsed policy to a CTX: drive SetMinVersion, * set_cipher_list, UseSupportedCurve, set1_sigalgs_list and diff --git a/src/ssl.c b/src/ssl.c index 6604d95296..cac6507881 100644 --- a/src/ssl.c +++ b/src/ssl.c @@ -5468,6 +5468,21 @@ int wolfSSL_crypto_policy_enable(const char * policy_file) 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; @@ -5529,6 +5544,15 @@ int wolfSSL_crypto_policy_enable_buffer(const char * buf) 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; diff --git a/tests/api.c b/tests/api.c index a4fcd931e7..ac7a0ca896 100644 --- a/tests/api.c +++ b/tests/api.c @@ -30045,8 +30045,6 @@ static int test_wolfSSL_crypto_policy_ciphers(void) static int crypto_policy_cipher_count(const WOLFSSL * ssl) { WOLF_STACK_OF(WOLFSSL_CIPHER) * sk = NULL; - WOLFSSL_CIPHER * current = NULL; - int i = 0; if (ssl == NULL) { return -1; @@ -30058,14 +30056,7 @@ static int crypto_policy_cipher_count(const WOLFSSL * ssl) return 0; } - do { - current = wolfSSL_sk_SSL_CIPHER_value(sk, i); - if (current) { - i++; - } - } while (current); - - return i; + return wolfSSL_sk_SSL_CIPHER_num(sk); } #endif @@ -30160,10 +30151,36 @@ static int test_wolfSSL_crypto_policy_granular(void) 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. */ + * 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 = 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"); @@ -30172,12 +30189,40 @@ static int test_wolfSSL_crypto_policy_granular(void) 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 */ From 7d2b407b9c86dc07bf477ef55d93acdc87ca8497 Mon Sep 17 00:00:00 2001 From: max-qlab Date: Thu, 28 May 2026 09:41:38 -0400 Subject: [PATCH 5/5] crypto-policies: keep granular sources ASCII-only Address the check-source-text failure on PR #10541, commit 0b95179: the 8-bit scan in pre-commit/check-source-text.sh flagged ten non-ASCII bytes (em-dash U+2014 and intersection U+2229) that had crept into commentary in src/crypto_policy_granular.c and tests/api.c. Replace each em-dash with " -- " and the lone "intersect" glyph with the word "intersect". Comments-only change; no code, no fixtures, no test logic touched. Diff is 10 insertions / 10 deletions across the two files. Verified post-fix: grep -rP '[^\x00-\x7F]' src/crypto_policy_granular.c tests/api.c -> no matches. Co-Authored-By: Dominik Blain --- src/crypto_policy_granular.c | 14 +++++++------- tests/api.c | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/crypto_policy_granular.c b/src/crypto_policy_granular.c index 8e2ab3e25e..41d8bec8ce 100644 --- a/src/crypto_policy_granular.c +++ b/src/crypto_policy_granular.c @@ -14,13 +14,13 @@ * 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 + * 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 ∩ build-supported" + * 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. @@ -256,7 +256,7 @@ int wolfSSL_crypto_policy_parse_granular(const char *buf, 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 — + * 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). */ @@ -407,7 +407,7 @@ static const char *wcp_lookup_str(const struct wcp_kv_str *m, const char *cp) * 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 + * 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, @@ -546,7 +546,7 @@ int wolfSSL_crypto_policy_min_version(const WolfGranularPolicy *p, * 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. */ + * family -- the right floor. */ if (v < best_pri) { best_pri = v; best = v; @@ -579,7 +579,7 @@ int wolfSSL_crypto_policy_apply_granular(WOLFSSL_CTX *ctx, /* 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 + * -- 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. * @@ -606,7 +606,7 @@ int wolfSSL_crypto_policy_apply_granular(WOLFSSL_CTX *ctx, return WOLFSSL_FAILURE; } if (buf[0] == '\0') { - WOLFSSL_MSG("granular policy: derived cipher list is empty — " + WOLFSSL_MSG("granular policy: derived cipher list is empty -- " "policy enables no suites this build can serve"); return WOLFSSL_FAILURE; } diff --git a/tests/api.c b/tests/api.c index ac7a0ca896..bd15c71b53 100644 --- a/tests/api.c +++ b/tests/api.c @@ -30064,7 +30064,7 @@ static int crypto_policy_cipher_count(const WOLFSSL * ssl) * * 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 + * 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) @@ -30155,7 +30155,7 @@ static int test_wolfSSL_crypto_policy_granular(void) * 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 + * applier silently kept the CTX's default suites -- i.e. the * allowlist did not constrain anything. * * We assert: @@ -30163,7 +30163,7 @@ static int test_wolfSSL_crypto_policy_granular(void) * 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 + * 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