Skip to content

UCA-5: keyless audit log records hash from silently-discarded serialize error #140

@avrabe

Description

@avrabe

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:

  1. Written to the audit log as `artifact_hash` (via `audit::log_verification_attempt` / `log_verification_success`).
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions