npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

sigil-protocol-verifier

v3.0.0

Published

Reference verifier for the Sigil Protocol v1.1. Independent implementation per the published specification.

Readme

sigil-protocol-verifier

Reference verifier for Sigil Protocol v1.1 (which amends v1.0). Independent of the Sigil platform — built from the spec, with node-forge as the only runtime dependency.

Install

npm install -g sigil-protocol-verifier

Use

sigil-verify <path-to-sealed.pdf>     # verify a sealed PDF (§5)
sigil-verify chain <path-to.json>     # verify a served load bundle (§6.3, §10)
    [--expect-load <id>]              #   the load id YOU asked for (§6.3.8 point 7)
    [--expect-root <hex>]             #   the anchor root YOU fetched (§11.9)
    [--seal-record <base-url>]        #   perform the §11.8.1 lookup (off by default)
sigil-verify --version                # protocol versions implemented (§9)

--version answers the §9 conformance requirement that a verifier advertise the maximum protocol version it implements. It prints JSON, because the caller is as likely to be a script deciding whether this binary can verify a v1.1 bundle as it is to be a human:

{ "protocolVersion": "1.1", "supportedProtocolVersions": ["1.0", "1.1"] }

This is not the package version and the two will diverge. The package gets patch releases that change no protocol behaviour. Read protocolVersion (or the exported PROTOCOL_VERSION / SUPPORTED_PROTOCOL_VERSIONS) to learn what spec this implements — never the package version. They already differ.

Both print a JSON report and exit 0 on a clean verify, 1 on a bad one, 2 on a usage or I/O error — so a shell can branch on the exit code without parsing anything. The bare-PDF form is unchanged from 1.0.x.

chain is the whole diligence check in one scriptable step: it recomputes the v2 load chain, opens any escrowed disclosures, and confirms the head is included under an anchor root, with Sigil out of the loop. The document it reads is the JSON a Sigil API serves:

{
  "loadId": "load_01H...",
  "entries": [ /* the v2 chain */ ],
  "openings": [ /* optional, index-aligned escrowed disclosures */ ],
  "anchor": { "head": {...}, "proof": [...], "root": "..." },
  "expectedRoot": "the operator's own claim about the root"
}

Only entries is required.

The two values you supply YOURSELF (§6.3.8 point 7, §11.9)

sigil-verify chain bundle.json --expect-load <the load id you asked for>
sigil-verify chain bundle.json --expect-root <the root you fetched yourself>

These are the only inputs that can corroborate anything, and it is worth being precise about why. Everything else in the document arrived from the party you are checking. A value that arrived with the artifact cannot corroborate the artifact, so:

  • --expect-root is the root you obtained out of band, from the anchor feed or by parsing the RFC-3161 token. Only this sets rootCorroborated: true. The envelope's own expectedRoot member cannot and never could: the operator agreeing with itself is not evidence. If it disagrees with the served anchor root the bundle is BROKEN, because that is the operator contradicting itself.
  • --expect-load is the load id you asked for. Without it the chain is checked only against the bundle's self-asserted loadId, which a transplant rewrites too, so an internally perfect document can be a real chain re-sealed onto a different load.

rootCorroborated: false is not a failure. It means you hold an inclusion claim rather than an external-timestamp claim, which is a real claim and a weaker one.

This section replaced text that was wrong, 2026-07-27. Earlier releases of this README told you to put the root you trust into the envelope's expectedRoot and said that upgraded the result to "the root you trust". That is the defect clean-room run 4 found in all three verifiers: a bundle whose envelope root equalled its own served anchor root reported rootCorroborated: true, so the protocol's strongest guarantee was satisfiable by the operator alone. §11.9 closed it. Published 2.0.0 does not implement §11.9 and rejects --expect-root as an unknown argument; check what you installed with npm ls sigil-protocol-verifier.

Programmatic

import { verifyBundle, verifyLoadChainV2, verifyAnchorInclusion } from 'sigil-protocol-verifier';

verifyBundle(doc); // what `sigil-verify chain` runs
verifyLoadChainV2(served.entries, 'load_01HQZ...'); // §6.3.6 — pass the load id you asked for
verifyAnchorInclusion(head, proof, publishedRoot); // §10.4

Payload roots and selective disclosure (§6.4)

payloadRoot is a per-field Merkle root, not a whole-payload digest. Its canonical form landed in 1.3.0, so this package can now produce a root and check a single-field disclosure against a sealed one — the half §6.3.4 has advertised since v1.1 shipped:

import {
  computePayloadRoot,
  payloadFieldOpens,
  payloadFieldProof,
  payloadLeaves,
  verifyPayloadField,
  MAX_PAYLOAD_DEPTH,
  MAX_PAYLOAD_LEAVES,
} from 'sigil-protocol-verifier';

computePayloadRoot({ agreedRateCents: 400000, currency: 'USD' }); // §6.4 — the sealed root
payloadLeaves(payload); // §6.4.6/§6.4.7 — the canonical sorted leaf set
payloadFieldProof(payload, ['agreedRateCents']); // §6.4.9.2 — { disclosure, proof }
verifyPayloadField(disclosure, proof, sealedRoot); // §6.4.9 — true / false, never throws
payloadFieldOpens(leafHashes, ['currency'], 'USD'); // §6.4.9 — candidate membership

Things worth knowing before you rely on it:

  • A disclosure that cannot be encoded returns false, it does not throw. A non-terminal value or a number outside §6.4.2's band is "not proven", never an exception a caller might mistake for something else. The leaf is always recomputed from (path, value); a caller-supplied digest is never accepted, because the fold would happily carry an interior node to the root.
  • computePayloadRoot and payloadLeafHash DO throw, deliberately. §6.4.2's accepted band is every safe integer plus non-integral magnitudes in [1e-4, 1e16) — exactly the window where JavaScript's String(n) and Python's repr(n) agree digit for digit. Outside it the two emit different forms (0.00001 vs 1e-05), so the value is refused rather than committed to a digest another conforming implementation would call BROKEN. Carry such a value as a string; money should stay integer cents.
  • A path step's JSON type is load-bearing. A string step is an object key and a number step is an array index, so ['a', 0] and ['a', '0'] name different leaves. Never coerce one into the other. A number step must be a non-negative integral binary64 within the safe-integer range (§6.4.9.1); 0.5, -1 and 1e21 name no leaf and are refused. 1 and 1.0 are the same index.
  • A string with no UTF-8 encoding is REFUSED, never substituted (§6.4.1.1). Node's encoder turns an unpaired surrogate into U+FFFD silently, which collides a payload carrying U+D800 with one carrying U+FFFD onto a single root, ties §6.4.7's total order for two distinct keys, and lets a genuine field proof authenticate a value that was never sealed. Both keys and values are checked, at every depth; U+FFFD itself and well-formed astral pairs are accepted as normal.
  • Two caps, and they are not redundant. MAX_PAYLOAD_LEAVES is 8192, counted per leaf collected rather than against any one array's length. MAX_PAYLOAD_DEPTH is 64, checked on descent — the leaf cap cannot see a 1500-deep payload, because that payload has exactly one leaf.
  • A malformed proof step returns false, it does not throw, and siblingIsRight must be an actual boolean. Truthiness coercion made this implementation and the Python one return opposite verdicts on identical bytes (§6.4.9).

sigil-verify chain is unchanged: it does not recompute payloadRoot, because a chain walk treats it as an opaque 64-hex field inside the §6.3.4 preimage (§1).

What it verifies

  • §3 hash chain (programmatic API).
  • §4 canonical payload form.
  • §5 sealed PDF — real PAdES-B-B signature verification: /ByteRange extraction, the signed messageDigest attribute checked against the SHA-256 of the signed bytes, and the CMS SignerInfo signature verified over the DER re-encoding of the SignedAttributes (node-forge parses the ASN.1; node:crypto performs the signature check). A single altered byte returns TAMPERED, never VALID.
  • §6.3 v2 load chain — length-prefixed, domain-separated entry hashes; commitment binding for actor, geo, and the device-attestation blob; opening verification; and rejection of any entry whose version is not 2.
  • §6.4 payload roots — the full canonical form: number encoding and its accepted band, path encoding, the six terminal type tags, the absent-key rule, the 8192-leaf and 64-step caps, the UTF-8 encodability rule, UTF-8 leaf ordering, the fold, and selective-disclosure proofs including the self-sibling case at odd interior levels.
  • §10 external chain-head anchoring — Merkle inclusion against a published checkpoint root, with the leaf recomputed from the head rather than trusted.

Recognizing a document (§11.8) — off by default

VALID from this verifier means the signature is sound, not Sigil sealed this. Anyone can self-sign a PDF, and it will verify against the certificate embedded beside it. v1.0 §8 always required status = UNKNOWN for a document with "no matching public seal record" — but that phrase was defined nowhere, so until v1.1 §11.8 the rule was implementable by the operator and by nobody else.

Pass a base URL to ask:

npx sigil-verify sealed.pdf --seal-record https://sealedby.com

The verifier then fetches {base}/seals/{sha256} — one unauthenticated GET, the only network request this package ever makes — and applies §11.8.3:

| recognized | meaning | | ------------ | ------------------------------------------------------- | | null | no lookup was performed. This verifier did not ask. | | false | asked, and the operator has no record of this document | | true | asked, and the operator sealed it |

null and false are different claims and are never collapsed. Neither is a failed lookup: if the operator is unreachable or answers badly, the command exits 2 and prints nothing as a verdict, because "I could not ask" must not be reported as "the answer was no".

TAMPERED outranks a recognition miss. Editing a sealed document is exactly what makes its digest miss every record, so a tampered document would otherwise be demoted to not recognized — reading as nothing-to-see-here on the one artifact where something did happen.

signatureValid is reported separately and always, so the cryptographic finding survives an UNKNOWN verdict.

Trust scope, and why a bare verify is now UNKNOWN

The prod signing certificate is self-issued ("Sigil Document Signing"). That one fact decides this whole section: there is no CA chain to validate against, and an impostor self-issues a certificate carrying the identical subject name. A PDF signed by a certificate reading Totally Not Sigil, Inc. verified as VALID from all three verifiers until 2026-07-28, and nothing about that was wrong as cryptography — the signature really did verify against the certificate sitting beside it. The defect was that VALID is read as an endorsement.

v1.0 §5.2 step 4 was supposed to prevent it, by confirming the signer against "a configured trust policy". It said to match the subject CN, and shipped a permissive default. Matching a name an attacker chooses is not a check, and a permissive default is not a policy.

So, per v1.1 §11.8.7: tell this verifier which certificate you trust, by fingerprint.

npx sigil-verify sealed.pdf --trust-cert <sha256-of-the-DER-certificate>

Repeatable — pass the retired certificate too, because a document sealed under it stays genuine after a rotation. openssl x509 -in cert.pem -noout -fingerprint -sha256 prints it, and the uppercase colon-separated spelling is accepted as-is.

| signerTrusted | meaning | | --------------- | -------------------------------------------------------- | | null | no policy was supplied. This verifier was not asked. | | false | a policy was supplied and this certificate is not in it | | true | a policy was supplied and this certificate is in it |

With no --trust-cert and no --seal-record, a perfectly sound signature is UNKNOWN and the command exits 1. Nothing has vouched for the signer, so there is no basis for an endorsement. This is a deliberate change to what the bare invocation returns; signatureValid stays true and the detail names which question went unanswered, so the cryptographic finding is reported rather than lost.

The two vouchers are alternatives. A seal-record lookup answering known: true is the operator saying I sealed this document, which is stronger than this is the certificate I told you about, and it reaches VALID on its own. A trust anchor that DIFFERS outranks either — you named a certificate and this is not it.

Get the fingerprint out of band. This report includes signerCertSha256 so you can COMPARE it against a pin you already hold. Reading it out of the report and handing it straight back is the document corroborating itself, which is the same defect §11.9 records for anchor roots.

VALID still means the document is byte-unaltered since signing by that key — the signature is verified against the embedded certificate. It does not establish CA / AATL certificate-chain trust; that is out of scope for this reference verifier (see AATL).

verifyChainAnchored reports valid: true only when the anchor evidence's head is the presented chain's head. Chain validity and inclusion validity are each independently true of unrelated artifacts, so the pairing is what the claim rests on. Anchor checkpoints fold org chain heads; a v2 load chain reaches the same guarantee transitively via the LOAD_EVENT_SEALED cross-reference (§6.2).

An anchor root is only as good as its publication and timestamp. This verifier checks inclusion under a root you supply; obtaining that root from the public feed, and validating its RFC-3161 token, is the caller's responsibility.

What it does not yet verify

  • §5 PAdES profile-specific constraints beyond B-B (LTV, LTA timestamping), and certificate-chain / AATL trust. This reference verifier proves a Sigil seal's signature is intact and the document unmodified since signing; full PAdES-B-LTA conformance is a planned add-on.
  • §7 capability tokens (verifier integration shipped with the platform in S10.T4). Standalone offline verification needs a standardised JWKS URL, which v1.1 does not introduce — it remains implementation-defined.
  • The RFC-3161 token over an anchor root (see the trust-scope note above).

Test

npm test

Cross-language parity against the Python reference is enforced by the Compliance Test Suite, which runs both over the same corpus and fails on any divergence.