release: v0.9.1 — close two #135-class keyless verify bypasses + STPA-Sec report#136
Merged
Conversation
KeylessVerifier::verify only checked that the embedded Fulcio cert
chain was valid and that a Rekor SET was present. It never recomputed
the module hash and never ECDSA-verified the signature blob against
the leaf cert's public key, so any tampered module — including the
issue's byte-flip repro and the broader case of splicing a signature
section from one artifact onto another — returned exit 0.
Adds KeylessSignature::verify_artifact_binding(module):
1. detach_signature → serialize the stripped module → SHA256 →
compare to the stored module_hash (reject on mismatch).
2. Parse the leaf cert's SPKI as a P-256 VerifyingKey → ECDSA-verify
the signature blob against the recomputed digest (reject on
mismatch).
Wires it into verify() as step 4, between Rekor SET verification and
identity claim checks. Both failure modes return VerificationFailed
with a log::error! describing which check rejected; the error variant
does not distinguish to avoid leaking which leg a tampered artifact
tripped.
Seven new tamper-rejection unit tests in format.rs build a real
ECDSA P-256 keypair (via rcgen → p256), a real self-signed cert, and
a real signed module, then exercise: genuine accept, byte-flipped
module, corrupted signature, substituted module_hash, substituted
cert, missing signature section, empty cert chain. All pass.
Downstream synth #140 case 3 (the original repro) can flip from
xfail back to a positive assertion once a release containing this
fix ships.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
This second security commit on the issue #135 branch closes the UCA-2 verification-bypass identified by STPA-Sec analysis after PR #136 landed the artifact-binding fix. UCA-2: KeylessVerifier::verify proved the Rekor entry was logged by Rekor (verify_set) and that the bundle's cert+sig+module were mutually consistent (verify_artifact_binding from PR #136), but never decoded the entry's body to check that the logged (hash, signature, public key) triple actually referenced *this* bundle. Concrete attack: 1. Attacker obtains a legitimate Fulcio cert for their own OIDC identity (any signed-in user can do this). 2. Signs malicious module M' with their ephemeral key. 3. Constructs a KeylessSignature with their genuine sig + cert + SHA256(M'), but embeds ANY unrelated public Rekor entry — e.g. the entry for someone else's hello-world module from 2024. 4. wsc verify --keyless returns exit 0. This re-emerges the #135 attack class one layer up. The fix adds KeylessSignature::verify_rekor_body_binds_to_bundle in format.rs, called from KeylessVerifier::verify as step 5 (after artifact binding, before identity claims). It decodes the hashedrekord/0.0.1 body and asserts three equalities: 1. body.spec.data.hash.value == hex(self.module_hash) (sha256 algo) 2. base64_decode(body.spec.signature.content) == self.signature 3. Leaf cert DER from body.spec.signature.publicKey.content byte-equals self.cert_chain[0]'s DER (PEM-parsed for whitespace normalisation). All failures emit WSError::VerificationFailed with a log::error! identifying the mismatch. The error variant deliberately does not discriminate — leaks no oracle to a forging attacker. Ten new unit tests in format.rs cover the happy path plus tampering each body field (hash, signature, public key), unsupported kind / apiVersion / hash-algorithm, garbage / non-JSON body, and accept genuine uppercase-hex hashes. Also delivered with this release: - docs/security/stpa-keyless-2026-05-25.md: full STPA-Sec analysis enumerating five UCAs against the keyless verify subsystem. UCA-2 is closed by this commit; UCA-1 (Rekor inclusion proof never run on verify path), UCA-3 (cert validity bound to attacker-pickable integrated_time), UCA-4 (cache hit skips SET re-verify without bundle-equality check), and UCA-5 (audit log records hash from silently-discarded serialize error) remain open as follow-up issues. Each UCA includes a code citation, the STPA-4 questions, loss scenario, suggested fix, and a verification trail. - Version bumped 0.9.0 -> 0.9.1 across workspace + internal pins. CHANGELOG entry under [0.9.1] documents both fixes and the new STPA report. Full cargo test -p wsc: 605 + 5 + 5 + 5 = 620 tests pass, 0 failures.
This was referenced May 25, 2026
avrabe
added a commit
that referenced
this pull request
May 29, 2026
…sh error (v0.9.2) Continues the STPA-Sec keyless-verify hardening from v0.9.1 (#136), closing two of the three clear-cut follow-ups from docs/security/stpa-keyless-2026-05-25.md. UCA-4 (#139): bind the proof-cache key to the full Rekor entry content via CacheKey::from_entry (every field, length-prefixed) instead of just (module_hash, uuid) — both attacker-supplied in the bundle. A cache hit can now only occur for a byte-identical entry that already passed SET verification, removing the defence-in-depth loss where a mutated entry rode a stale cache slot past the SET re-check on a cache hit. UCA-5 (#140): propagate the module serialize error in the audit-hash computation (? instead of .ok()), so a serialize failure aborts verify instead of recording the SHA-256 of zero bytes as a false artifact identity in the audit log and proof-cache key. UCA-1 (#137) was scoped in but DESCOPED: wiring the existing Merkle inclusion-proof verifier into verify() revealed it recomputes the wrong root for fresh production Rekor entries on the log2025-* shards (Rekor v2 / tiled-log migration) — the SET verifies but the computed root != the proof's root_hash. Enabling it fail-closed would reject all legitimate keyless signatures, so it stays unwired until the verifier is fixed. Reclassified on #137. UCA-3 (#138) remains deferred (clock-policy). Four new unit tests (3 cache-key binding + 1 fail-closed inclusion-proof building block); 609 library tests pass. Clean-room verified. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #135. This branch was started as the artifact-binding fix for the original bug (one byte flipped in a signed module returned exit 0 from
wsc verify --keyless). Once that landed, an STPA-Sec pass on the keyless verify subsystem found a second #135-class bypass at the next layer up — the embedded Rekor entry was never cross-referenced against the bundle, so any unrelated public Rekor entry would pass.Both fixes ship in this release. The STPA report is also included so the maintainer has a durable reference for the three other open UCAs that didn't land here.
Two security fixes (both #135-class)
Fix 1 — Artifact binding (initial commit on this branch)
KeylessVerifier::verifyverified the Fulcio cert chain + Rekor SET, then accepted the module without recomputing its hash or ECDSA-verifying the signature against the leaf cert. NewKeylessSignature::verify_artifact_bindingstrips the signature section, recomputes SHA-256, andverify_digests the signature under the leaf cert's SPKI. Wired as step 4 ofverify().Fix 2 — Rekor body binding / UCA-2 (second commit on this branch)
After fix 1, the verifier proved (cert+sig+module) were mutually consistent and (Rekor entry was logged by Rekor), but never decoded the entry's body to check that the logged
(hash, signature, public key)triple actually referenced this bundle. An attacker holding any legitimate Fulcio cert could sign a malicious module and embed any unrelated public Rekor entry to pass verification:KeylessSignaturewith their genuine sig + cert +SHA256(M'), embedding any unrelated public Rekor entry.wsc verify --keylessreturns exit 0.The original #135 attack class re-emerges one layer up.
New
KeylessSignature::verify_rekor_body_binds_to_bundledecodes the hashedrekord/0.0.1 body and asserts three equalities: artifact hash, signature content, and leaf-cert DER. Wired as step 5 ofverify().Tests
17 new tamper-rejection unit tests in
src/lib/src/signature/keyless/format.rs, all passing. Coverage matrix:module_hashfieldkindapiVersionFull
cargo test -p wsc: 620 tests pass (605 lib + 5 + 5 + 5), 0 failures.Error variant design note
Both checks return
WSError::VerificationFailedwith alog::error!identifying the specific mismatch. The error variant is deliberately not discriminated (noHashMismatch/SignatureInvalid/BodyHashMismatchvariants) — that would give an attacker an oracle to discover which leg of the check rejected their forgery while iterating. Operators get the detail via the log line.STPA-Sec report (new)
docs/security/stpa-keyless-2026-05-25.md— five UCAs identified, all independently verified against HEAD. Each UCA has a code citation, STPA-4 questions, loss scenario, suggested fix, and a verification trail (grepcommands + file:line that confirmed the gap):integrated_timePlus 4 speculative items the STPA explicitly couldn't confirm (multi-SAN selection,
serializedeterminism, two-signature-section asymmetry,Module::Clonecompleteness), 1 out-of-scope flag (sct.rsSCT verifier stub never invoked — Fulcio SCTs entirely unchecked, separate subsystem), and 7 properties explicitly cleared (skip-Rekor sentinel, chain depth bound, empty cert chain handling, negative-time rejection, etc.).Version + release
This PR bumps the workspace version 0.9.0 → 0.9.1 (patch release for security fixes). CHANGELOG
[0.9.1] — 2026-05-25entry written. Once this merges, tagv0.9.1will trigger the release workflow.Downstream impact
pulseengine/synth #140 case 3 currently xfails
wsc verify --keylesson a byte-flipped module. After this release ships, that assertion can flip back to "tampered file rejected" with no synth-side changes. (Note: synth would need a separate test for the UCA-2 attack — synth #140 case 3 only exercises Fix 1's behaviour.)Disclosure
Both UCAs are verification-bypasses in the load-bearing keyless path. Affected versions: all releases that included
KeylessVerifier::verify(back through v0.9.0 and earlier). Recommend GHSA + RUSTSEC advisory + yank of affected versions; flagging here rather than asserting.🤖 Generated with Claude Code