-
Notifications
You must be signed in to change notification settings - Fork 2
docs: Record security audit verdict #23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
BenWestgate
wants to merge
4
commits into
fix-hrp-83-limit
Choose a base branch
from
20-security-audit-docs
base: fix-hrp-83-limit
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
1201944
docs: Record security audit verdict
BenWestgate 654039b
docs: Link audited GUI revisions
BenWestgate 31598f0
docs: Separate recovery accident and tampering checks
BenWestgate d07024a
docs: Clarify no-record restore authorization
BenWestgate File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,213 @@ | ||
| # Security audit verdict — 2026-09-22 | ||
|
|
||
| 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 | ||
| [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 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 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 | ||
|
|
||
| ### Medium — unbounded correction alignment cache | ||
|
|
||
| Tracking: [#21](https://github.com/BenWestgate/python-codex32/issues/21). | ||
|
BenWestgate marked this conversation as resolved.
|
||
|
|
||
| - 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: 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. | ||
|
|
||
| ### Threat-model disposition — 32-bit fingerprint is not a malicious-tampering defense | ||
|
|
||
| Tracking: closed issue #27; stronger descriptor authentication is issue #55. | ||
|
|
||
| - Affected component: wallet record/recovery identity design across restore | ||
| front ends. | ||
| - 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 | ||
|
|
||
| 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: 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 | ||
|
|
||
| - 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 when active substitution is in scope. | ||
| - Exploitability: practical when the attacker can replace the required share | ||
| 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 | ||
|
|
||
| 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: | ||
|
|
||
| > 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. | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.