{"_id":"e-ari-verify","_rev":"3-5423b99d1c315ca6c96cc765a4816c3e","name":"e-ari-verify","dist-tags":{"latest":"1.1.1"},"versions":{"1.0.0":{"name":"e-ari-verify","version":"1.0.0","license":"MIT","_id":"e-ari-verify@1.0.0","maintainers":[{"name":"e-ari","email":"Aymenarc@gmail.com"}],"bin":{"e-ari-verify":"verify.mjs"},"dist":{"shasum":"791700d6d22d2bd551509e7d6439d28873ade391","tarball":"https://registry.npmjs.org/e-ari-verify/-/e-ari-verify-1.0.0.tgz","fileCount":8,"integrity":"sha512-V2hJqRAtX3kFzNUsXzVnFfI/7qrrbArQfHFYNhP1O5Ra6ErA+fLlodnjuwYAzg0w9HA9TAGPSFIvV53kI/RZ/g==","signatures":[{"sig":"MEUCIQCVDx4qjIX6mXO/r0xQKeMOBf4crfkARJEfWVjulhPhygIgQoLHiPiU8tRQT1/5c0i7D4byKMnQUOk8cf0nr4OMCs4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":157434},"type":"module","engines":{"node":">=18.17"},"gitHead":"f162200bb589789f0eeb73c0e7d34db5b24e8603","scripts":{"test":"node verify.test.mjs && node replay.test.mjs"},"_npmUser":{"name":"e-ari","email":"Aymenarc@gmail.com"},"_npmVersion":"11.19.0","description":"Independent CLI verifier for Sealed Evidence Export (.see) bundles — written against the format spec, shares no code with the platform. Node built-ins only.","directories":{},"_nodeVersion":"26.8.1","_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/e-ari-verify_1.0.0_1788188204594_0.07068561948322594","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"e-ari-verify","version":"1.1.0","keywords":["eu-ai-act","compliance","transparency-log","merkle","rfc6962","rfc9162","ed25519","evidence","audit","verifier","tamper-evident"],"author":{"name":"E-ARI"},"license":"MIT","_id":"e-ari-verify@1.1.0","maintainers":[{"name":"e-ari","email":"Aymenarc@gmail.com"}],"homepage":"https://www.e-ari.com/verify","bin":{"e-ari-verify":"verify.mjs"},"dist":{"shasum":"6ec038449edff0c99d19cd9ecd9d8e33f054814d","tarball":"https://registry.npmjs.org/e-ari-verify/-/e-ari-verify-1.1.0.tgz","fileCount":10,"integrity":"sha512-AajOsq0qhBDPKYzNtfZffe485E0peSHBnRFebdJB+IJdo+9RYCg70u2wzNXFVCaZU3zDroWismH0JLdObs3JYg==","signatures":[{"sig":"MEQCIHlfuPMF5JxxnmVnjERSaIuILYv3L9owH9voAwzZ7MdRAiA/q/UIEYhJCecCq13EhXdxgAuEmcLVFZbNJhpmZyTlWA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIHwiwRclCPOfJ6b3dde1tNUyhiweYAdTyRNkynaNLk7FAiEAzKHJDn91T2MbSUWSoSs+sCb+G8RjjUjKRKuMQB2iM5g=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":185771},"type":"module","engines":{"node":">=18.17"},"gitHead":"c9681a71dbafe4df4063474ef8577d098f183d8a","scripts":{"test":"node verify.test.mjs && node replay.test.mjs"},"_npmUser":{"name":"e-ari","email":"Aymenarc@gmail.com"},"_npmVersion":"11.19.0","description":"Independent CLI verifier for Sealed Evidence Export (.see) bundles — written against the format spec, shares no code with the platform. Node built-ins only.","directories":{},"_nodeVersion":"26.8.1","_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/e-ari-verify_1.1.0_1793027141241_0.018978477874451283","host":"s3://npm-registry-packages-npm-production"}},"1.1.1":{"_id":"e-ari-verify@1.1.1","bin":{"e-ari-verify":"verify.mjs"},"dist":{"shasum":"722cc20b668454d60bfb85d31bf000d9b6810c69","tarball":"https://registry.npmjs.org/e-ari-verify/-/e-ari-verify-1.1.1.tgz","fileCount":10,"integrity":"sha512-oweNq8tRdVPC+O4kIfx3HfoP2ZVNNza3u034hr/Ai+EIehAYrVqT/O8nR9P8x0C2Vw5aaVnK4a3JduQVOUNvrA==","signatures":[{"sig":"MEYCIQC7Mlm3v44xxi0VjCCsGZp+D2dkVTe+plRTQkr0W40YhAIhAPvDEBXg0TWjPiHF5jEVKa7WPBgBXEdRaQTnWH/gG5Tz","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIFgUgbm9Be7QGthonrftQdyR7XpZ2zds4BUtxtStL/mLAiABNqYzo4djNy3VuUDVVoDfQYcOKPkMAaRLWObz9TFx/Q=="}],"unpackedSize":188014},"name":"e-ari-verify","type":"module","author":{"name":"E-ARI"},"engines":{"node":">=18.17"},"gitHead":"3f91d232245eb153f24492a6eeb7b4c086f84be8","license":"MIT","scripts":{"test":"node verify.test.mjs && node replay.test.mjs"},"version":"1.1.1","_npmUser":{"name":"e-ari","email":"Aymenarc@gmail.com"},"homepage":"https://www.e-ari.com/verify","keywords":["eu-ai-act","compliance","transparency-log","merkle","rfc6962","rfc9162","ed25519","evidence","audit","verifier","tamper-evident"],"_npmVersion":"11.19.0","description":"Independent CLI verifier for Sealed Evidence Export (.see) bundles — written against the format spec, shares no code with the platform. Node built-ins only.","directories":{},"maintainers":[{"name":"e-ari","email":"Aymenarc@gmail.com"}],"_nodeVersion":"26.8.1","_hasShrinkwrap":false,"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/e-ari-verify_1.1.1_1793028196841_0.28554207669346154"}}},"time":{"created":"2026-08-31T14:56:44.349Z","modified":"2026-10-26T15:23:17.449Z","1.0.0":"2026-08-31T14:56:44.747Z","1.1.0":"2026-10-26T15:05:41.350Z","1.1.1":"2026-10-26T15:23:16.942Z"},"author":{"name":"E-ARI"},"license":"MIT","homepage":"https://www.e-ari.com/verify","keywords":["eu-ai-act","compliance","transparency-log","merkle","rfc6962","rfc9162","ed25519","evidence","audit","verifier","tamper-evident"],"description":"Independent CLI verifier for Sealed Evidence Export (.see) bundles — written against the format spec, shares no code with the platform. Node built-ins only.","maintainers":[{"name":"e-ari","email":"Aymenarc@gmail.com"}],"readme":"# e-ari-verify\n\nIndependent CLI verifier for Sealed Evidence Export (`.see`) bundles.\n\nThis package is the independent verification path; it shares no code with the platform.\nIt was written against the Sealed Evidence Export format spec (SEER container bytes,\nSEE-CANON-1 canonical JSON, SEE-TBS-v1 / STH to-be-signed constructions, RFC 6962\nMerkle trees) and against RFC 6962 / RFC 9162 directly — not against the platform's\nsource. Its test suite proves verdict parity with the platform's implementation in\nboth directions (see `verify.test.mjs` / `cross-check.mjs`).\n\nPlain ESM JavaScript (`.mjs`), Node built-ins only (`node:crypto`), zero npm\ndependencies. Node >= 18.17.\n\n## Usage\n\n```\nnode verify.mjs <bundle.see> [--offline] [--log-key <hex32>] [--json] [--origin <url>]\n```\n\n- `--offline` — verify the bundle's internal integrity only; skip all network calls.\n- `--log-key <hex32>` — the transparency log's raw Ed25519 public key as 64 hex chars.\n  Without it, online mode fetches the epoch-0 key from the origin's\n  `GET /api/transparency/keys`.\n- `--origin <url>` — transparency-log origin. Default: `https://www.e-ari.com`.\n- `--json` — print the full result object as JSON.\n\nLibrary use also works: `import { verifyBundle, parseContainer, canonicalize, ... } from \"./verify.mjs\"`.\n\n## What it verifies (in order)\n\n1. **Container parse** — the file is a `SEER` v1.0 container: magic + version, then\n   well-formed records (`u8 type | u8 flags | u16 reserved | u64 BE length | payload`),\n   manifest record present, no duplicates, size limits respected.\n2. **Manifest canonical (SEE-CANON-1)** — the stored manifest bytes must be exactly\n   the canonicalization of the parsed manifest JSON (sorted keys, minimal escapes,\n   safe integers only). Any re-serialization, re-ordering or float fails closed.\n3. **Manifest signature (SEE-TBS-v1)** — the manifest payload is framed as\n   `\"see-tbs-v1\" ‖ 0x00 ‖ format.version ‖ 0x00 ‖ payload`; a `platform` role\n   Ed25519 signature entry must exist, its `keyId` must equal\n   `\"see-k-\" + base64url(sha256(rawKey))[0..22]`, its committed `manifestSha256`\n   must equal the digest of those TBS bytes, and the signature must verify.\n4. **Artifact integrity** — every manifest artifact's container record must hash to\n   the declared lowercase-hex `sha256` and match the declared `sizeBytes`.\n5. **Log anchor: STH signature** — for each `logProofs[]` entry, the Signed Tree Head\n   (`\"e-ari-sth-v1\" ‖ u8 len(logId) ‖ logId ‖ u32 BE keyEpoch ‖ u64 BE treeSize ‖\n   u64 BE timestampMs ‖ rootHash`) must carry an Ed25519 signature that verifies\n   under the transparency-log public key (from `--log-key`, or the epoch-0 key\n   fetched from the origin in online mode; without any key this fails closed).\n6. **Log anchor: inclusion proof** — the bundle's leaf hash\n   (`SHA-256(0x00 ‖ payload)`) must resolve to the STH root via the RFC 6962 §2.1.3.2\n   audit-path walk.\n7. **Log anchor: timestamp** — the STH timestamp must not be more than 5 minutes in\n   the future (clock-skew guard, `E_TIME`).\n\nAdditionally: any `customer`-role (cosign) signature entries in the signatures record\nmust verify over the same TBS bytes, unknown container record types produce a\n`WRN_UNKNOWN_RECORD` warning (never a failure), and a bundle without `logProofs`\nverifies `PASS` with `log: \"not-claimed\"` plus a `WRN_NO_LOG_PROOF` warning.\n\n## Online liveness (the anti-rewrite check)\n\nA bundle can be internally intact while the log that anchored it has been rewritten.\nWhen a bundle verifies `PASS` **and** claims an anchor (default online mode), the CLI\nadditionally checks against the live log at `{origin}`:\n\n- **Live head** — `GET /api/transparency/head` (204 = empty log) and its Ed25519\n  signature verified under the log public key.\n- **Consistency** — an RFC 9162 §2.1.4.2 consistency proof between the bundle's\n  embedded STH `treeSize` (root from the bundle) and the live `treeSize` (root from\n  the live head), via `GET /api/transparency/proof/consistency?first&second`.\n  A live tree *smaller* than the embedded one is an immediate alarm: an append-only\n  log cannot shrink.\n- **Re-inclusion** — the bundle's `leafIndex`/`leafHash` must re-include against the\n  **live** root at the live tree size via `GET /api/transparency/proof/inclusion`.\n\n## Trust model\n\n- **`--log-key <hex32>`** pins the log identity explicitly: you supply the raw\n  Ed25519 public key out-of-band and every STH / head signature is checked against\n  it. This is the strongest mode and the one a cold auditor should use.\n- **Without `--log-key`** (online mode only) the CLI fetches `GET\n  /api/transparency/keys` and uses the **epoch-0** public key — the key that\n  bootstrap committed into the log as the first key leaf. The CLI then reports,\n  in the output and in `--json`:\n  - `trustSource: \"epoch-0 key fetched from origin (NOT pinned — confirm the\n    fingerprint out-of-band)\"`,\n  - the `epoch0Fingerprint` the origin declares and whether it matches\n    `SHA-256` of the served key (`fingerprintMatch`).\n  This proves *self-consistency* of the served log, not that the origin is the\n  genuine one: for a first-contact verification, compare the reported fingerprint\n  against a value you obtained earlier, then re-run with `--log-key`.\n- **Offline mode** (`--offline`) verifies only what the bundle itself proves. A\n  bundle that claims a log anchor still needs the log key for its STH signature;\n  with no key available verification **fails closed** (`E_PROOF_INVALID`).\n- **Honest \"not claimed\" state** — a bundle with no `logProofs` is reported\n  `PASS` / `log: \"not-claimed\"` (exit 1). The CLI never upgrades that to\n  \"anchored\", and never invents a liveness claim for it; it makes no network\n  requests for such bundles.\n\n## Audit Replay (`replay` subcommand)\n\n```\ne-ari-verify replay <bundle.see> [--spec-dir <path>] [--log-key <hex32>]\n                   [--origin <url>] [--report <out.json>] [--report-key <pem>] [--json]\n```\n\nThe auditor's half of Audit Replay (spec: `docs/scoring-spec.md`,\n`e-ari-scoring-spec/5.6`). Bundles sealed since 5.4 carry a\n`decisionTrail.scoring` block — the exact answers the platform scored beside\nthe score it published. `replay` re-executes the ENTIRE scoring pipeline\noffline, from those inputs, with this package's own independent implementation\n(the third one: the engine is #1, the conformance corpus #2, this file #3 — it\nshares no code with the platform), then compares.\n\nSCORING_VERSION 5.5 adds Don't-know / Not-applicable answers: an answer may\ncarry an annotation (`'dk' | 'na'`) instead of a Likert rating. The restated\npipeline takes them as an optional third argument —\n`scorePerSpec(responses, sector, annotations)` with\n`annotations: Record<questionId, 'dk' | 'na'>` — and the same map is read from\na conformance vector's optional `annotations` field and from the bundle's\n`decisionTrail.scoring.annotations` when it carries one. Annotated questions\ncount as answered for completeness but are excluded from the pillar\narithmetic: each pillar renormalises over its answered questions\n(`((raw − n) / 4n) × 100`), so full coverage is arithmetic-identical to 5.4 —\nvectors and bundles without annotations replay to exactly their old numbers.\nTwo refusals come with them: a question that is BOTH annotated and numerically\nanswered, and an annotation key that is not a known questionId, throw\n`E_INVALID_ANSWER` naming the question; a pillar whose every question is\nannotated throws `E_NO_PILLAR_COVERAGE` naming the pillar (a score computed\nfrom no measurements is not a score). X-Ray detectors whose declared inputs\ninclude an annotated question are skipped — their triggers cannot be evaluated\non data that does not exist.\n\nSCORING_VERSION 5.6 bounds that renormalisation at three rated questions per\npillar: a pillar with only one or two of its five rated throws\n`E_INSUFFICIENT_PILLAR_COVERAGE` naming the pillar.\n\nFail-closed in order:\n\n1. **Self-conformance** — the published corpus (`src/lib/scoring-spec/vectors.json`,\n   found via `--spec-dir` or next to the repo) is run through THIS\n   implementation first. Any disagreement and nothing is reported (exit 3).\n2. **Bundle integrity** — the same fail-closed checks as `verify` (container,\n   canonicalisation, platform signature, artifact hashes, log anchor).\n3. **Scoring input** — the bundle must carry `decisionTrail.scoring` scored by\n   `SCORING_VERSION 5.6`; anything else is \"nothing to replay\" (exit 3), never a\n   verdict. Each release replays one scoring version: see `CHANGELOG.md` for\n   which release replays an older bundle.\n\nThen the recomputation runs and the verdict is issued:\n\n| Exit | Verdict | Meaning |\n| --- | --- | --- |\n| `0` | **REPLAYED-IDENTICAL** | recomputed composite + band equal the platform-scored values |\n| `3` | error | malformed bundle, verify FAIL, no scoring input, version skew, or self-check failure — nothing was replayed |\n| `4` | **REPLAY MISMATCH** | the bundle is intact but the platform score did not survive recomputation — the alarm |\n\nThe output report is signed Ed25519 over its canonical bytes (`--report-key`\nsupplies the key; default ephemeral, flagged `signedWith.ephemeral`) and\ncarries `specVersion`, `inputsHash`, the per-pillar derivation chain, fired\nrules, composite, band, the platform figures and `match`. `match: true` means\n**reproducible**, never \"correct\" — deterministic ≠ correct, and the report\nsays so.\n\nDemo: `node demo-replay.mjs` seals the seeded demo assessment from the dev\ndatabase with the platform's real primitives and replays it;\n`node demo-replay.mjs --tamper` seals a compromised scorer (valid signature,\nwrong persisted score) and shows the exit-4 alarm.\n\n## Exit codes\n\n| Code | Meaning |\n| --- | --- |\n| `0` | **Verified** — bundle PASSes and the live log confirms the embedded anchor (consistency + re-inclusion). |\n| `1` | **Offline-consistent only** — bundle PASSes, but the log is not claimed by the bundle, or `--offline` was used. |\n| `2` | **LIVENESS FAILURE** — the bundle PASSes offline, but the live log contradicts the embedded anchor (rewritten/shrunken tree, bad head signature, empty log) **or** could not be reached to confirm it. Treat as a possible rewrite; this is the alarm. |\n| `3` | **Malformed / verify FAIL** — the bundle is not a valid SEER container or fails any coded check (`E_BUNDLE_PARSE`, `E_CANON_MISMATCH`, `E_SIG_MISSING`, `E_SIG_INVALID`, `E_SIG_KEY_MISMATCH`, `E_HASH_MISMATCH`, `E_SIZE_MISMATCH`, `E_PROOF_INVALID`, `E_TIME`, ...). |\n\nEvery executed check is listed in the output; `--json` prints the full result object\n(`result`, `attestation`, `log`, `cosign`, `checks[]`, `warnings[]`, `code`/`detail`,\n`bundle` info and the complete `liveness` section).\n\n## Mirror cron recipe (20 lines)\n\nWatch the log itself: every tick, pull the head, check it grew, and verify a\nconsistency proof between the previous head and the new one. Any failure is an\nalarm — append-only logs only ever grow, and consecutive roots must chain.\n\n```bash\n#!/bin/sh\n# mirror-watch.sh — run from cron; STATE_DIR holds the last seen head.\nORIGIN=\"https://your-log.example.com\"; STATE_DIR=\"$HOME/.log-mirror\"; mkdir -p \"$STATE_DIR\"\nhead=$(mktemp) || exit 1\ncurl -fsS --max-time 20 \"$ORIGIN/api/transparency/head\" > \"$head\" || { echo \"ALARM: head unreachable\"; exit 2; }\nsize=$(jq -r .treeSize \"$head\"); root=$(jq -r .rootHash \"$head\")\nprev=\"$STATE_DIR/last.json\"\nif [ -s \"$prev\" ]; then\n  first=$(jq -r .treeSize \"$prev\"); second=$size\n  if [ \"$second\" -lt \"$first\" ]; then echo \"ALARM: log shrank $first -> $second\"; exit 2; fi\n  proof=\"$STATE_DIR/proof.json\"\n  if [ \"$second\" -gt \"$first\" ]; then\n    curl -fsS --max-time 20 \"$ORIGIN/api/transparency/proof/consistency?first=$first&second=$second\" > \"$proof\" \\\n      || { echo \"ALARM: consistency proof unavailable\"; exit 2; }\n    jq -e --argjson f \"$first\" --argjson s \"$second\" \\\n      '.consistencyPath | length > 0' \"$proof\" >/dev/null || { echo \"ALARM: empty consistency path\"; exit 2; }\n  fi\nfi\ncp \"$head\" \"$prev\"\necho \"mirror ok: treeSize=$size root=$root\"\n```\n\nFor full verification of entries and proofs, replay leaves from\n`GET /api/transparency/entries?start&end` (RFC 6962-style `get-entries`) and check\neach payload's leaf hash — or simply run `e-ari-verify` against a `.see` bundle\nanchored to this log, which performs the whole chain (STH signature, inclusion,\nconsistency, re-inclusion) for you.\n\n## Tests\n\n```\nnode verify.test.mjs\n```\n\nThe suite seals bundles with this package's own implementation (keys, manifest,\nSEER container, private RFC 6962 log, STH), asserts `PASS`, asserts every tamper\nvariant fails with its exact error code, drives the full online liveness flow\nagainst mock endpoints that mirror the platform's API shapes, fires every alarm\npath (rewrite / shrink / empty / bad head signature / unreachable), and — when\n`npx tsx` is available — proves cross-implementation verdict parity in **both\ndirections**: a bundle sealed by the platform verifies `PASS` here, and every\nbundle this package seals (plus all tampered variants) reaches the identical\nverdict under the platform's `verifyBundle`.\n\n### Replay input fingerprint compatibility\n\n`scoringInput.inputsHash` includes non-empty sealed `annotations`, so changing an answer from `dk` to `na` changes the fingerprint even when the numeric score is unchanged. Absent or empty annotations retain the original fingerprint. Previously generated annotated reports omitted this field from the input hash; their signed bundle digest still binds the complete bundle. Regenerate a replay report to obtain the complete input fingerprint. This changes report metadata, not scoring methodology or sealed bundle bytes.\n","readmeFilename":"README.md"}