Security policy

Generated from the repository's security policy when this page was built, so it cannot drift from the document the project keeps. One section is not reproduced here: Claims discipline.

SignetGate is a security control, so this file states what it defends, what it assumes, and what it does not cover. It points at the detailed records rather than restating them — where this file and the published list of limitations disagree, that list is right.


Read these two first

Two entries in the register are launch blockers. Neither is closed, and no work in this repository has closed them.

1. Verifier credentials are a development stand-in

A verifier signs in with a verifier id and a password. There is no second factor and no device binding, and that session is the sole source of the verifierId written into every receipt. A receipt therefore shows that some holder of that credential approved a payload; it does not show which person did.

Production refuses to boot on password auth unless the operator records, in configuration, that they accept this specific risk, which is a deliberate, self-documenting acceptance rather than a default.

Do not deploy against real payment rails before SSO with enforced MFA. Full entry: entry 1 in the published limitations.

2. What the reviewer sees is bound to what is signed by the server, not by the client

The receipt binds a fingerprint of the payload. The review screen renders values derived from that same payload, by one shared implementation, and the server refuses to serve or to certify a payment whose stored bytes no longer hash to their stored hash.

All of that is enforced on our server. The reviewer's browser does not recompute the fingerprint, so a modified client is outside what any of it reaches. Independent checking is what the offline verifier is for, and it happens after the fact. Full entry: entry 2 in the published limitations.


Threat model

Four adversaries, in the order the design addresses them.

# Adversary Principal mitigations
1 Compromised or prompt-injected agent — submits a fraudulent payee with a matching doctored invoice Server-side document capture; the invoice-versus-extraction diff, which is a real defence only where invoiceGroundTruth.source is STRUCTURED_FEED (register entries 3 and 4)
2 Fatigued verifier — approves reflexively Dwell thresholds and a typed challenge; canary items with planted defects, measured per reviewer
3 Insider verifier — approves a payment and later denies it Identity derived from the authenticated session and never from a request; asymmetric signing; revocable sessions. Bounded by entry 1
4 Post-hoc disputant — contests what was approved or shown The receipt binds payloadHash and documentSha256; the offline verifier answers whether a genuine receipt covers a given payment

The agent is the untrusted party throughout. It supplies the payload, the document URL and the invoice ground truth, and its self-reported gate configuration is labelled as self-reported everywhere it is displayed.


Cryptographic assumptions

Purpose Primitive Reference
Receipt signatures Ed25519, pure (no prehash) RFC 8032 (https://www.rfc-editor.org/rfc/rfc8032) §5.1
Bytes that get signed JSON Canonicalization Scheme RFC 8785 (https://www.rfc-editor.org/rfc/rfc8785)
Payload and document fingerprints SHA-256 FIPS 180-4 (https://csrc.nist.gov/pubs/fips/180-4/upd1/final)
Verifier password storage scrypt RFC 7914 (https://www.rfc-editor.org/rfc/rfc7914)
Session cookie integrity HMAC-SHA-256 RFC 2104 (https://www.rfc-editor.org/rfc/rfc2104)

What these give, stated precisely:

  • Signatures are existentially unforgeable under chosen-message attack. That is a claim about producing a signature without the private key. It is not a claim that a receipt is about the payment in front of you — a receipt can be authentic, unaltered and about something else, which is why the verifier tools compare a payment instruction against it and report PAYLOAD_MISMATCH as a distinct verdict.
  • Signature-string uniqueness is not claimed. Ed25519 malleability and small-order points are properties of each verifier implementation, and this repository runs three (Node, browser WebCrypto, the offline page). Nothing here depends on a signature being the only valid one for a message: receipts are identified by receiptId, with a uniqueness constraint per verification, and are never keyed by signature.
  • Canonicalisation is a narrowing of RFC 8785, deliberately. Non-integer numbers and lone surrogates are rejected rather than serialised, so the cross-language reproducibility claim stays cheap to honour. A second encoder, canonicalizeForensic, has weaker guarantees by design and is kept out of every hashing and signing path by a source-level allowlist. Unicode is not normalised, which is load-bearing for agreement with other JCS implementations.
  • HMAC for session cookies is not a substitute for asymmetric signing. A cookie is issued and verified by the same server and is never handed to anyone as evidence. Receipts are third-party-verifiable and use Ed25519.

The offline verifier uses the browser's own Ed25519 and bundles no crypto library, so there is no opaque blob to trust. It self-tests against RFC 8032's published vector before judging anything — including confirming that it refuses a signature it should refuse — and reports CANNOT_CHECK rather than a verdict where the algorithm is unavailable.


Trust boundaries

Boundary What crosses it How it is treated
Agent → API Payload, raw tool args, ground truth, document URL, gate attestation Authenticated by API key; agentId derived from the key and refused in a body. Validated with strict schemas. Two free-text fields are restricted to a character class so displayed text matches hashed text
invoiceDocumentUrl → vault Attacker-supplied URL Fetched server-side behind a host allowlist, with DNS resolution checked against private, loopback, link-local and metadata ranges, the connection pinned to the validated address, every redirect hop re-checked, size and type capped and magic-byte sniffed
Vault → browser Attacker-supplied document bytes Parsed by pdf.js inside a hidden, opaque-origin sandbox with no allow-same-origin. Only an ImageBitmap and integers return; nothing from the frame is rendered as text or markup
Browser → API Decisions, claims, telemetry Session cookie derived identity; client-reported telemetry recorded as claims, with the enforced quantities measured server-side
Receipt → auditor Signed evidence Verifiable offline against a published key, with no network and no dependency on this repository

Sandbox containment rests on three independent layers — the sandbox's own connect-src, the session cookie's SameSite, and an Origin allowlist on state-changing routes. Because three mechanisms prevent one outcome, that outcome is not evidence for any of them, so each is asserted separately.

The offline verifier's CSP is deliberate and must not be loosened. 'strict-dynamic' was measured letting an injected inline script run, and an explicit origin was adopted instead. Generic advice recommending 'unsafe-inline' is wrong for this codebase.


Reporting a vulnerability

Email [email protected] ([email protected]). This address is for vulnerability reports only. For pilots and other enquiries, use [email protected] ([email protected]).

Please report privately rather than opening a public issue. Include what you did, what you observed, and what you expected. A proof of concept helps; a working exploit is welcome and not required.

We aim to acknowledge within 3 working days and to give an assessment within 10. There is no bug bounty.

In scope: this repository — the SDK, the backend API, the verifier console, the canonicaliser and the offline verifier bundle.

Out of scope, because they are already documented: anything in the published list of limitations. Reports naming an entry there are read with interest where they show the exposure is larger than the entry claims, and that is a useful report. A report that restates an entry is not a finding.

Also out of scope: the seeded development credentials in .env.example, the self-signed TLS fixture used by our own tests (generated for tests, never served), and the development-only mock invoice routes, which refuse to run under NODE_ENV=production.


Known limitations

Every one is written up in the register, with what it is, what it specifically is not, what limits the damage today, and what would close it. They are not restated here.

  • 1 — verifier credentials are a shared secret · launch blocker
  • 2 — the reviewer-sees/what-is-signed binding is server-enforced · launch blocker
  • 3 — rendering deception is unmitigated
  • 4 — OCR-sourced ground truth is not independent of the document it is compared against
  • 5 — the render-failure signal is client-reported and can be suppressed
  • 6 — shadow detection cannot see settings-file allow rules
  • 7PreToolUse hook precedence is unresolved
  • 8 — Confirmation of Payee is simulated
  • 9 — a single verifier can approve any payment
  • 10 — coverage is not uniform: some paths are tested only against stubs
  • 11 — the offline verifier moves the auditor's trust, it does not remove it
  • 12 — accented and unaccented payee names collide in the fingerprint
  • 13 — two duty-of-care checks rest on what the server cannot verify, and the release check lives in our client rather than in the protocol

Two things worth naming here because they are easy to assume away:

  • Homoglyphs are not detected. The character class on vendor names admits any Unicode letter, deliberately, so that a legitimate non-Latin supplier is not refused. A Cyrillic о in a Latin name passes it. What limits the exposure is indirect: such a name changes the payee fingerprint and yields a CoP NO_MATCH, which raises the payment to elevated handling.
  • Confirmation of Payee is simulated in V1, and every surface showing a result is labelled SIMULATED — NOT A LIVE COP RESPONSE. Configuring a live check throws at boot rather than degrading quietly.