@dtfoundry/trustable-verify
v0.5.0
Published
Offline verifier for the Trustable portable verify package. Checks a credential's integrity, its issuer's authorship and its revocation state with no network access.
Readme
@dtfoundry/trustable-verify
Verify a Trustable from its portable verify package, with no network access of any kind.
A Trustable is normally checked by asking the Platform that minted it. That works while both sides share a deployment and the minting tenant is known. It does not work for a counterparty in another organisation, an auditor reading an archive years later, or anyone on a machine with no route out.
The verify package is the evidence made portable: the credential, the issuer's signed key event log, and the registry's history of that credential, exported as one document and archived beside the business artifact it attests. This library is the checker for it.
npm install @dtfoundry/trustable-verifyimport { readFileSync } from "node:fs";
import { verifyPackage } from "@dtfoundry/trustable-verify";
const pkg = JSON.parse(readFileSync("contract.keri.json", "utf8"));
const result = verifyPackage(pkg);
result.isValid; // false unless every check below holds
result.status; // verified | revoked | suspended | invalid
result.authenticity.established; // was the issuer's authorship proven
result.as_of; // the moment this answer is aboutWhat it checks
Integrity. The credential's identifier is recomputed over its own bytes. A self-addressing identifier is a digest of the field that carries it, so the field is replaced by a dummy of its own length before hashing — which means the answer cannot be reproduced from a database projection, and any byte changed anywhere breaks it.
Authorship. This is the part that matters, and it does not work the way people expect. There is no signature on the credential to check. What binds a credential to its issuer is a chain:
- the credential's bytes hash to the identifier it claims;
- the registry's issuance event names that credential and hashes to its own identifier;
- that identifier appears in a seal inside an event in the issuer's key event log;
- that key event is signed by the keys that were in force when it was written, established by walking the log from its inception through every rotation.
Registry events carry no signatures of their own. Break step 3 and anyone could
mint an issuance for a credential they do not control — which is exactly what
fixtures/unanchored-issuance.json is, and the suite proves it is rejected.
Revocation. Read from the last anchored registry event. An event the issuer never sealed does not get to say whether a credential stands.
Approval. A credential names the signature policy it was issued under, and that policy is inside the envelope the identifier is computed over — so a credential that required three approvals cannot be edited to look like one that required none.
Each approval is a credential of its own, minted by the signer, and it earns its
place in the count the same way the genesis credential earns its verdict: its
identifier recomputed from its own bytes, an e.genesis.n edge back to the
credential being verified, and its issuance sealed into the approving signer's
key event log — their log, not the issuer's, because they are the one whose
approval it is. An approval the signer later revoked does not count, and neither
does one from outside the set the policy names.
Thresholds are counted per round and per signer: two one-signer rounds are not a two-signer round, and the same approval presented twice is one signer. Where the count is not reached, the report says which requirement went unmet and why each uncounted approval was not counted. Unproven is a refusal, not a pass.
What it does not check
Witness receipts. They travel in the key event log and are parsed past. A witness threshold is a different trust question, and no relying party is currently asking it of an archived package.
Schema compliance. Reported as degraded. Carrying the schema document is
what makes the check possible later; it is not the check.
The roles a policy demands. required_roles names who a signer has to be,
and an approval credential carries their identifier rather than their office —
so a threshold met by named identifiers is as far as the bytes reach. A policy
demanding roles is reported as degraded, not waved through on a count. A
credential minted before the policy was bound cannot speak to any of this, and
is reported as unstated rather than as compliant.
Whether the credential is valid now. It cannot be, and that is the whole
point of as_of. An offline answer describes the moment the package was made.
A credential revoked after that will still read as issued here, because the
evidence of the revocation is not in the file. Offline means valid as of
package time, not currently valid. Where a live check is possible, make one.
Honest failure
isValid is true only when authorship was proven, not merely
un-contradicted. Recomputing an identifier shows a credential hashes to itself,
which anyone can arrange for a credential they minted — so a package whose key
log is missing or unverifiable comes back isValid: false with the reason
stated, rather than a "verified" that would mean only that the file is
self-consistent.
The report shares its shape with the Platform's hosted verification — the same steps, the same statuses, and the same weighted trust score down to the band and the counts — so a consumer does not learn a second vocabulary to read an offline answer, and two reports on the same evidence do not disagree about the number.
One consequence is worth stating rather than discovering: witness receipts and
watcher status are optional checks no offline verifier can run, and the band
only reaches high when an optional check passes. A package that verifies
completely therefore bands medium at 70. That is the honest ceiling for an
offline answer, not a penalty.
Fixtures
fixtures/ holds real packages minted with keripy against a throwaway
identifier. They are checked in on purpose: a verifier whose tests build their
own inputs proves only that it agrees with itself.
Compatibility
Implements package schema_version: 1. A package declaring anything else is
refused rather than interpreted. Node 20+; no runtime dependency beyond
@noble/hashes and @noble/curves, and nothing platform-specific — it runs in
a browser as readily as on a server.
Licence
Apache-2.0.
