# ZeroVaultID Cert Seal Rung 6 golden bundle

This bundle records the public outcome of Cert Seal Rung 6 run `rung6r3b-542e60f5`
on a two participant cn-quickstart LocalNet. A single offline script recomputes
the file digests (check 1), the Pedersen commitment (check 3), and openA
(check 4), checks the destination-context and the cross-artifact and metadata
consistency (checks 2 and 5) and one structural property (check 6), and verifies
an Ed25519 signature over the manifest (check 0).

Scope: this bundle proves three things, the construction of Close_C and openA,
their reproducibility from the published inputs, and tamper-evidence of the
recorded artifacts under the signature. It does not prove Canton ledger
enforcement; the ledger references attest to that evidence, they do not reproduce
it.

## What is in here

```
artifacts/                 curated public artifacts (JSON), no secrets
  run.json                 run id, package id, epoch, topology, party ids
  ledger-refs.json         offsets, update id, command id, contract ids
  close-c.json             Close_C scalars, blind rho, Grumpkin point
  sigma.json               canonical public Sigma fields and openA
  destination-context.json the four context components and the sealed string
  nullifier.json           the nullifier and its two binding sites
  negative-controls.json   the five rejection assert texts
vendor/                    pinned bb.js and its runtime deps (Pedersen recompute)
verify.js                  the standalone offline verifier
manifest-pubkey.pem        Ed25519 public key the signature is checked against
manifest.json              SHA-256 of every file except manifest.json/.sig
manifest.sig               Ed25519 signature over the exact bytes of manifest.json
README.md                  this file
```

## How to run

Node 20, no network needed.

```
node verify.js
```

Clean room (recommended), a fresh container with networking disabled and the
bundle mounted read only:

```
docker run --rm --network none -v "$PWD/bundle:/bundle:ro" -w /bundle node:20 node verify.js
```

Exit code is 0 only when all seven checks pass, and nonzero on any failure.
Check 0 fails closed: if the signature does not verify, the verifier aborts
before running checks 1 through 6.

## Expected output

```
=== ZeroVaultID Rung 6 bundle verifier ===
[0] [SIGNATURE] manifest signature ... PASS
        signature valid over manifest.json; bundled key fp=e84ce01d300fef9427c979174068bf3d95d27b3c9bdaf58f5f2a3f2094a0d0e9. This printed value is NOT the authenticity backstop: a re-signing forger controls it. Establish authenticity independently first (openssl fingerprint of manifest-pubkey.pem matched against the anchor, then openssl signature verify); see README section "Establishing authenticity". Anchor: https://github.com/ZeroVaultID-Inc/evidence-signing-keys/blob/cc49d7fc519081e2d567dd80d58b7ac10dc1d2df/README.md
[1] [RECOMPUTATION] manifest hash integrity ... PASS
        recomputed SHA-256 of 428 files; mismatches=0, missing=0, extra=0
[2] [CONSISTENCY] destination-context ... PASS
        components join "canton-1|0xDestAuthority|v1|E1" == sealed "canton-1|0xDestAuthority|v1|E1" (internal consistency only)
[3] [RECOMPUTATION] Close_C Pedersen recompute (bb.js 0x4343) ... PASS
        recomputed 149a673f840dcb9233f8… == stored 149a673f840dcb9233f8… (6 inputs, no obligation term)
[4] [RECOMPUTATION] openA == SHA3-256(Sigma) ... PASS
        recomputed 110bd8d2740dc7dadff8… == stored 110bd8d2740dc7dadff8…
[5] [CONSISTENCY] cross-artifact and metadata consistency ... PASS
        nullifier bound at both sites; sigma.closeC==close-c.point; sigma.kScalar==close-c.scalars.k; close-c and sigma self-describing metadata equal the verifier constants
[6] [STRUCTURAL] negative-controls attestation ... PASS
        5/5 rows name their own check and no other
RESULT: 7/7 PASS (verified against key fp=e84ce01d300fef9427c979174068bf3d95d27b3c9bdaf58f5f2a3f2094a0d0e9)
```

## Check 0, the signature, and what it does not do

Check 0 verifies that `manifest.sig` is a valid Ed25519 signature, under the
public key in `manifest-pubkey.pem`, over the exact bytes of `manifest.json`.
Because `manifest.json` hashes every other file, the signature transitively
covers the whole bundle. A forger who edits any file must regenerate
`manifest.json`, which invalidates the retained signature, and without the
private key they cannot produce a new one, so check 0 fails closed.

There is one attack the signature does not stop, and disclosing it is the point.
A forger who generates their own Ed25519 key, swaps `manifest-pubkey.pem` for
their public key, alters the artifacts freely, regenerates `manifest.json`, and
signs it with their private key produces a bundle that passes every check and
exits 0. Worse, because verify.js runs inside the bundle, the forger can also edit
it to print the authentic fingerprint and "7/7 PASS". Nothing the bundle prints
can be trusted to expose the swap; the verifier is not, and cannot be, its own
authenticity backstop.

So the backstop is not anything verify.js prints. It is computing the fingerprint
of `manifest-pubkey.pem` with your own tooling and matching it against the anchor,
then verifying the signature with your own tooling, before you run any code from
this bundle. Those steps, set out under "Establishing authenticity" below, are the
entire authenticity claim. The public key ships inside this bundle and proves
nothing on its own; do not trust any fingerprint quoted inside this bundle,
including any value verify.js prints.

The authentic fingerprint is published by ZeroVaultID at

  https://github.com/ZeroVaultID-Inc/evidence-signing-keys/blob/cc49d7fc519081e2d567dd80d58b7ac10dc1d2df/README.md

a public repository that only ZeroVaultID can edit. The link pins one immutable
commit, so the file contents at that commit hash cannot be altered; that guarantee
rests on git commit addressing and nothing more. Three limits follow. First,
availability is not guaranteed: ZeroVaultID can delete the repository or make it
private, and GitHub can remove it, in which case the anchor becomes unreachable.
Second, the only thing pinned is content integrity at that commit hash, not any
promise that the page stays up. Third, you must independently establish that this
GitHub organization is in fact ZeroVaultID; this bundle cannot prove that for you.

The ZeroVaultID zerovault-canton-gate repository is private and is not the anchor;
the copy of the fingerprint in its README is a convenience for people with
repository access only.

Four forgeries, reproduced against this verifier:

| Forgery | What the forger does | Result |
|---|---|---|
| A | Alters attested files (run.json, ledger-refs.json), regenerates manifest.json, keeps the original signature | Fails check 0. The signature no longer matches the regenerated manifest and cannot be reforged without the key. |
| B2 | Alters sigma.json and nullifier.json, re-derives openA, regenerates manifest.json, keeps the original signature | Fails check 0. Same reason. |
| C | Swaps in an attacker public key, alters artifacts freely, regenerates manifest.json, re-signs with the attacker key; verify.js honestly prints the attacker's fingerprint | Passes the bundle's own verifier. Caught at step 1 of Establishing authenticity: openssl computes the attacker's fingerprint from manifest-pubkey.pem and it does not match the anchor. |
| D | As C, and additionally edits verify.js to print the authentic fingerprint and "7/7 PASS" | Passes the bundle's own verifier completely, and its output lies. verify.js cannot catch this, and nothing inside the bundle can: the forger controls all of it. Caught only by independent verification, steps 1 and 2, where openssl on manifest-pubkey.pem yields the attacker's real fingerprint, which does not match the anchor. This is why steps 1 to 4 are mandatory and run before verify.js. |

There is no row B. The reviewer's original B recipe altered only sigma.json and
failed check 5; B2 is the corrected retained-signature forgery that also updates
nullifier.json. These labels (A, B2, C, D) match the acceptance run and the
remediation record.

## Establishing authenticity (do this before running anything in the bundle)

Tools assumed: OpenSSL 3.x (Ed25519 raw verification needs the 3.0+ `-rawin`
form), a shell with process substitution (bash or zsh) plus perl for the
hex-to-binary step in step 3, a SHA-256 command (`sha256sum` on Linux, or
`shasum -a 256` on macOS), and `grep`. The node:20 clean room ships bash, perl,
and openssl but not xxd, which is why perl is the primary decoder; xxd is a host
fallback only. The order below is load-bearing.

1. Compute the fingerprint of the bundled public key with independent tooling:

     openssl pkey -pubin -in manifest-pubkey.pem -pubout -outform DER | openssl dgst -sha256

   The hex after the "= " is the fingerprint.

2. Match that value against the authentic fingerprint published at the anchor:

     https://github.com/ZeroVaultID-Inc/evidence-signing-keys/blob/cc49d7fc519081e2d567dd80d58b7ac10dc1d2df/README.md

   If they do not match, stop. Do not run the bundle; the key is not ZeroVaultID's.

3. Verify the manifest signature with independent tooling, using that key. This
   form writes nothing into the bundle, so it works when the bundle is mounted
   read only. It needs a shell with process substitution (bash or zsh, not POSIX
   sh) and perl:

     openssl pkeyutl -verify -pubin -inkey manifest-pubkey.pem -rawin -in manifest.json -sigfile <(perl -ne 'chomp; print pack("H*",$_)' manifest.sig)

   Expect "Signature Verified Successfully". If it fails, stop.

   Host fallback, if you would rather use xxd. It writes a temporary file to /tmp
   (never into a read-only bundle) and removes it afterward:

     xxd -r -p manifest.sig > /tmp/manifest.sig.bin
     openssl pkeyutl -verify -pubin -inkey manifest-pubkey.pem -rawin -in manifest.json -sigfile /tmp/manifest.sig.bin
     rm -f /tmp/manifest.sig.bin

4. manifest.json is now trusted. Confirm the verify.js you are about to run is the
   one it covers:

     grep '"verify.js":' manifest.json
     sha256sum verify.js

   The hex in the manifest line must equal the sha256sum output.

5. Only now run the bundle's verifier:

     node verify.js

   Its output, including any fingerprint it prints, is meaningful only after steps
   1 through 4. It is a convenience for a reviewer who has already established trust
   independently, never the source of that trust.

## What each check proves, and what it does not

Each line is tagged by class. RECOMPUTATION re-derives a value from inputs and
compares. CONSISTENCY checks stored values agree with each other or with the
verifier's fixed constants, with no derivation. STRUCTURAL checks a stored
value's shape. SIGNATURE checks the manifest signature.

0. SIGNATURE. Proves the manifest is signed by the holder of the key whose
   fingerprint is printed, so the bundle is tamper evident against anyone without
   that key. Does not prove authenticity on its own; the printed fingerprint must
   match the out-of-band copy (see above).

1. RECOMPUTATION, manifest hash integrity. Recompute SHA-256 of every file,
   compare to `manifest.json`. Proves every listed file is present and unmodified
   relative to the signed manifest (internal completeness). Does not prove the
   artifacts reflect any real ledger state.

2. CONSISTENCY, destination-context. The four components join with `|` to equal
   the sealed string. Proves the sealed context is exactly the four part binding,
   epoch included. Internal consistency only; it cannot fail against anything
   external and does not prove Canton enforced it.

3. RECOMPUTATION, Close_C Pedersen. Recompute
   `pedersenCommit([encodeScalar(v), ..., encodeScalar(s), to32BE(rho)], 0x4343)`
   with the vendored bb.js, compare to `close-c.json`. Proves the committed point
   is the correct Barretenberg Grumpkin Pedersen commitment of the run's public
   scalars under the published blind, on the reserved 0x4343 basis. Does not prove
   this value was on the ledger (it was not, the ledger stored a placeholder),
   nor anything about a secret witness.

4. RECOMPUTATION, openA equals SHA3-256(Sigma). Recompute SHA3-256 over the
   canonical serialization in `sigma.json`, compare. Proves openA is the literal
   SHA3-256 binding of the canonical public Sigma fields. Does not prove the
   ledger stored this digest (it stored a placeholder), nor that the envelope was
   signed.

5. CONSISTENCY, cross-artifact and metadata. Proves four things by comparison,
   with no derivation: the nullifier in `nullifier.json` equals the one in
   `sigma.json` (bound at both sites); `sigma.json` fields.closeC equals
   `close-c.json` point; `sigma.json` fields.kScalar equals `close-c.json`
   scalars.k; and the self-describing metadata in `close-c.json` (hashIndex,
   scalarModulus, encoding, inputOrder, serialization) and `sigma.json` (encoding,
   fieldOrder) each equal the constants the verifier hardcodes. A mismatch on any
   of these fails the check. The nullifier is a chosen label in this run, not a
   cryptographically derived value.

6. STRUCTURAL, negative-controls. For each of the five rows, the recorded text
   contains `Check N failed:` for its own N and nothing else's. Proves the
   captured rejection texts each name their own check. Does not prove the verifier
   re-ran Canton; these are attested references.

## Attested, not recomputed

Checks 0 and 1 make the whole bundle tamper evident. Separately, the following
artifacts are not recomputed by any RECOMPUTATION check. Their values are taken
as asserted by the signer; the verifier confirms only that they were signed and,
where noted, internally consistent.

- `run.json` (run metadata and party ids): bound by nothing but the signature.
- `ledger-refs.json` (offsets, update id, command id, contract ids): bound by
  nothing but the signature.
- `destination-context.json`: only its internal consistency is checked (check 2).
- `nullifier.json`: only its agreement with Sigma is checked (check 5).
- `negative-controls.json`: only the structural shape of each row (check 6).

State it plainly: `run.json` and `ledger-refs.json` carry the whole provenance
(package id, parties, offsets, contract ids) and the verifier does not recompute
them or check them against the live ledger. Their correctness rests entirely on
trusting the signer and matching the out-of-band fingerprint. Before the manifest
was signed, this was exploitable directly (regenerate the manifest and the
verifier accepts anything in these files); the signature is what now binds them,
to the signer and no further.

## Cross-checked metadata and linkage

`close-c.json` and `sigma.json` carry self-describing metadata: in close-c.json
hashIndex, scalarModulus, encoding, inputOrder, and serialization; in sigma.json
encoding and fieldOrder. The verifier never computes from these fields. It
computes from its own hardcoded constants, so a forger cannot steer a
recomputation by editing an artifact's metadata. Check 5 additionally asserts that
each of these fields equals the verifier's constant, and asserts the cross-artifact
links sigma.fields.closeC equals close-c.json point and sigma.fields.kScalar
equals close-c.json scalars.k. Any mismatch fails check 5.

What this does and does not do: it catches metadata that lies about the method,
and it catches an internally inconsistent bundle, for example one where
sigma.fields.closeC does not match the committed point. It does not stop a
competent forger who keeps everything self-consistent and re-signs with their own
key; that forgery still passes and is caught only by the fingerprint mismatch
(forgery C above). The metadata assertions are exact string comparisons on the
encoding and serialization descriptions, so rewording those strings, even
harmlessly, fails check 5; that strictness is intentional, the strings are pinned.

## bb.js

The Pedersen recompute in check 3 runs inside `@aztec/bb.js` version
`5.0.0-nightly.20260515`. This is a dated nightly, not a stable release. The
verifier loads the WebAssembly module
`vendor/node_modules/@aztec/bb.js/dest/node/barretenberg_wasm/barretenberg-threads.wasm.gz`,
whose SHA-256 is
`5409ac9688c9a6c882b8cd5909fd866a084f24b04a409eb6b71bdf364f427867`. Check 3's
cryptography executes inside that precompiled WebAssembly, so a reviewer who
wants independence from this binary should recompute the commitment with their
own Barretenberg build rather than trusting the vendored one.

`vendor/.../crs/net_crs.js` contains network code that downloads a structured
reference string. It is never called: check 3 initializes Barretenberg with
`skipSrsInit: true` (Pedersen needs no SRS), which the offline runs confirm. The
file is retained only because the vendored bb.js statically imports it in its
module graph (removing it breaks the import); it is unreachable dormant code, not
a live network path.

## Claim limits

- Of the five Cert Seal Rail checks, three are ledger native (Check 3 verifying
  key registry admissibility, Check 4 nullifier uniqueness via the sharded
  NullifierShard.Consume, Check 5 destination context recompute and compare) and
  two are verified off-ledger and asserted at the gate (Check 1 Merkle inclusion,
  Check 2 signature and proof of possession). This bundle does not reproduce
  Checks 1 and 2; there are no Merkle roots or signatures in this run to verify,
  and it fabricates none.
- The verifier proves the artifacts it recomputes and the manifest signature. It
  does not prove Canton's enforcement. The ledger references point at that
  evidence, they do not reproduce it.
- Close_C and openA are computed off-ledger from the run's public scalars. In
  this run the ledger stored placeholder values for these fields, so the values
  here are not Canton read backs. The `piClose`, `sigSrc`, `sigCust`, and
  `sigDst` fields inside Sigma are the run's asserted-in gate tokens, not real
  inclusion proofs or signatures.
- This bundle publishes all six commitment inputs, the five scalars and the blind
  rho, in `close-c.json`, because independent recomputation of the Pedersen point
  requires them. The Close_C commitment in this bundle therefore provides no
  hiding: anyone can open it. The rho here was generated at bundle build time and
  the scalars are demo labels from a test-network run, so nothing confidential is
  disclosed. This bundle demonstrates the commitment's construction and
  reproducibility, not its confidentiality properties.
- Unlinkability is by construction, not a tested property, and in this run it is
  not even structurally clean. The customer alice, the destination authority
  zvCanton, and the fresh Canton pseudonym aliceB all share the same participant
  fingerprint `1220e6a713bd...270951`, because all three were hosted on the
  app-user participant. The fresh pseudonym is therefore trivially linkable to the
  source customer by participant fingerprint. Only zvBase carries a different
  fingerprint.
- The topology statement is: close rail on one participant, open rail on the
  other.
- The proving system is Noir / UltraHONK.
- The commitment basis is Barretenberg's Grumpkin basis via bb.js. No other
  derivation is claimed. This is the single transfer cert seal path: Close_C
  commits six inputs (v, f, k, t, s, rho) with no obligation term.
