From 12019448703e1df8de6f4898ae3aa0dc797305fd Mon Sep 17 00:00:00 2001 From: Ben Westgate Date: Wed, 23 Sep 2026 07:11:14 -0500 Subject: [PATCH 1/4] docs: Record security audit verdict Keep local planning state ignored and publish only the finished audit result in a dedicated audit directory. This narrows the review surface to stable findings and their tracking references. Refs #20. --- docs/audits/security-2026-09-22.md | 184 +++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 docs/audits/security-2026-09-22.md diff --git a/docs/audits/security-2026-09-22.md b/docs/audits/security-2026-09-22.md new file mode 100644 index 0000000..aad9704 --- /dev/null +++ b/docs/audits/security-2026-09-22.md @@ -0,0 +1,184 @@ +# Security audit verdict — 2026-09-22 + +Scope: python-codex32 at `c118a834fc9cce95ead3c05b3a2532e1320980c5`, +the graphical wallet flow at `78f4a3b4e99b02bf012ff179aaa7f3f8a96683f2` +and its current descendant `8f1001f`, and the BIP93/Codex32 assumptions those +paths rely on. + +This document records the security-review result. It is not a replacement for +the normative [security model](../security/model.md) or +[security invariants](../security/invariants.md). + +## Verdict + +The core implementation is careful about parsing, checksum validation, +reconstruction, Bitcoin Core process boundaries, and secret transfer. No break +of the Codex32 checksum, finite-field reconstruction, OS-CSPRNG use, or BIP32 +root validation was found. + +The audit did find two medium library implementation defects, one low +interoperability/specification-divergence defect, and two wallet recovery +design/ordering defects. It also confirmed two properties of BIP93 +that applications must not mistake for authentication: the 20-bit set +identifier does not prevent cross-set mixing, and checksum-valid shares do not +authenticate the intended reconstructed secret against active substitution. + +The practical rule for callers is therefore stricter than “valid Codex32 +shares reconstruct a valid seed.” Recovery must also be bound to an independent +wallet identity before any recovered key material mutates a live wallet. + +## Confirmed implementation findings + +### Medium — unbounded correction alignment cache + +Tracking: [#21](https://github.com/BenWestgate/python-codex32/issues/21). + +- Affected component: `src/codex32/correction.py`, `_syndrome_alignment`. +- Failure scenario: caller-controlled HRP/length combinations create distinct + entries in a process-global unbounded cache. A long-lived GUI or service can + retain alignment tables monotonically. +- Property violated: bounded resource use for untrusted correction input. +- Exploitability: practical for long-lived processes. A local reproduction with + 50 attacker-distinct HRP keys retained about 5.66 MiB, approximately 113 KiB + per key in that run. +- Remediation: remove attacker-specific global caching or use a small bounded + LRU with lifecycle clearing; add churn tests that assert a hard bound. +- Confidence: high. + +### Medium — secret-bearing default string representations + +Tracking: [#22](https://github.com/BenWestgate/python-codex32/issues/22). + +- Affected component: artifact string/repr paths and nested correction + candidates. +- Failure scenario: `_Artifact.__str__` returns the complete Codex32 + secret/share text, while default object representations can retain or expose + secret-bearing fields. Generic logging, notebooks, REPLs, assertion output, + or exception context can persist recovery material unexpectedly. +- Property violated: secrets stay out of ordinary output and logs unless an + explicit reveal/export operation is requested. +- Exploitability: practical accidental disclosure; no attacker is required once + a secret-bearing object reaches generic diagnostics. +- Remediation: make normal `str`/`repr` redacted and require an explicit + reveal/export operation for the full Codex32 string; test nested containers + and correction candidates. +- Confidence: high. + +### Low — overlong BIP173 human-readable parts are accepted + +Tracking: [#32](https://github.com/BenWestgate/python-codex32/issues/32). + +- Affected component: `src/codex32/bip93.py` decoder and public correction + contexts. +- Failure scenario: an otherwise checksum-valid Codex32 string with an HRP of + 84 or more characters is accepted even though BIP173 limits the HRP to 83 + characters. The same invalid namespace can be admitted to correction setup. +- Property violated: accepted encodings must remain inside the normative + BIP173/BIP93 grammar so independent implementations agree on validity. +- Exploitability: practical interoperability failure, not a secret compromise. + A concrete 84-character-HRP string was accepted during the audit. +- Remediation: reject HRPs longer than 83 characters in decoding and correction + context validation; cover the 83/84-character boundary in tests. +- Confidence: high. + +### Medium — wallet restore verifies recovered identity after mutation + +Tracking: GUI issue #26; CLI issue #30. + +- Affected component: GUI recovery in `src/codex32_gui/pages.py`, CLI recovery + in `src/codex32/cli.py`, and the shared mutating wallet initialization path. +- Failure scenario: both supported restore front ends mutate Core before the + recovered wallet identity is checked. The GUI calls `wallet_setup.fill(...)` + before it computes and displays the identity. `ms32 wallet` calls + `core.initialize(...)` before displaying the recovered fingerprint. A wrong + but checksum-valid recovered seed can therefore import private descriptors + into the selected empty Bitcoin Core wallet before the operator can detect + the mismatch. +- Property violated: recovery identity must be authenticated before recovered + key material changes durable wallet state. +- Exploitability: practical. It can be triggered by operator mixing of distinct + share sets, malicious share substitution, or any other path yielding a valid + but unintended seed. +- Remediation: on every restore path, derive a recovery identity without + importing descriptors, require the operator to verify it against an + independent wallet record, and only then call the mutating wallet + initialization path. +- Confidence: high. Reconfirmed against the Bails-pinned GUI commit, the current + GUI descendant, and the supported `ms32 wallet` CLI path. + +### Medium design concern — 32-bit fingerprint is too weak as an active recovery gate + +Tracking: issue #27. + +- Affected component: wallet record/recovery identity design across restore + front ends. +- Failure scenario: even if the ordering above is fixed, the current wallet + identity is only the four-byte BIP32 master fingerprint. An active adversary + able to search candidate seeds can target a 32-bit value; accidental + collisions also remain possible over sufficiently large populations. +- Property violated: a recovery authentication gate should have enough binding + strength that deliberate collision search is infeasible. +- Exploitability: the weakness is practical as a design limit, although a + targeted seed search still has real compute cost. It is not a break of BIP32. +- Remediation: store and verify an independent public commitment with at least + 128 bits of collision resistance before import. Keep the BIP32 fingerprint as + a human-friendly diagnostic, not the sole authorization value. +- Confidence: high on the weakness; medium on the final record format because + the product UX still needs a concrete commitment encoding. + +## Confirmed BIP93/Codex32 application hazards + +These are protocol properties, not evidence that python-codex32 implemented the +specified arithmetic incorrectly. + +The current BIP93 text already states that identifiers are for disambiguation +and that the checksum does not protect against maliciously constructed errors. +These findings therefore require application-side enforcement and documentation, +not an upstream BIP93 vulnerability report. + +### Medium — 20-bit set identifier does not prevent cross-set mixing + +- Affected specification: BIP93 set identifier and threshold recovery model. +- Failure scenario: shares from two valid sets with the same compatible header + can be combined into the required threshold and interpolate a third seed that + is checksum-valid. The audit reproduced this behavior. +- Property violated: if an application assumes the identifier authenticates + set membership, that assumption fails. +- Exploitability: practical once shares from compatible colliding/matched sets + are available. The 20-bit identifier is a typo/mix-up aid, not a + cryptographic set commitment. +- Remediation: applications must bind recovery to an independent wallet/public + commitment and reject a recovered secret that does not match it. +- Confidence: high. + +### Medium — valid-share substitution can force an attacker-chosen reconstruction + +- Affected specification: unauthenticated threshold-share model. +- Failure scenario: an attacker who can replace enough shares can construct + checksum-valid replacement shares that make the victim reconstruct a chosen + target secret. The audit reproduced this for a 2-of-2 set. +- Property violated: share integrity/provenance and intended-secret + authentication. +- Exploitability: practical when the attacker can replace the required share + material. This is not a secrecy break; it is an authenticity failure if the + application treats checksum validity as authorization. +- Remediation: verify an independent public commitment before using recovered + key material; protect physical shares against substitution where that threat + is in scope. +- Confidence: high. + +## Falsified candidate + +The earlier “opaque-HRP deadline bypass” candidate was falsified during review +and is not a security finding. It must not be cited or tracked as one. + +## Required recovery invariant + +For every wallet restore path built on python-codex32: + +> Parsing, checksum validity, threshold reconstruction, and the BIP32 +> fingerprint are not authorization to mutate a wallet. A recovered seed must +> first match an independent, sufficiently strong public wallet commitment. + +That invariant is the boundary needed to contain both accidental share-set +mixing and active share substitution. From 654039bfc31ec13e61c9351018d6b2cbe5b168db Mon Sep 17 00:00:00 2001 From: Ben Westgate Date: Wed, 23 Sep 2026 11:05:15 -0500 Subject: [PATCH 2/4] docs: Link audited GUI revisions --- docs/audits/security-2026-09-22.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/audits/security-2026-09-22.md b/docs/audits/security-2026-09-22.md index aad9704..cb8a41e 100644 --- a/docs/audits/security-2026-09-22.md +++ b/docs/audits/security-2026-09-22.md @@ -1,9 +1,12 @@ # Security audit verdict — 2026-09-22 -Scope: python-codex32 at `c118a834fc9cce95ead3c05b3a2532e1320980c5`, -the graphical wallet flow at `78f4a3b4e99b02bf012ff179aaa7f3f8a96683f2` -and its current descendant `8f1001f`, and the BIP93/Codex32 assumptions those -paths rely on. +Scope: python-codex32 at +[`c118a834fc9cce95ead3c05b3a2532e1320980c5`](https://github.com/BenWestgate/python-codex32/commit/c118a834fc9cce95ead3c05b3a2532e1320980c5), +the graphical wallet flow at +[`78f4a3b4e99b02bf012ff179aaa7f3f8a96683f2`](https://github.com/BenWestgate/python-codex32/commit/78f4a3b4e99b02bf012ff179aaa7f3f8a96683f2) +and its current descendant +[`8f1001f61983cf60023120af4b01bbc99d6f3a84`](https://github.com/BenWestgate/python-codex32/commit/8f1001f61983cf60023120af4b01bbc99d6f3a84), +and the BIP93/Codex32 assumptions those paths rely on. This document records the security-review result. It is not a replacement for the normative [security model](../security/model.md) or From 31598f0a6482dbfbdc55ac703925aba6afed5b07 Mon Sep 17 00:00:00 2001 From: Ben Westgate Date: Fri, 25 Sep 2026 08:28:45 -0500 Subject: [PATCH 3/4] docs: Separate recovery accident and tampering checks --- docs/audits/security-2026-09-22.md | 111 +++++++++++++++++------------ 1 file changed, 65 insertions(+), 46 deletions(-) diff --git a/docs/audits/security-2026-09-22.md b/docs/audits/security-2026-09-22.md index cb8a41e..bf3a52c 100644 --- a/docs/audits/security-2026-09-22.md +++ b/docs/audits/security-2026-09-22.md @@ -20,15 +20,20 @@ of the Codex32 checksum, finite-field reconstruction, OS-CSPRNG use, or BIP32 root validation was found. The audit did find two medium library implementation defects, one low -interoperability/specification-divergence defect, and two wallet recovery -design/ordering defects. It also confirmed two properties of BIP93 -that applications must not mistake for authentication: the 20-bit set -identifier does not prevent cross-set mixing, and checksum-valid shares do not -authenticate the intended reconstructed secret against active substitution. - -The practical rule for callers is therefore stricter than “valid Codex32 -shares reconstruct a valid seed.” Recovery must also be bound to an independent -wallet identity before any recovered key material mutates a live wallet. +interoperability/specification-divergence defect, and a wallet recovery ordering +defect. It also confirmed two properties of BIP93 that applications must not +mistake for cryptographic authentication: the 20-bit set identifier does not +prevent cross-set mixing, and checksum-valid shares do not authenticate the +intended reconstructed secret against active substitution. + +The release-gate rule for callers is therefore stricter than “valid Codex32 +shares reconstruct a valid seed”: before recovered key material mutates a live +wallet, the operator must authenticate that the recovered seed is the wallet +they intended. For the current single-sig restore flow this is an accident-safety +check: a typed wallet-record fingerprint, or an explicit visual confirmation +when no record exists. Stronger resistance to malicious share/seed replacement +is a separate problem, tracked as an authenticated encrypted descriptor backup +in issue #55. ## Confirmed implementation findings @@ -97,37 +102,45 @@ Tracking: GUI issue #26; CLI issue #30. but checksum-valid recovered seed can therefore import private descriptors into the selected empty Bitcoin Core wallet before the operator can detect the mismatch. -- Property violated: recovery identity must be authenticated before recovered - key material changes durable wallet state. -- Exploitability: practical. It can be triggered by operator mixing of distinct - share sets, malicious share substitution, or any other path yielding a valid - but unintended seed. -- Remediation: on every restore path, derive a recovery identity without - importing descriptors, require the operator to verify it against an - independent wallet record, and only then call the mutating wallet - initialization path. +- Property violated: the operator must identify the intended wallet before + recovered key material changes durable wallet state. +- Exploitability: practical for accidental wrong/mixed cards, miscorrection, or + another valid-but-unintended recovery. Deliberate threshold-share replacement + is a separate threat because an attacker who can replace a threshold can + already learn the seed; issue #55 tracks authenticated descriptor evidence for + that stronger threat model. +- Remediation: on every restore path, derive wallet identity without importing + descriptors. Prefer a verified encrypted descriptor backup when available; + otherwise require a matching wallet-record fingerprint (and later optional + single-sig descriptor checksum), or make the no-record visual confirmation an + explicit operator choice. Only then call the mutating wallet initialization + path. - Confidence: high. Reconfirmed against the Bails-pinned GUI commit, the current GUI descendant, and the supported `ms32 wallet` CLI path. -### Medium design concern — 32-bit fingerprint is too weak as an active recovery gate +### Threat-model disposition — 32-bit fingerprint is not a malicious-tampering defense -Tracking: issue #27. +Tracking: closed issue #27; stronger descriptor authentication is issue #55. - Affected component: wallet record/recovery identity design across restore front ends. -- Failure scenario: even if the ordering above is fixed, the current wallet - identity is only the four-byte BIP32 master fingerprint. An active adversary - able to search candidate seeds can target a 32-bit value; accidental - collisions also remain possible over sufficiently large populations. -- Property violated: a recovery authentication gate should have enough binding - strength that deliberate collision search is infeasible. -- Exploitability: the weakness is practical as a design limit, although a - targeted seed search still has real compute cost. It is not a break of BIP32. -- Remediation: store and verify an independent public commitment with at least - 128 bits of collision resistance before import. Keep the BIP32 fingerprint as - a human-friendly diagnostic, not the sole authorization value. -- Confidence: high on the weakness; medium on the final record format because - the product UX still needs a concrete commitment encoding. +- Observation: the four-byte BIP32 master fingerprint is intentionally a + human-scale identifier. A party choosing substitute seeds can grind a matching + value; the 20-bit backup identifier is weaker still. +- Release-gate disposition: this does not invalidate the fingerprint as an + accident-safety gate. Its job in #28/#57 is to catch wrong/mixed cards, + miscorrection and transcription mistakes before Core is changed. It is not + represented as authorization against a malicious party able to replace a + threshold of shares. +- Stronger design: issue #55 authenticates the full single-sig descriptor backup + with material derived from the recovered seed, verifies that the recovered + seed reproduces those descriptors before wallet mutation, and relies on + independently stored copies of that backup for tamper resistance. +- Human record follow-up: #43 adds checksummed/type-back record fields and may + accept a canonical single-sig descriptor checksum as another second-class + accident check. +- Confidence: high on the distinction between the two threat models; the exact + encrypted-backup format remains design work in #55. ## Confirmed BIP93/Codex32 application hazards @@ -150,8 +163,9 @@ not an upstream BIP93 vulnerability report. - Exploitability: practical once shares from compatible colliding/matched sets are available. The 20-bit identifier is a typo/mix-up aid, not a cryptographic set commitment. -- Remediation: applications must bind recovery to an independent wallet/public - commitment and reject a recovered secret that does not match it. +- Remediation: for accidental mix-ups, require the operator to identify the + intended wallet before import using the strongest evidence they actually + possess. Do not describe the identifier itself as authentication. - Confidence: high. ### Medium — valid-share substitution can force an attacker-chosen reconstruction @@ -161,13 +175,14 @@ not an upstream BIP93 vulnerability report. checksum-valid replacement shares that make the victim reconstruct a chosen target secret. The audit reproduced this for a 2-of-2 set. - Property violated: share integrity/provenance and intended-secret - authentication. + authentication when active substitution is in scope. - Exploitability: practical when the attacker can replace the required share - material. This is not a secrecy break; it is an authenticity failure if the - application treats checksum validity as authorization. -- Remediation: verify an independent public commitment before using recovered - key material; protect physical shares against substitution where that threat - is in scope. + material. This is not a secrecy break; such an attacker can already recover + the original seed from the threshold material they control. +- Remediation: keep the release-gate fingerprint/no-record UX scoped to accident + safety. Where malicious substitution is in scope, authenticate the complete + descriptor backup independently as designed in #55 and keep copies apart from + the recovery cards. - Confidence: high. ## Falsified candidate @@ -179,9 +194,13 @@ and is not a security finding. It must not be cited or tracked as one. For every wallet restore path built on python-codex32: -> Parsing, checksum validity, threshold reconstruction, and the BIP32 -> fingerprint are not authorization to mutate a wallet. A recovered seed must -> first match an independent, sufficiently strong public wallet commitment. +> Before any recovered key material mutates a wallet, the operator must +> authenticate that the recovered seed is the wallet they intend using the +> strongest evidence they actually have: a verified seed-keyed descriptor +> backup when available; otherwise a matching wallet-record fingerprint and/or +> canonical single-sig descriptor checksum; otherwise an explicit visual +> confirmation of the recovered fingerprint and codex32/Bails identifier result. -That invariant is the boundary needed to contain both accidental share-set -mixing and active share substitution. +The fingerprint, descriptor-checksum and no-record paths are accident-safety +controls. Malicious share/seed replacement is a separate threat model and uses +the authenticated encrypted descriptor backup tracked in #55. \ No newline at end of file From d07024a79c708b9e7928b79b7cc3b720411f78c0 Mon Sep 17 00:00:00 2001 From: Ben Westgate Date: Tue, 29 Sep 2026 01:39:47 -0500 Subject: [PATCH 4/4] docs: Clarify no-record restore authorization --- docs/audits/security-2026-09-22.md | 39 ++++++++++++++++++------------ 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/docs/audits/security-2026-09-22.md b/docs/audits/security-2026-09-22.md index bf3a52c..2b48cdf 100644 --- a/docs/audits/security-2026-09-22.md +++ b/docs/audits/security-2026-09-22.md @@ -28,12 +28,15 @@ intended reconstructed secret against active substitution. The release-gate rule for callers is therefore stricter than “valid Codex32 shares reconstruct a valid seed”: before recovered key material mutates a live -wallet, the operator must authenticate that the recovered seed is the wallet -they intended. For the current single-sig restore flow this is an accident-safety -check: a typed wallet-record fingerprint, or an explicit visual confirmation -when no record exists. Stronger resistance to malicious share/seed replacement -is a separate problem, tracked as an authenticated encrypted descriptor backup -in issue #55. +wallet, the operator must establish that the recovered seed is the wallet they +intend using the strongest independent evidence they actually have. For the +current single-sig restore flow this is an accident-safety check: normally a +typed wallet-record fingerprint; when no independent record exists, the product +instead makes that absence explicit and requires a last-resort visual +confirmation before the operator authorizes the restore. That no-record path is +not independent authentication of the recovered seed. Stronger resistance to +malicious share/seed replacement is a separate problem, tracked as an +authenticated encrypted descriptor backup in issue #55. ## Confirmed implementation findings @@ -194,13 +197,17 @@ and is not a security finding. It must not be cited or tracked as one. For every wallet restore path built on python-codex32: -> Before any recovered key material mutates a wallet, the operator must -> authenticate that the recovered seed is the wallet they intend using the -> strongest evidence they actually have: a verified seed-keyed descriptor -> backup when available; otherwise a matching wallet-record fingerprint and/or -> canonical single-sig descriptor checksum; otherwise an explicit visual -> confirmation of the recovered fingerprint and codex32/Bails identifier result. - -The fingerprint, descriptor-checksum and no-record paths are accident-safety -controls. Malicious share/seed replacement is a separate threat model and uses -the authenticated encrypted descriptor backup tracked in #55. \ No newline at end of file +> Before any recovered key material mutates a wallet, the operator must establish +> the intended wallet using the strongest independent evidence they actually +> have: a verified seed-keyed descriptor backup when available; otherwise a +> matching wallet-record fingerprint and/or canonical single-sig descriptor +> checksum. If no independent wallet record exists, the restore must state that +> identity cannot be authenticated and require an explicit last-resort visual +> confirmation of the recovered fingerprint and codex32/Bails identifier result +> before the operator authorizes mutation. + +The fingerprint and descriptor-checksum paths are human-scale accident-safety +checks. The no-record path is an explicit accident-safety waiver/authorization, +not independent authentication evidence. Malicious share/seed replacement is a +separate threat model and uses the authenticated encrypted descriptor backup +tracked in #55. \ No newline at end of file