You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(store,tls): check the Vault Transit key TYPE and floor negotiable suites at 128 bits (BACKLOG #1166) (#771)
* fix(store,tls): check the Vault Transit key TYPE and floor negotiable suites at 128 bits (BACKLOG #1166)
Two limbs of ASVS 11.2.3, both operator-supplied key material with no
counterparty on the other end.
Limb 1, the 2026-08-22 owner ruling itself, which was not built: every
Vault Transit key this product uses is operator-chosen by name, and the
product read its type and threw the answer away. Measured with a fake
Transit backend at 2b8bccb, build_transit_cipher ACCEPTED ecdsa-p256,
aes128-cmac and rsa-2048 and built a working cipher, while an unreachable
key refused in the same run. The envelope-unwrap path was worse than the
item recorded: it called read_key zero times. One authority,
require_transit_key_type, now checks all three uses against a positive
per-use allow-list and refuses to start on a mismatch or an unreadable
response. The data key takes the AEAD types alone because the cell AAD
binding depends on Transit binding associated_data; the audit key adds
Vault's dedicated hmac type; the KEK adds the RSA key-transport types at
or above the floor, so rsa-2048 is absent from all three.
Limb 2, the cipher-string strength gate. The item's own repro is stale
and re-measured false: the NULL string it names is refused at HEAD,
because BACKLOG #1317 landed that check after the measurement. The gap
that survives is the six *-CCM8 suites, which are forward-secret,
encrypting and peer-authenticating and which OpenSSL rates at 64 bits
because the tag is truncated to 8 octets. A fourth predicate now runs in
validate_tls_ciphers and harden_cipher_suites, outside the approved-list
guard so it also binds the proxy declaration path.
Deliberately not built, each for a reason the item states: the jwks.py
floor, the list-preserving security_level raise, and the JWKS verify-path
floor. Where the code states a floor it states the effective one, and a
test pins that the load_cert_chain sites sit at OpenSSL security level 2.
Nine mutations, each red, zero vacuous, each checked to fail rather than
error. Every absence claim on the vault record was run over the added
lines with a positive control: none is silenced, so no pattern re-cut.
* fix(tls): assert the strength gate, not the platform's cipher inventory (BACKLOG #1166)
CI red on test (ubuntu-latest, py3.14): four failures, all mine, all in
tests/test_tls_policy.py, one root cause.
The tests hardcoded ECDHE-ECDSA-AES256-CCM8 as a 64-bit suite. That is a
property of the linked OpenSSL, not of TLS. Measured: the Windows build
rates all ten visible *-CCM8 suites at 64, while ubuntu-latest rates the
same name 256. So two tests failed on their own premise (assert 256 <
128) and two with DID NOT RAISE.
The sharpest was the approved-list arm, which had been PASSING on
Windows for the wrong reason: _APPROVED_TLS_SUITES was doing the
refusing, not the new gate. The ubuntu failure proved it by printing the
allow-list message where the strength message was expected. That is the
same defect class this item is about, committed in the fix for it.
The real-suite rows now DISCOVER a sub-floor suite on whatever build
they run on and skip with a reason naming that OpenSSL when there is
none; they never assert one exists. Synthesised-entry rows carry the
discrimination on every platform and drive both real functions -- an
SSLContext subclass for harden_cipher_suites, and a shimmed ssl module
for validate_tls_ciphers, which builds its own probe and offers no seam.
That second shim is not belt-and-braces. With only the real-suite rows,
a mutation deleting the strength branch of validate_tls_ciphers was
measured VACUOUS under a simulated no-sub-floor build: the skip would
have hollowed out the branch this limb exists to add.
_is_strong_enough is unchanged in behaviour and was correct throughout.
Its comment moved from stating the Windows census as universal to naming
both builds and warning against restating either.
Verified by simulating the ubuntu shape locally: green with three honest
skips, and all four gate mutations still red under that simulation.
The other failing leg, repo harness tests (windows-2025), is
test_worktree_gate.py timing out launching pwsh -- BACKLOG #1304, a file
this branch does not touch.
---------
Co-authored-by: wshallwshall <mefordev@messagefoundry.org>
Copy file name to clipboardExpand all lines: docs/BACKLOG.md
+26Lines changed: 26 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -10006,6 +10006,32 @@ Proof: four mutations, each red, zero vacuous -- per-character floor disabled, v
10006
10006
10007
10007
**Residual:** the estimator stays ORDER-blind and DICTIONARY-blind, so a string of sixteen distinct sequential letters measures the arithmetic maximum and passes. Nothing short of generating the salt ourselves fixes that, which is what ADR 0030 already requires.
10008
10008
10009
+
**A THIRD #1166 LIMB BUILT 2026-09-03: the Vault Transit key-type check -- the 2026-08-22 OWNER RULING itself, which the two limbs above did not touch.** The ruling: *"check the type the product already fetches against what each of the three uses requires, and refuse to start on a mismatch. The round trip is already being paid for; only the answer is being thrown away."* All three uses are now checked and all three refuse.
10010
+
10011
+
Measured before building, by execution against a fake Transit backend at engine commit `2b8bccb43`, with a positive control in the same run. `build_transit_cipher` ACCEPTED `ecdsa-p256` (a signing-only key that cannot encrypt at all), `aes128-cmac` (MAC-only) and `rsa-2048` (roughly 112 bits, and Transit RSA takes no `associated_data`), building a working cipher each time -- while an unreachable key DID refuse. So the metadata read was live and simply never asked about type. **The envelope-unwrap path was worse than the item recorded:** `keyprovider_vault.VaultKeyProvider.active_key` called `read_key` ZERO times, so there was no round trip to salvage there and the fix ADDS one, once per process start.
10012
+
10013
+
Built: one authority, `require_transit_key_type`, in `store/keyprovider_vault.py` (the module that already owns `_build_client` and `_EXTRA`, and which `crypto_transit.py` already imports -- so no new module and no new import edge), with a POSITIVE allow-list per use. A deny-list would admit every key type Vault adds after the line was written, and the failure mode of admission here is an at-rest PHI primitive nobody checked. The three sets differ on purpose and a test pins the differences so a future edit that collapses them has to confront why: the data key takes the AEAD types alone, because the store passes `cell_aad(...)` on every call and the ASVS 11.3.3 cell binding depends on Transit binding it; the audit MAC key adds Vault's dedicated `hmac` type, which cannot encrypt but is the right type for a MAC; the KEK adds the RSA key-transport types AT OR ABOVE the floor. `rsa-2048` is absent from all three, which is the 128-bit floor doing the refusing rather than the type -- a mutation admitting it turns the suite red. A key serving as both data and audit key is held to the narrower data set. An unreadable response shape refuses too: a validator that cannot see the type and returns anyway is a control reporting success forever.
10014
+
10015
+
**A FOURTH LIMB BUILT THE SAME DAY: the operator cipher-string strength gate -- and THIS ITEM'S OWN REPRO FOR IT IS STALE AND WAS RE-MEASURED FALSE.** The item records that `validate_tls_ciphers` accepts `ECDHE-ECDSA-NULL-SHA:ECDHE-RSA-NULL-SHA:@SECLEVEL=0` and that `harden_cipher_suites` does not raise on the resulting context. At `2b8bccb43` **both functions REFUSE that string**, because BACKLOG #1317 landed a NULL-cipher check after the 2026-08-20 measurement was taken. Re-measured rather than relayed, and recorded here so nobody re-derives the false repro.
10016
+
10017
+
The gap that survives is narrower and different. Of the 158 suites offerable at `@SECLEVEL=0` on CPython 3.14.6 / OpenSSL 3.5.7, **80 survive all three shipped property checks and exactly SIX of the 80 are rated below 128 bits** -- the `*-CCM8` family, which is forward-secret, encrypting and peer-authenticating with a 128- or 256-bit cipher key, and which OpenSSL rates at **64** because the authentication tag is truncated to 8 octets. That is ASVS 11.2.3's *"algorithm, key size, and CONFIGURATION"* clause, and none of the three existing predicates can see it: `alg_bits` reads 128 or 256 and would miss it entirely. Measured before the fix: a string naming all six was ACCEPTED by `validate_tls_ciphers(require_approved_suites=False)` -- the `[api].proxy_tls_ciphers` declaration path, where the allow-list deliberately does not run -- and the resulting context was ACCEPTED by `harden_cipher_suites`. A fourth predicate, `_is_strong_enough`, now runs in both, outside the `require_approved_suites` guard so it binds the declaration path too. It fails closed on an ungradeable entry. It raises on no supported configuration today: every default context shape resolves to 128 and 256 bits only, and a test pins that so a future OpenSSL cannot break it silently.
10018
+
10019
+
**THE FIRST CUT OF LIMB 2'S TESTS COMMITTED THIS ITEM'S OWN DEFECT AND CI CAUGHT IT.** The six-suite census above was measured on Windows and written into the tests as though it were a fact about TLS. It is a fact about the linked OpenSSL. On `ubuntu-latest` the same `ECDHE-ECDSA-AES256-CCM8` rates **256** bits, so four tests went red: two on the premise itself, two with `DID NOT RAISE`. The sharpest was the approved-list arm, which had been **passing on Windows for the wrong reason** -- `_APPROVED_TLS_SUITES` was doing the refusing, not the new gate, and the ubuntu failure proved it by printing the allow-list message where the strength message was expected. That is precisely the shape this item exists to fix: an assertion that passes or fails for a reason other than the control it names.
10020
+
10021
+
Repaired so the tests assert the GATE rather than the platform's cipher inventory. The real-suite rows now DISCOVER a sub-floor suite on whatever build they run on and skip with a reason naming that OpenSSL when there is none -- they never assert one exists. Synthesised-entry rows carry the discrimination on every platform, driving both real functions (an `SSLContext` subclass for `harden_cipher_suites`, a shimmed `ssl` for `validate_tls_ciphers`, which builds its own probe and offers no seam). Verified by simulating the ubuntu shape locally: with no sub-floor suite the run is green with three honest skips, and **all four gate mutations still turn it red under that simulation**. The `validate_tls_ciphers` synthesised row exists because that mutation was measured VACUOUS under the simulation before it was added -- the skip would otherwise have hollowed out the very branch the limb is about. `_is_strong_enough` itself was correct throughout and is unchanged in behaviour; only its comment moved, from stating the Windows census as universal to naming both builds and warning against restating either.
10022
+
10023
+
**The floor stated in the code is the EFFECTIVE one, not the written one, because this item warns about exactly that inversion.** `_MIN_TLS_STRENGTH_BITS` grades NEGOTIABLE SUITES. The floor in force on operator KEY MATERIAL at the `load_cert_chain` sites is OpenSSL's security level, measured on this runtime as **2 on every context shape this module builds** -- roughly 112 bits -- with `SSLContext.security_level` READ-ONLY (`AttributeError` on assignment). A test asserts both numbers so the two floors cannot be conflated later.
10024
+
10025
+
**One peer test's fake was under-specified and the new check caught it, which is the check working.** `tests/test_vault_client_redirect_policy.py` drives `build_transit_cipher` end to end to prove the Transit cipher inherits the no-redirect client policy, and its stand-in Transit returned a `read_key` response with no `type` at all. That is now a refusal, so the fake reports a valid data-key type and gets out of the way of the property it exists to measure. No production behaviour was relaxed to accommodate it.
10026
+
10027
+
**The instrument trap was checked, not assumed, and it did not fire.** Every absence claim on the vault record was compiled and run over the 221 source lines this change adds, with a positive control proving the sweep can see those lines: zero matched. This requirement's recorded absence pattern keys on a 3072-bit RSA comparison, which this change does not introduce, so the machine guard is still firing and no re-cut was needed.
10028
+
10029
+
Proof: nine mutations, each red, zero vacuous -- each of the three key-type checks reverted to the bare read in turn, the unreadable-type fail-closed disabled, `rsa-2048` admitted to the KEK set, `hmac` admitted to the data set, the strength check disabled in each of its two functions, and `_is_strong_enough` forced True. Each mutation was checked to FAIL rather than ERROR: a first cut of three of them broke the syntax instead of the behaviour, which reds the run while proving nothing about the check. Four of the nine were then re-run against a SIMULATED build offering no sub-floor suite, and all four stayed red there. CI is the authority on the suite, and on the pre-repair commit it reported both Windows legs green and `ubuntu-latest` at 4 failed / 11,496 passed / 843 skipped -- all four failures the platform defect described above. The repaired commit has not yet been through CI at the time of writing; read the run rather than this line.
10030
+
10031
+
**DELIBERATELY NOT BUILT, and each for a reason this item already states.** Raising `_MIN_RSA_BITS` in `auth/oidc/jwks.py` -- named here as the thing that would not be an honest pass. The list-preserving `security_level` raise -- contingent on a counterparty ruling not yet made, and measured to refuse chains anchored at 35 of the 84 trust anchors the shipped default already loads. The JWKS verify-path floor -- an availability decision about somebody else's infrastructure. None of the three key-type checks has a counterparty on the other end: the operator provisions their own Vault, so a refusal there breaks no handshake with anybody, which is the same profile that made the signing-key floor safe to land.
10032
+
10033
+
**Still open:** the XML signature-verification algorithm policy, the WebAuthn advertised-algorithm restriction, the PKCS#12 import and inbound mTLS rows, and the verdict itself. The cell moves by a scorecard re-score, which no Builder can perform.
10034
+
10009
10035
## 1167. research an honest pass for ASVS 11.2.4 -- constant-time recovery-code verification without turning ten argon2id slots into an amplification target
10010
10036
10011
10037
> 🔢 **Re-scored 2026-08-20 -> P3.** Value **4/10** · Difficulty **7/10** · _money pit_. The data-dependent early return survives on the shipped MFA path: _verify_second_factor walks the argon2id recovery hashes and returns on the first match, so the number of ~64 MiB verifications is a function of which code was presented. Value 4 because the leak is a wall-clock signal on an already-authenticated second factor rather than a bypass; difficulty 7 because the obvious constant-time loop multiplies a 64 MiB argon2id verification by the slot count on every attempt, converting a timing leak into a memory and CPU amplification target, and the evidentiary half has no precedent in this tree. _(was 4/10 · 7/10.)_
0 commit comments