crypto-policies: granular allowlist parser for Fedora system policies#10541
crypto-policies: granular allowlist parser for Fedora system policies#10541max-qlab wants to merge 4 commits into
Conversation
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 wolfSSL#60 as the granular alternative to the
single-line @SECLEVEL=N:... cipher string the wolfSSL back-end was
rejected for in wolfSSL 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 <dominik@qreativelab.io>
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 <dominik@qreativelab.io>
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 wolfSSL#9802, fedora-crypto-policies wolfSSL#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 <dominik@qreativelab.io>
|
Can one of the admins verify this patch? |
|
Thank you @max-qlab, this is very cool! I'm reviewing now. |
There was a problem hiding this comment.
Pull request overview
Adds a new “granular allowlist” parser/applier for Fedora/RHEL system crypto-policies and routes wolfSSL_crypto_policy_enable*() between the legacy single-line @SECLEVEL=... format and the new sectioned allowlist format. This integrates policy application into WOLFSSL_CTX creation and adds API tests plus real Fedora-generated fixtures and a format README.
Changes:
- Introduces
src/crypto_policy_granular.{c,h}implementing allowlist parsing, mapping, and CTX application. - Updates
src/ssl.cto sniff/route formats and apply granular policy on each newWOLFSSL_CTX. - Adds
tests/api.ccoverage and ships example allowlist fixtures + format documentation.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
tests/api.c |
Adds granular allowlist API test and helper for counting enabled suites. |
src/ssl.c |
Routes enable() via header sniff; applies granular policy during wolfSSL_CTX_new_ex; expands SetMinVersion guard logic. |
src/include.am |
Builds the new granular module and distributes its internal header. |
src/crypto_policy_granular.h |
Declares internal granular policy structs and helper APIs. |
src/crypto_policy_granular.c |
Implements granular allowlist parsing, mapping tables, derivation, and CTX application. |
examples/crypto_policies/README.md |
Documents both legacy and granular formats and the apply semantics. |
examples/crypto_policies/*/wolfssl-allowlist*.txt |
Adds Fedora-generator outputs and a DTLS-only test fixture used by tests. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
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 (wolfSSL#10541). 1. src/crypto_policy_granular.c top includes. The file was using the standalone `#include <config.h>` + `#include <wolfssl/wolfcrypt/settings.h>` 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 <wolfssl/wolfcrypt/libwolfssl_sources.h>` (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 wolfSSL#2), (b) the DTLS CTX survives apply_granular (regression guard for fixes wolfSSL#3 and wolfSSL#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 <dominik@qreativelab.io>
|
Hi @philljj, @Copilot — sorry about the noisy initial submission. New commit What changed
Test extension The DTLS block of Local verification Built and tested on Ubuntu 24.04 + gcc 13 with five configurations:
On the contributor agreement — Max and Dom (Dominik Blain, co-author on every commit) will email Thanks again for the careful read — the DTLS issues in particular are the kind of thing you only catch when someone who's read the code points at the right line. |
philljj
left a comment
There was a problem hiding this comment.
It looks like non-ASCII chars are sneaking in, which is causing the check-source-text failure:
[check-source-text] [1 of 1] [0b95179dac]
...
8 bit:
./tests/api.c:30067: * created afterwards must reflect the policy's primitives ¶ cipher
./tests/api.c:30158: * applier silently kept the CTX's default suites ¶ i.e. the
./tests/api.c:30166: * success ¶ empty derive now returns WOLFSSL_FAILURE and
./src/ssl.c:332: * per-setter guards (SetMinVersion, SetMinRsaKey_Sz, ¶) must let our
./src/ssl.c:5547: /* Mirror the derived cipher list ¶ see comment in
./src/crypto_policy_granular.c:17: * the `crypto_policy_applying` flag in src/ssl.c ¶ that is the only
./src/crypto_policy_granular.c:23: * upgrading; the intersection of "policy-enabled ¶ build-supported"
./src/crypto_policy_granular.c:259: * may add directives that change the *meaning* of existing keys ¶
./src/crypto_policy_granular.c:410: * enable every TLS 1.2 row that survives the other constraints ¶ and
./src/crypto_policy_granular.c:549: * family ¶ the right floor. */
[...and more...]
Please remove the non-ASCII characters. You might need to instruct Claude to use ASCII-only for code and comments.
Thank you!
Summary
Adds a granular allowlist back-end for the system-wide crypto-policy file consumed by
wolfSSL_crypto_policy_enable*(), in parallel with the existing single-line@SECLEVEL=N:...parser. Routing is done by header sniff (version =/override-mode =/[section]) so existing legacy deployments keep working unchanged.This is the wolfSSL-side counterpart of the format that
fedora-crypto-policiesemits via theWolfSSLGeneratorPython class (drop-in next to the existing GnuTLS back-end). The companion Python generator is being submitted in parallel against fedora-crypto-policies.Closes the file-format objection from @asosedkin on fedora-crypto-policies #60 (PR #8205's single-line OpenSSL cipher string judged insufficient) and unblocks wolfSSL #9802.
What changed
src/crypto_policy_granular.{c,h}src/ssl.cwolfSSL_crypto_policy_enable*()sniffs the header and routes;wolfSSL_CTX_new_ex()calls the applier when granular;wolfSSL_CTX_SetMinVersion()guard widened to allow the applier throughsrc/include.am!BUILD_CRYPTONLYguardtests/api.ctest_wolfSSL_crypto_policy_granular: enable / suite-count / membership / monotonicity / DTLS / format-version / override-mode invariantsexamples/crypto_policies/{legacy,default,future}/wolfssl-allowlist.txtexamples/crypto_policies/default/wolfssl-allowlist-dtls.txtexamples/crypto_policies/README.mdThe granular applier drives, on every fresh
WOLFSSL_CTX:wolfSSL_CTX_SetMinVersion(from the lowestenabled-version)wolfSSL_CTX_set_cipher_list(from thecipher × kx × mac × versioncross-product against the known TLS suite table)wolfSSL_CTX_UseSupportedCurve(perenabled-group)wolfSSL_CTX_set1_sigalgs_list(from the mappedenabled-sigset)wolfSSL_CTX_SetMin{Rsa,Dh,Ecc}Key_Sz(frommin-rsa-bits/min-dh-bits)Steps 1 and 4 are best-effort: a build that lacks TLS 1.0 support or rejects an
rsa_pss_*sigalg the policy lists logs and continues. Cipher list and key-size floors are authoritative.Test plan
make checkon three configurations, all green:--enable-opensslextra --enable-tls13 --enable-arc4 --enable-debug --enable-supportedcurves --enable-ed25519 --enable-ed448 --enable-curve25519 --enable-curve448: 743 passed / 0 failed / 1507 total--enable-debug(production build): 738 passed / 0 failed / 1507 total--disable-dtlsvariant: 688 passed / 0 failed / 1507 total (the DTLS section of our test auto-skips via#ifdef WOLFSSL_DTLS)Zero regression vs
masterbaseline. The newtest_wolfSSL_crypto_policy_granularexercises:WOLFSSL_CTXwith the expected security level and suite contents.version = 2is rejected outright.override-mode = blocklistis rejected.Scope
What this PR does not do (deferred):
MLDSA*, additional cipher suites,KEM-ECDH,HMAC-SHA1, etc. are parsed and silently ignored when the build does not implement them — explicit support can land per primitive).wolfSSL_crypto_policy_init_ctx()to derive its min-key floors from the granular struct rather than fromsecLevel. Today the granular path mirrorssecurity-levelinto the legacycrypto_policy.secLevelso the existing min-key-size scaffolding inwolfSSL_crypto_policy_init_ctx()continues to behave; an explicit pass-through can land in a follow-up.The exact section/key names (
enabled-version,enabled-cipher, …) are deliberately close to the GnuTLS back-end so the crypto-policies maintainers can adjust them cheaply. That naming call is theirs, on fedora-crypto-policies work item #60.Authors
🤖 Generated with Claude Code