Skip to content

fix: origin uniqueness enforcement + revoc artifact hash enforcement#54

Merged
rmhrisk merged 2 commits intomainfrom
fix/origin-uniqueness-and-revoc-enforcement
Mar 17, 2026
Merged

fix: origin uniqueness enforcement + revoc artifact hash enforcement#54
rmhrisk merged 2 commits intomainfrom
fix/origin-uniqueness-and-revoc-enforcement

Conversation

@rmhrisk
Copy link
Copy Markdown
Contributor

@rmhrisk rmhrisk commented Mar 17, 2026

Closes two correctness gaps from the grade assessment.

Origin uniqueness (#32): All four SDK Issuers now reject construction if the same origin string is already registered in the process. Same origin → same origin_id → ambiguous verifier routing. Process-wide registry in each language with test-only deregistration helpers.

Revoc artifact hash enforcement: All four verifiers now enforce the revoc: hash committed in checkpoint bodies. Enforcement rule: if the freshly fetched artifact has tree_size < checkpoint.tree_size and SHA-256(artifact) ≠ committed_hash, reject. If tree_size >= checkpoint.tree_size (artifact updated after checkpoint was signed, e.g. a revoke was processed), skip — allows normal revocation workflows.

Also fixed: all four issuers now build the artifact before signing the checkpoint, so checkpoint.revoc_hash and latestRevArtifact are always consistent.

Tests: Go 5/5 · TS SDK 43 tests · Rust 35 · Java 44 · Interop 19/19

Ryan Hurst added 2 commits March 17, 2026 18:28
Two correctness gaps identified in the grade assessment, both now closed.

═══ Origin uniqueness ═══

Each SDK now prevents two Issuer instances from sharing the same origin string
within a process. Same origin → same origin_id → ambiguous routing in a
multi-anchor verifier with undefined behavior if trust configs differ.

Go:   sync.Map-backed registeredOrigins global; DeregisterOrigin() for test
      cleanup; test helper updated to use t.Name()-derived unique origins.
TS:   Module-level Set<string> _registeredOrigins; _deregisterOrigin() exported
      for tests; guard in Issuer constructor.
Rust: static Mutex<Option<HashSet<String>>> REGISTERED_ORIGINS; register_origin()
      called in Issuer::new; #[cfg(test)] deregister_origin() for test cleanup.
Java: static synchronized Set<String> REGISTERED_ORIGINS; guard in private
      Issuer constructor; public static deregisterOrigin() for test cleanup.

═══ Revocation artifact hash enforcement ═══

All four verifiers now enforce the revoc: hash committed in checkpoint bodies.
When a freshly fetched artifact has tree_size < checkpoint.tree_size (the artifact
predates the checkpoint) and SHA-256(artifact_bytes) ≠ committed_hash, the
verification is rejected with a clear error. When tree_size >= checkpoint.tree_size
(the artifact was legitimately updated, e.g. after a revoke), the check is
skipped — this allows normal revocation workflows where the issuer rebuilds
the artifact more frequently than checkpoints are witnessed.

Artifact hash storage: each SDK stores SHA-256(raw_artifact_bytes) alongside
the cascade when a revocation artifact is fetched and parsed.

Go:   ArtifactHash [32]byte in CachedRevocation; verifyNote/fetchAndVerify
      return revocHash as 3rd value; checkpointRevocHash threaded from fetch
      path into checkRevocation; enforcement at artifact.TreeSize < checkpoint.
TS:   artifactHash: string in revocCache entries; verifyNote returns [root, size,
      revocHash]; fetchedRevocHash threaded to runAfterRootHash; enforcement with
      cached.treeSize < checkpointTreeSize guard.
Rust: artifact_hash: [u8;32] in CachedRevocation; stored via SHA-256 in
      parse_revocation_artifact; check_revocation accepts committed_revoc_hash;
      enforcement in fresh-fetch path only.
Java: byte[] artifactHash in CachedRevocation record; stored via MessageDigest
      in parseRevocationArtifact; checkRevocation accepts committedRevocHash;
      enforcement in thenApply of fresh fetch.

Also: all four issuers now build the revocation artifact BEFORE signing the
checkpoint, so checkpoint.revoc_hash and latestRevArtifact are always consistent.
(TS and Java previously used the old artifact from the previous publish cycle.)

SPEC.md and README updated: revoc enforcement promoted from 'advisory' to
'enforced with tree_size guard'.

Test totals: Go 5/5, TS SDK 34+9=43 tests, Rust 35, Java 44, interop 19/19.
Two issues caught by the CI strict typecheck:

1. parseRevArtifact return type was missing artifactHash — the function
   computes and returns it but the type signature said { cascade, treeSize }
   only. Updated to { cascade, treeSize, artifactHash }.

2. After the stale-eviction code sets cached = null, TypeScript strict mode
   cannot prove cached is non-null at the bottom of checkRevocation (the
   assignment inside if(!cached){...} doesn't help TS narrowing at the outer
   scope). Added an explicit null guard that returns fail-closed if somehow
   we reach that point without a cached artifact.
@rmhrisk rmhrisk merged commit a872115 into main Mar 17, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant