@thedriveai/provenance-verifier
v0.1.2
Published
Verify a Drive AI provenance record with the file, the published keys and the public log: no account, no dependencies, Node 20+ or a browser.
Readme
@thedriveai/provenance-verifier
Check who stands behind a document, without trusting The Drive AI's word for it. No account, no dependencies, Node 20+ or a browser.
npx @thedriveai/provenance-verifier contract.pdf # the file, the published keys, the public log
npx @thedriveai/provenance-verifier contract.pdf --offline # the file and the keys only
npx @thedriveai/provenance-verifier contract.pdf --keys keys.json --jsonimport { verifyFile } from '@thedriveai/provenance-verifier';
const { pdf, log } = await verifyFile(bytes, { keysUrl: 'https://service.thedrive.ai/.well-known/provenance-keys.json' });A record proves origin and integrity: this exact content, made for this person by this agent, approved by this person at this time, and unchanged since. It does not prove the content is true. A file with no record is unverified, not forged.
What is checked
- The signature. The record is a compact JWS (ES256, header
typ: thedrive/provenance+jws,kid), checked against the key set at/.well-known/provenance-keys.json(JSON Web Keys, EC P-256). A key rotated out stays published, so old records keep verifying. A connector token signed under the same curve is never a record: thetypsays what it is. - The binding. Inside a PDF the record sits in a slot of fixed length, uncompressed, named by the document
information dictionary:
/DriveAIProvenanceRange [offset length](the last mention is the newest record) and/DriveAIProvenanceRecord (id). The statement'sbound.valueis SHA-256 of the whole file with that slot left out,bound.exclnames the slot,bound.origthe length of the original bytes. The verifier hashes the file around the slot and compares; one byte changed anywhere, or anything appended, fails. - The approved prefix.
hash.valueis SHA-256 of the bytes the person approved. The record is appended to them as a PDF incremental update, so those bytes are the file's prefix of lengthbound.orig; the verifier checks that too (approvedPrefix). An e-signature already on the document keeps its ByteRange. - The log (unless
--offline). Every signing and every revocation is a leaf of a public, append-only Merkle log (RFC 6962 / RFC 9162).GET /api/v1/public/provenance/log/proof/<recordId>returns inclusion proofs under a signed tree head (typ: thedrive/provenance-sth+jws); the verifier checks the head's signature and the proof, and reports a revocation entry when one exists. A monitor that keeps heads checks/log/consistency/<first>/<second>to see the log only grew.
The statement
Canonical JSON: object keys sorted at every depth, undefined members left out, no whitespace. The JWS payload is
exactly these bytes.
{
"v": 1, "iss": "thedriveapi",
"rid": "<record id, uuid>", "iat": "<ISO 8601 UTC>",
"name": "contract.pdf", "mime": "application/pdf",
"hash": { "alg": "sha256", "value": "<hex of the approved bytes>", "size": 12345 },
"bound": { "alg": "sha256", "value": "<hex of the file around the slot>", "excl": [offset, length], "orig": 12345 },
"prev": "<previous record id>" ,
"subject": { "name": "Uma", "email": "[email protected]" },
"agent": { "kind": "mcp", "client": "Claude" },
"approval": { "at": "<ISO 8601 UTC>", "kind": "confirmation" },
"sources": [ { "name": "invoice-1.pdf", "hash": "<hex>", "claimedBy": "agent" } ],
"note": "final for the client"
}bound is null for a record not inside a file (the registry answers for those by hash). agent.kind is mcp
for an outside assistant acting through The Drive AI's MCP connector (client is its name) or app for the
product itself (client names the part: the agent when null, E-Sign for a completed signature request).
approval.kind is confirmation (the person agreed through the connector), card (in the app) or esign (the
content was approved by the completion of an e-signature request the person sent; the sources then name the
document that was signed). sources are the files the agent said it drew on, checked for existence and hashed at signing: the
agent's claim, not proof they were read. The statement is public by construction: it carries no workspace, file
or user ids and no paths.
The manifest
The JSON in the slot: { "v": 1, "about": "...", "recordId": "<id>", "verify": "<url or null>", "signature":
"<the JWS>", "keys": "<url or null>" }, space-padded to the slot's length. It is also an embedded file named
thedrive-provenance-<id>.json for readers that list attachments.
The log
Leaf of a signing: SHA-256(0x00 || canonical({ "kind": "record", "rid": "<id>", "jws": "<the JWS>" })).
Leaf of a revocation: SHA-256(0x00 || canonical({ "kind": "revoked", "rid", "at", "reason" })), its data in
the clear on the entry. Nodes: SHA-256(0x01 || left || right). The signed tree head is a JWS over
canonical({ size, root, at }). Proof routes return hex.
Keeping heads (a monitor)
A signature shows a record was not altered after it was made, not that the log it is in has not been rewritten. Anyone who keeps a head can tell: run the monitor on a schedule from a machine that is not the log's.
npx @thedriveai/provenance-verifier monitor --state heads.jsonEach visit fetches /.well-known/provenance-log.json, checks the head's signature, and asks
/api/v1/public/provenance/log/consistency/<kept size>/<new size> for a proof that the kept head is a prefix of the
new one (RFC 9162 §2.1.4). A log that shrank, changed an entry, or cannot prove consistency exits 1 and the kept head
is not replaced. checkLogGrowth in the library is the same check for your own tooling.
Results
verifyFile returns { pdf, log }. pdf is { ok: true, statement, kid, manifest, approvedPrefix } or
{ ok: false, reason } with one of no_record, malformed, unknown_key, bad_signature, bad_statement,
slot_mismatch, bytes_changed. log is null when not consulted, { ok: true, head, index, revoked? } or
{ ok: false, reason }.
