@fts-tas/verify
v1.6.0
Published
Offline verifier for TAS composite ACDCs. Verifies issuer signatures, ACDC edge chaining, component integrity, signed status lists and on-chain anchor commitments without contacting TAS.
Maintainers
Readme
@fts-tas/verify
Offline verifier for TAS composite ACDCs. Verifies component integrity, issuer signature, issuer trust, and on-chain anchor commitment without contacting TAS.
This package exists so DIFO - and any other party consuming a TAS-assembled composite credential - can validate it independently. TAS can go dark and verification still works.
Install
npm i @fts-tas/verifyUsage
import { verifyComposite } from "@fts-tas/verify";
const result = await verifyComposite(bundle, {
trustedIssuers: new Set(["EBfxc4dBtLk3a..."]),
// Supply the issuer's KERI key event log and the verifier derives the
// signing key itself - no key needs pinning out of band.
resolveKel: async (aid) =>
fetch(`https://your-watcher.example/oobi/${aid}`).then((r) => r.text()),
readAnchor: async (proof) => {
// Caller supplies their own on-chain reader - we never phone home.
// TAS notarises to Polygon PoS. Any other chain a caller wishes to read
// is the caller's own arrangement.
if (proof.chain === "evm") return readEvmCommitment(proof.txId);
return null;
},
});
if (result.verdict === "valid") accept(bundle);Identifiers and single credentials
import { verifyAid, verifyCredential } from "@fts-tas/verify";
// Replays the key event log from inception and binds it to the prefix.
const aidResult = await verifyAid({ aid, kel });
// Recomputes the credential SAID and checks the issuer signature.
const credResult = await verifyCredential({ acdc, raw, signature, issuerKel });From the command line, with the bundle fetched for you:
npx @fts-tas/verify --aid E... --api https://api.trustanchorservice.com/v1
npx @fts-tas/verify --said E... --offline --api https://api-sandbox.trustanchorservice.com/v1
npx @fts-tas/verify bundle.json # composite, identifier or credentialThe offline bundle endpoint used by --aid and --offline is public. No key,
account or relationship with TAS is needed to run these checks. --anon is still
accepted for deployments that place the endpoint behind a gateway.
Checking the TAS root identifier yourself
This is the shortest independent check of the identifier that anchors the service. It contacts the public endpoint, replays the whole key event log and verifies the witness receipts on every event.
npx @fts-tas/verify \
--aid EOSmZFUzeh813BgwYogwpdgfb0NthqWTPNcExx_bo0I2 \
--api https://api.trustanchorservice.com/v1The key event log can also be fetched straight from any of the four witnesses
and checked with --kel, which removes the TAS API from the path entirely:
curl -s https://wit2.keri.trustanchorservice.com/oobi/EOSmZFUzeh813BgwYogwpdgfb0NthqWTPNcExx_bo0I2/witness > root.cesr
npx @fts-tas/verify --aid EOSmZFUzeh813BgwYogwpdgfb0NthqWTPNcExx_bo0I2 --kel root.cesrExit codes
| Code | Meaning | | --- | --- | | 0 | valid | | 1 | invalid | | 2 | unknown, including a bundle that could not be fetched | | 3 | usage or input error, including a local file that cannot be read or parsed |
A verdict is never 0 by accident. A missing argument, an unreadable file and an unreachable endpoint are all reported and all non-zero.
What gets checked
| Check | How | | --- | --- | | Component integrity | sha256 of each raw component equals the manifest hash | | Key event log | CESR stream replayed from inception: every SAID recomputed with a bundled BLAKE3, sequence numbers, prior digests and controller signatures | | Issuer signature | Ed25519 over the exact canonical payload, against the key state derived from the log (or a pinned key) | | Issuer trust | optional caller-supplied allow list | | Anchor | on-chain commitment equals the composite SAID, read by the caller's own node |
verdict is valid only when every applicable check passes, unknown when
no key material was supplied at all, and invalid otherwise. There is no
silent pass.
Dependencies
None. BLAKE3 is bundled; Ed25519 uses Web Crypto with a node:crypto
fallback. Works in Deno, Node 20+, Bun and modern browsers.
Bundle shape
A composite bundle is the JSON returned by the TAS
assemble-composite-acdc function plus the raw component credentials the
verifier needs to recompute hashes:
{
"acdc": { "d": "...", "i": "...", "s": "...", "a": {}, "e": { "components": [...] } },
"signature": "<CESR signature>",
"components": [ { "raw": "<original credential>", "entry": { ... } } ],
"anchor": { "chain": "cardano", "network": "mainnet", "txId": "...", "commitment": "..." }
}Status
1.3.1. Component hashes, KEL replay, issuer signature, trust list and
anchor checks are all implemented and produce the same verdict as the TAS
kel-replay-verify service running server side.
Spec
See SPEC.md for the wire format and verification algorithm.
License
Apache-2.0.
What is checked offline, and what is not
Verifying an identifier bundle with no network reports four things separately, and the separation is deliberate.
| Check | What a pass means | | --- | --- | | key event log | Every event's digest recomputes, the log is linked and in sequence, each rotation honours its prior commitment, and the controller signatures meet the signing threshold in force. | | prefix binding | The log self-certifies the identifier the bundle claims. | | witness agreement | The witness receipts attached to each event verify against the witness list that event declares, and meet the witness threshold. | | trust list | The identifier is on the allow list the caller supplied. |
Witness receipts have been verified since 1.4.0. Earlier versions parsed past the receipt bytes without checking them, which meant a bundle with altered receipts and a bundle with sound receipts produced the same result. If you are pinned to 1.3.x, upgrade.
Witness agreement prints NOT ESTABLISHED rather than FAIL when a log
simply carries no receipts, because that is an unmeasured condition and not
evidence of a fault. A receipt that is present and does not verify does fail
the verdict.
What this does not do
Verifying receipts shows that the named witnesses signed these exact event bytes. It does not show that no conflicting version of the log was served to someone else. Establishing that means comparing what different parties were served over time, which is a watcher function and is not part of this package. Nothing this package outputs is duplicity detection.
Revocation is also out of scope offline: the question answered is whether the record was validly issued, not whether it is still current.
