Surfaced by STPA-Sec analysis recorded in `docs/security/stpa-keyless-2026-05-25.md`, section 4 — UCA-5.
This is not a verification bypass — the artifact-binding check (added in PR #136) uses its own serialization that propagates errors, so verify() will still reject a module it can't serialize. UCA-5 is a correctness / audit-trail-integrity gap.
What's wrong
In `KeylessVerifier::verify` at `signer.rs:569–573`:
```rust
let mut module_bytes = Vec::new();
module.serialize(&mut module_bytes).ok(); // <-- error swallowed
let module_hash = Sha256::digest(&module_bytes); // hash over possibly-empty vec
let artifact_hash = format!("sha256:{}", hex::encode(&module_hash));
```
`module.serialize` returns `Result<(), CoreError>` (`mod.rs:323`). The `.ok()` discards the error: `module_bytes` remains empty and `module_hash` becomes the SHA-256 of zero bytes:
```
sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
```
This hash is then:
- Written to the audit log as `artifact_hash` (via `audit::log_verification_attempt` / `log_verification_success`).
- Used as part of the proof cache key (`signer.rs:594–597`).
Why it matters
- Audit trail integrity. A defender doing post-incident forensics on `wsc`'s audit trail sees an empty-hash record for the verification attempt, with no indication that the serialization failed. That hash is not the artifact's actual identity.
- Cache key collision. All such error cases share a single cache slot (the empty-hash slot). Observable collision.
- Silent error hiding. A user / operator running `wsc verify --keyless` does not learn that the verifier had a problem serializing the input; the verification proceeds (and may legitimately fail later or succeed depending on whether the input parses).
Suggested fix
Replace `.ok()` with `?`:
```rust
let mut module_bytes = Vec::new();
module.serialize(&mut module_bytes)?;
let module_hash = Sha256::digest(&module_bytes);
```
Or — cleaner — compute the audit hash from the stripped-bytes that `verify_artifact_binding` already produces (so audit log and verification use the same bytes-as-hashed). That requires plumbing those bytes back out, which is more refactoring.
Acceptance
- `.ok()` becomes `?` (or equivalent error propagation).
- Test: a `Module` that fails to serialize must surface a non-zero exit code; the audit log must not record an empty-hash artifact.
Cross-ref
Not in the prior audit. Novel finding from the STPA-Sec pass.
Surfaced by STPA-Sec analysis recorded in `docs/security/stpa-keyless-2026-05-25.md`, section 4 — UCA-5.
This is not a verification bypass — the artifact-binding check (added in PR #136) uses its own serialization that propagates errors, so verify() will still reject a module it can't serialize. UCA-5 is a correctness / audit-trail-integrity gap.
What's wrong
In `KeylessVerifier::verify` at `signer.rs:569–573`:
```rust
let mut module_bytes = Vec::new();
module.serialize(&mut module_bytes).ok(); // <-- error swallowed
let module_hash = Sha256::digest(&module_bytes); // hash over possibly-empty vec
let artifact_hash = format!("sha256:{}", hex::encode(&module_hash));
```
`module.serialize` returns `Result<(), CoreError>` (`mod.rs:323`). The `.ok()` discards the error: `module_bytes` remains empty and `module_hash` becomes the SHA-256 of zero bytes:
```
sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
```
This hash is then:
Why it matters
Suggested fix
Replace `.ok()` with `?`:
```rust
let mut module_bytes = Vec::new();
module.serialize(&mut module_bytes)?;
let module_hash = Sha256::digest(&module_bytes);
```
Or — cleaner — compute the audit hash from the stripped-bytes that `verify_artifact_binding` already produces (so audit log and verification use the same bytes-as-hashed). That requires plumbing those bytes back out, which is more refactoring.
Acceptance
Cross-ref
Not in the prior audit. Novel finding from the STPA-Sec pass.