Cert Seal on Canton

A compliance rail you can verify yourself.

We ran it live across two Canton participants: closed on one rail, sealed, reopened under a fresh identity, with five attacks each rejected at its own check on the ledger. The kit carries those rejections as recorded evidence, and the cryptography you recompute yourself, offline, in minutes.

2
Canton participants, one rail each
5
attacks rejected in the run, recorded in the kit
7
checks you run offline, three of them recomputations

What Cert Seal does

Move a position between venues without re-disclosing the counterparty, and without re-running the compliance checks it already passed. Built for overnight and intraday repo and collateral mobility, where the same position must be recognized in more than one place, fast, without leaking who holds it.

Forward-unlinkable by design

Built, per our patent, so each party sees only its own leg of a transfer while the full chain stays auditable to authorized parties.

Auditable for the full horizon

Banking-grade compliance attestations, verifiable today and decades from now.

Quantum-anticipating

ML-DSA signatures on the validator node, per NIST FIPS 204.

The attestation format, the Passport portability protocol, and the composite signature design are shared under NDA. What you see below is one run, fully open, so you can judge the engineering before we go further.

New to this? See how the compliance gate works →

Watch it run

The verifier executing its seven checks against the evidence bundle.

node verify.js . zerovaultid-certseal-rung6
Press Run the verifier to begin.
A dramatization. The real proof is the kit you run yourself, below.
A note on this demo. It runs on our page, so it will always pass. A demo the vendor controls is not evidence. The result that matters is the one you produce with the kit, which is why the steps below begin with your own tools.

Verify it yourself

One signed archive, one standalone verifier, fully offline. No Canton, no Docker, no account. You establish authenticity with your own tools before running anything we shipped.

  1. Fingerprint the signing key yourself, with your own openssl.
  2. Match it against the fingerprint we publish at a fixed public location. If they differ, stop.
  3. Verify the manifest signature with your own openssl.
  4. Confirm the verifier's own hash is in the now-trusted manifest.
  5. Run the verifier. It recomputes the cryptography and reports pass or fail.
Why the order matters. Any fingerprint the verifier prints is a convenience, not the source of trust: it ships inside the bundle, so a forger who re-signs could make it print anything. Steps 1 through 4 use your own tools and run first.

What the kit publishes

Archivezerovaultid-certseal-rung6.zip  (4510686 bytes, reproducible)
Archive SHA-256fe876ce644321c2f22b3cd2c565b25ea6c5452912dfdc905a100f31b49e3efac
Verifiernode verify.js  (offline, 7 checks)
Authentic signing-key fingerprint, published independently of the bundle
234374010ba7c648e59f26003b7915ce2c70899cd101c770cb046f5d6e3b5286
Published at
github.com/ZeroVaultID-Inc/evidence-signing-keys

Verify the file you received before trusting it. Its SHA-256 is
fe876ce644321c2f22b3cd2c565b25ea6c5452912dfdc905a100f31b49e3efac
On your machine, run sha256sum zerovaultid-certseal-rung6.zip (or shasum -a 256 on macOS) and confirm it matches.

Prefer to talk first? Contact ZeroVaultID and we will send the kit and walk your engineers through it.

Download it, and check it in transit

Confirm the archive you received is the archive ZeroVaultID published, before you open it. Everything on this page was executed against the archive described here.

Filezerovaultid-certseal-rung6.zip
Size4510686 bytes
Contents430 files, all under a single bundle/ directory
SHA-256fe876ce644321c2f22b3cd2c565b25ea6c5452912dfdc905a100f31b49e3efac

Confirm the digest. Either command, run in the directory holding the download.

sha256sum zerovaultid-certseal-rung6.zip
shasum -a 256 zerovaultid-certseal-rung6.zip

Both print the same line:

fe876ce644321c2f22b3cd2c565b25ea6c5452912dfdc905a100f31b49e3efac  zerovaultid-certseal-rung6.zip

If that digest does not match, stop. You do not have the archive published here, and nothing further on this page applies to the file you are holding.

Extract it. Any standard zip tool will extract this archive. The form below is the one we ran end to end for this page.

python3 -m zipfile -e zerovaultid-certseal-rung6.zip .

That creates bundle/ beside the archive. Every command in the next three sections runs from inside bundle/.

On the two file counts. The archive holds 430 files. The verifier later reports 428, because it hashes every file except manifest.json and manifest.sig, which are the manifest and its signature. Both numbers are correct; they count different things.

Establish authenticity before you run anything we shipped

These five steps are reproduced unchanged from the bundle's own README. The order is load-bearing: steps 1 through 4 use only your tools, and none of them executes our code.

Why the verifier cannot vouch for itself. 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.

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.

  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. On the published archive it prints:

    SHA2-256(stdin)= 234374010ba7c648e59f26003b7915ce2c70899cd101c770cb046f5d6e3b5286
  2. Match that value against the authentic fingerprint published at the anchor
    https://github.com/ZeroVaultID-Inc/evidence-signing-keys/blob/9a3ef7eed30481a4010441829af5a0a85526fca5/README.md

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

    The link above pins one immutable commit and will never change, which is what makes it trustworthy and also what makes it blind: a key revoked after that commit was written cannot appear in it. Before relying on a match, open the repository's default branch and read the current key status and the revoked-key list there.

  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.

    Signature Verified Successfully

    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. Confirm the verify.js you are about to run is the one the manifest covers

    manifest.json is now trusted.

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

    The hex in the manifest line must equal the sha256sum output. On the published archive both are e9fa708bf48ae88354d60ebc379dacb277d2f425b64895aeb6b5206c6114cdb4.

  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.

Run it

Step 5 in full. Node 20, no network, no Canton, no account. This is the complete output of the published archive, byte for byte, with nothing trimmed.

node verify.js; echo "exit $?"
=== ZeroVaultID Rung 6 bundle verifier ===
[0] [SIGNATURE] manifest signature ... PASS
        signature valid over manifest.json; bundled key fp=234374010ba7c648e59f26003b7915ce2c70899cd101c770cb046f5d6e3b5286. 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/9a3ef7eed30481a4010441829af5a0a85526fca5/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=234374010ba7c648e59f26003b7915ce2c70899cd101c770cb046f5d6e3b5286)
exit 0

Seven checks, exit code 0. The check tags are not decoration. RECOMPUTATION means a value was re-derived from published inputs and compared, and only checks 1, 3 and 4 do that. CONSISTENCY, checks 2 and 5, compares stored values against each other or against the verifier's own hardcoded constants, with no derivation. STRUCTURAL, check 6, inspects the shape of a stored value. SIGNATURE, check 0, is the manifest signature and it fails closed: if it does not verify, the verifier aborts and checks 1 through 6 never run.

Now break it

A verifier that only ever prints PASS tells you nothing. Damage the evidence deliberately and watch named checks fail. Every result below was produced by running the exact command shown.

Work on an extracted copy, never on the archive. Each recipe begins by extracting a throwaway copy into break/, mutates that copy, runs the verifier, and leaves your archive and any other extraction untouched. Delete break/ when you are done.

The wrapper. Identical for every recipe, run from the directory holding the archive. Substitute one mutation line from the recipes below.

rm -rf break && python3 -m zipfile -e zerovaultid-certseal-rung6.zip break
cd break/bundle
# apply one mutation from a recipe below
node verify.js; echo "exit $?"
cd ../..

Recipes that break a recomputation

1. Change a committed scalar

artifacts/close-c.json

perl -pi -e 's/"k": "vk-cr05\+cr09"/"k": "vk-TAMPERED"/' artifacts/close-c.json

Fails check 1 (the file no longer matches its hash in the signed manifest), check 3 (the Pedersen commitment recomputes to a different point), and check 5 (sigma.kScalar no longer equals close-c.scalars.k). RESULT: 4/7 PASS, exit 1.

Run inside a fresh extracted copy, using the wrapper above.

2. Rename the customer inside Sigma

artifacts/sigma.json

perl -pi -e 's/"customer": "Alice-BaseIdentity/"customer": "Mallory-BaseIdentity/' artifacts/sigma.json

Fails check 1 and check 4: openA is the SHA3-256 of the canonical Sigma serialization, so changing any covered field changes the digest. RESULT: 5/7 PASS, exit 1.

Run inside a fresh extracted copy, using the wrapper above.

Recipes that break a consistency or structural check

3. Shift the destination epoch

artifacts/destination-context.json

perl -pi -e 's/"destEpoch": "E1"/"destEpoch": "E2"/' artifacts/destination-context.json

Fails check 1 and check 2: the four components no longer join to the sealed string. RESULT: 5/7 PASS, exit 1.

Run inside a fresh extracted copy, using the wrapper above.

4. Unbind the nullifier from Sigma

artifacts/nullifier.json

perl -pi -e 's/"nullifier": "nu:A:rung6r3b-542e60f5"/"nullifier": "nu:A:REPLAYED"/' artifacts/nullifier.json

Fails check 1 and check 5: the nullifier is recorded at two sites and they must agree. RESULT: 5/7 PASS, exit 1.

Run inside a fresh extracted copy, using the wrapper above.

5. Make a negative control name the wrong check

artifacts/negative-controls.json

perl -pi -e 's/Check 3 failed: verifying key not admissible/Check 4 failed: verifying key not admissible/' artifacts/negative-controls.json

Fails check 1 and check 6, which reports 4/5 rows naming their own check and no other. RESULT: 5/7 PASS, exit 1.

Run inside a fresh extracted copy, using the wrapper above.

Recipes that show the limits of the checks

6. Falsify the run metadata

artifacts/run.json

perl -pi -e 's/"epoch": "E1"/"epoch": "E9"/' artifacts/run.json

Fails check 1 only. RESULT: 6/7 PASS, exit 1. Nothing recomputes this file. Its integrity rests on the manifest hash and the signature over that manifest, and on nothing else. That is worth seeing directly.

Run inside a fresh extracted copy, using the wrapper above.

7. Falsify the ledger references

artifacts/ledger-refs.json

perl -pi -e 's/"offset": 2601/"offset": 9999/' artifacts/ledger-refs.json

Fails check 1 only. RESULT: 6/7 PASS, exit 1. Same limit as recipe 6: the offsets, update id and contract ids carry the run's provenance, and the verifier does not check them against a live ledger.

Run inside a fresh extracted copy, using the wrapper above.

Recipes that trip the signature and stop everything

8. Edit the manifest

manifest.json

perl -pi -e 's/"fileCount": 428/"fileCount": 429/' manifest.json

Fails check 0. exit 1, and the verifier prints RESULT: FAIL at check 0 (signature). Aborting; remaining checks not run. Checks 1 through 6 never execute. This is the fail-closed path.

Run inside a fresh extracted copy, using the wrapper above.

9. Corrupt the signature

manifest.sig

perl -pi -e 's/^(.)/sprintf("%x",(hex($1)+1)%16)/e' manifest.sig

Advances the first hex digit by one. Fails check 0, aborts, exit 1.

Run inside a fresh extracted copy, using the wrapper above.

10. Swap in an attacker's public key

manifest-pubkey.pem

openssl genpkey -algorithm ed25519 -out attacker-key.pem && openssl pkey -in attacker-key.pem -pubout -out manifest-pubkey.pem && rm attacker-key.pem

Fails check 0, aborts, exit 1, and prints a key fingerprint that is not 23437401.... This is the halfway point of the one forgery the signature does not stop. A forger who also regenerates and re-signs manifest.json with that private key produces a bundle that passes all seven checks. The only thing that catches it is step 1 of the section above: your own openssl computes the attacker's fingerprint and it does not match the anchor.

Run inside a fresh extracted copy, using the wrapper above.

The pattern. Check 1 fires on every single-file edit, because the manifest hashes every file and the signature covers the manifest. The additional checks that fire tell you what kind of claim you broke. Two files, run.json and ledger-refs.json, fire check 1 and nothing else, and that is the honest boundary of what this bundle recomputes.

What this bundle does not prove

Reproduced from the bundle's own README, unchanged. Two of the Cert Seal Rail checks at the ledger gate are Check 1, Merkle inclusion, and Check 2, signature and proof of possession.

A correction to the bundle's own README.

The README inside the signed bundle contains one sentence that is wrong. You should read it here rather than discover it yourself.

Under "Claim limits" the README says that two of the Cert Seal Rail checks, Check 1 Merkle inclusion and Check 2 signature and proof of possession, are "verified off-ledger and asserted at the gate". The second half is accurate. The first half is not. Nothing verified those two checks in this run, at either layer. The ledger gate receives a boolean from its caller and asserts that the boolean is true; it does not itself validate an inclusion proof or a signature. The caller supplied true unconditionally, and the four corresponding fields recorded in the evidence, piClose, sigSrc, sigCust and sigDst, are the literal placeholder strings you can read for yourself in artifacts/sigma.json. The accurate position is that Check 1 and Check 2 were asserted in and were verified nowhere.

No published value on this page changes because of this, and none of the seven checks depends on that sentence. We cannot correct it in place: the README is hashed by the manifest and the manifest is covered by the signature, so editing it would invalidate the archive you are checking. The bundle will be re-sealed with corrected text, and this note will name the replacement when it ships.

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.

bundle/README.md, "Claim limits"

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.

bundle/README.md, "Claim limits"

The piClose, sigSrc, sigCust, and sigDst fields inside Sigma are the run's asserted-in gate tokens, not real inclusion proofs or signatures.

bundle/README.md, "Claim limits"

What it proves

Exactly what the evidence supports, and where the edges are.

Independently reviewed. The kit was put through repeated adversarial review, every finding addressed and the artifact re-verified from scratch, before release.