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.
- 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
PROVENparticipation andPROVENnon-participation without touching a signature. Reports now derive every substantive claim from the signedrecord.decision.inputs; supplied events are corroboration only, and divergence raises a caveat naming both values. - Merkle inclusion proofs were never checked.
anchorCoversRecordaccepted a proof on string equality alone, so anchoring an arbitrary digest yieldedANCHORED_VALIDfor a record never in the tree. Paths are now genuinely recomputed, and the tree uses RFC 6962 domain separation. PROVENwas unreachable and quorum was falsely green. Neither surface passed anchors into report building, so every line capped atSIGNED_PENDING; meanwhile a fabricated self-signed RFC-3161 token satisfied quorum. Anchors now flow through to reports, andAnchorAttestation.verificationis a required field — a quorum counts onlyverified, 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 | Fixed — verified/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 | Fixed — state.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 | Fixed — seal --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 | Fixed — fsync 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 | Fixed — src/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 fixed — state.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 | Documented — DATA-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:
- Network egress is default-deny. Private RPC nodes, on-prem Witness hubs and local test doubles need
WITNESS_DAO_ALLOW_PRIVATE_NETWORK=1orallowPrivateNetwork. - 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. - Free text defaults to
hash-only. Third-party proposal bodies are sealed as digests unless you opt intofull. - 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. requireAnchoredForReportsnow genuinely enforces. It throws when a record does not verify asANCHORED_VALID, naming each one. Previously it gated on a deletable unsigned field.- Merkle digests changed (RFC 6962 domain separation). Nothing was in production; anything stored must be re-anchored.
- MCP writes and exports are off by default — set
WITNESS_DAO_MCP_ALLOW_WRITESto enable. - 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.