Witness-DAO

Docs & audits / Audit

This is an adversarial audit, published unedited. Its verdict describes the codebase as it stood on the audit date. The disposition of every finding is in the remediation status, which is explicit about what was implemented versus documented.

Contents

Remediation status — Witness-DAO

Date: 26 July 2026 Baseline: four independent adversarial audits (crypto, appsec, enterprise/SRE, compliance/data protection) producing 86 findings. Final state: tsc --noEmit clean · 1,198 tests passing, 0 failing (from 753) · build clean · CLI and MCP smoke-tested.

This document is the disposition of every finding. It is deliberately explicit about what was implemented versus documented, because for this product the difference is the whole point.


Headline: what was actually broken

Three defects invalidated the product's core promise. All three are fixed.

  1. The signatures were decorative. Reports drew actors, event kind and binding confidence from a caller-supplied, unsigned event array. The audit PoC flipped a named individual between PROVEN participation and PROVEN non-participation without touching a signature. Reports now derive every substantive claim from the signed record.decision.inputs; supplied events are corroboration only, and divergence raises a caveat naming both values.
  2. Merkle inclusion proofs were never checked. anchorCoversRecord accepted a proof on string equality alone, so anchoring an arbitrary digest yielded ANCHORED_VALID for a record never in the tree. Paths are now genuinely recomputed, and the tree uses RFC 6962 domain separation.
  3. PROVEN was unreachable and quorum was falsely green. Neither surface passed anchors into report building, so every line capped at SIGNED_PENDING; meanwhile a fabricated self-signed RFC-3161 token satisfied quorum. Anchors now flow through to reports, and AnchorAttestation.verification is a required field — a quorum counts only verified, so out of the box, with no pinned material, quorum is honestly NOT met.

Disposition by audit

Cryptographic (24 findings) — all addressed

ID Severity Disposition
F-01 Critical Fixed — reports derive from signed decision.inputs; events are corroboration only
F-02 Critical Fixed — inclusion paths recomputed via verifyMerkleProof; both proof shapes typed as a union
F-03 High Fixed — chain walk breaks on recordId, not seq; decoy records can no longer skip the check
F-04 High Fixed — compliance gate decides on the computed verdict, fail-closed; the unsigned anchorState is no longer load-bearing
F-05 High Fixedverified/asserted_unverified made structural and required; quorum refuses unverified, errored or digest-mismatched attestations
F-06 High Fixed — integers outside ±(2^53−1) refused (CANONICAL_UNSAFE_INTEGER); carry wei as strings
F-07–F-09 Medium Fixed__proto__ smuggling closed; DID bound to didDocument.id; genesis must have no prevHash
F-10–F-12 Medium Fixed — OTS allocation bounded; single canonicalisation (TOCTOU closed); fork detection added
F-13–F-22 Low/Info Fixed — overlong varuints, DER minimality, quorum digest binding, OTS branch bounds, nonce persistence, strict base64, single accepted message form, displayName now signed. digestsEqual's inaccurate constant-time claim corrected in comment
F-23, F-24 Info No action — correct as written (RFC 6962 verified against 820 independent vectors)

Application security (23 findings) — all addressed

ID Severity Disposition
A-01 Critical Fixed — RCE via core.fsmonitor closed; every git call pins a hardened config (fsmonitor, hooksPath, pager, editor, sshCommand, credential.helper, diff.external, protocol.allow) under a minimal environment with GIT_CONFIG_NOSYSTEM=1
A-02, A-15 High Fixed — default-deny SSRF egress guard (IP-literal ranges, DNS resolution, IPv4-mapped/NAT64 unwrapping, metadata endpoints); redirect: 'manual' with per-hop revalidation and auth headers dropped on origin change; bearer over http:// refused
A-03–A-06 High Fixed — 8 MiB streamed response cap; DER INTEGER capped (5.1 s → <10 ms); ABI offset aliasing rejected; OTS upgrade fan-out deduped and capped
A-07 High Fixed — minted key written O_CREAT|O_EXCL|O_WRONLY|O_NOFOLLOW at 0600, mode re-verified, symlinks refused
A-08 Medium Fixed — Markdown escaping across every sink; the forged-PROVEN-row PoC now adds zero table rows
A-09–A-12 Medium Fixed — linear heading scan; RPC request/time budgets; O(n²) verification linearised; deep nesting → INSUFFICIENT_PROOF, not a false tampering allegation
A-13, A-14 Medium/Low Fixed — MCP mutating and export tools disabled by default; seal defaults to dry-run and needs explicit confirmation; real bundle schema
A-16–A-20 Low Fixed — extended secret detection, byte-bounded framing, Object.hasOwn guard, --out clobber/traversal protection
A-21, A-22 Info Fixedstate.ts now has 29 regression tests; engines corrected to >=22.6.0
A-23 Info Positive finding — supply chain confirmed clean, unchanged

Enterprise / SRE (17 findings) — all addressed

ID Severity Disposition
E-01 Critical Fixedseal --retry drains the journal by replaying stored requests; attempt counts, backlog depth and age-of-oldest are tracked, exposed and alertable
E-02 Critical Fixed — live re-ingest deleted; reports are pure, offline, reproducible functions of signed records
E-03 Critical Fixed — anchors + verifier loaded from the durable store and threaded into reports; PROVEN reachable and tested end to end
E-04, E-05 High Fixedfsync on file and directory; advisory lockfile with staleness detection refuses concurrent runs
E-06 High Corrected claim — customer-held-key signing does not exist; README no longer implies it does
E-07 High Fixed — controller-key renewal implemented (keys bind-controller, keys renew), pluggable KMS-ready signer, pre-flight expiry warnings
E-08, E-10 High Fixedsrc/obs/ structured JSON logging + metrics with pluggable sinks; machine-classifiable problem codes; quota, tier and key TTL surfaced
E-09 High Fixed — verification linearised: 5,000 records ≈ 0.97 s (was ~3 min extrapolated)
E-11 Medium Fixed — pending-age budget with non-zero exit; systemd and k8s CronJob examples in RUNBOOK.md
E-12 Medium Partly fixedstate.ts covered; live integration tests still absent (see below)
E-13 Medium Fixed — Dockerfile, CI workflow, selftest command, RUNBOOK.md
E-14–E-17 Medium/Low Fixed — per-tenant state namespacing, clock-skew tolerance, size caps, dist/ documented as the production entrypoint

Compliance / data protection (19 findings) — implemented + documented

ID Severity Disposition
C-01 Critical DocumentedDATA-PROTECTION.md: processed fields, controller/processor model, sub-processor register, transfers, retention, DPIA input pack, Art. 30 template
C-02 Critical Implemented — crypto-shredding (erase), tombstones retaining bodyHash/prevHash/signature, standing erasure directives applied to re-fetched records, retention policy + purge
C-03, C-05, C-06, C-08 High Documented — addresses treated as personal data; Art. 9 inference risk flagged; roles and sub-processors named; Art. 14 gap stated as unsolvable by the product alone
C-04 High Implemented — content policy (full/hash-only/omit), default hash-only for third-party free text
C-07 High Implemented — residency policy and EU-only mode, enforced at wiring time
C-09 High Fixed — EU AI Act Art. 12 framing removed; it governs high-risk AI systems, and this seals human governance actions
C-10 High Fixed — unimplemented AML mandate removed from the shipped rule template, with a warning about permanently anchoring a sanctions allegation
C-11, C-12 Medium Implemented — local purge; signed supersession propagated into both report types
C-13, C-14 Medium Fixed — eIDAS wording corrected throughout (Art. 41(1) admissibility, not Art. 41(2) presumption — the default TSAs are not qualified); MiCA claim narrowed and the keyword dropped
C-15 Medium Implemented — Merkle root over the sorted actor set, so non-participation can be shown without sealing the voter roll in plaintext
C-16, C-18 Medium/Low Implemented — mandatory --purpose recorded in the artefact, dossier access log, MCP export gated
C-17 Low Documented — RFC-3161 requests are plaintext HTTP; cadence and volume leak to observers
C-19 Info Positive finding — determinism, reportDigest and the non-weakenable disclaimer retained

Behaviour changes operators must know about

These are deliberate and will affect an existing pilot:

  1. Network egress is default-deny. Private RPC nodes, on-prem Witness hubs and local test doubles need WITNESS_DAO_ALLOW_PRIVATE_NETWORK=1 or allowPrivateNetwork.
  2. Quorum is honestly not met out of the box. Without a pinned TSA root, Rekor log key or Bitcoin header source, anchors are asserted_unverified. The CLI shows this in yellow (anchors exist but cannot be verified) rather than false green or misleading red.
  3. Free text defaults to hash-only. Third-party proposal bodies are sealed as digests unless you opt into full.
  4. Reports can no longer be inflated by unsigned data. Lines whose binding existed only in the unsigned event now grade UNSUPPORTED. This is legitimate deflation, not a regression.
  5. requireAnchoredForReports now genuinely enforces. It throws when a record does not verify as ANCHORED_VALID, naming each one. Previously it gated on a deletable unsigned field.
  6. Merkle digests changed (RFC 6962 domain separation). Nothing was in production; anything stored must be re-anchored.
  7. MCP writes and exports are off by default — set WITNESS_DAO_MCP_ALLOW_WRITES to enable.
  8. Node >= 22.6.0 and dist/ is the production entrypoint.

Still outstanding — honest list

Nothing here is a known defect; these are gaps that need real-world inputs or a decision.

Item Why it remains
Live integration tests (E-12) No wire format has been exercised against a real DigiCert/Sectigo, OTS calendar, Rekor or Witness endpoint. The test:integration script exists but is unpopulated. This is the largest remaining technical risk — a wire-format surprise would silently downgrade anchors.
RFC-3161 signature verification untested against a live TSA Conservative by design (an unusable certificate yields not_checked, never a false pass), but wants one real round-trip.
Body-hash signature form The verifier now accepts only canonical body bytes by default. If upstream Witness signs the hex bodyHash instead, that path needs acceptBodyHashSignature. Confirm against a real Witness response before release.
No TSA environment binding Strict residency will refuse the default US TSAs — honest, but reconfiguring requires code.
Customer-held-key signing (E-06) Genuinely not implemented. Claim removed rather than faked.
Remote erasure The VDA API has no delete endpoint. Local enforcement is complete and applies to re-fetched records; the remote half is a documented RemoteRecordEraser interface, and erase exits 4 to say so.
Anchor store read/write race anchor upgrade can write while report reads. Reads are safe (atomic rename) but a report may observe a pre-upgrade snapshot and understate by one cadence. Documented in RUNBOOK.md §4.4 rather than blocking reports behind maintenance.
Counsel review Every compliance-facing claim, the Art. 17(3)(e) retention argument and the limitation-period analysis need a lawyer. Nothing here is legal advice.

Verification

tsc -p tsconfig.json --noEmit     clean
npm test                          1198 passing, 0 failing
npm run build                     clean
node dist/cli/index.js --help     13 commands incl. erase, purge, selftest, keys renew
node dist/mcp/server.js           4 tools by default; 6 with writes enabled

Every fix carries a regression test that fails before and passes after; each audit PoC is encoded as a test.