{"id":"CVE-2026-40070","aliases":["GHSA-hc36-c89j-5f4j"],"url":"https://o3.security/vulnerability/CVE-2026-40070","summary":"bsv-sdk and bsv-wallet persist unverified certifier signatures in acquire_certificate (direct and issuance paths)","details":"# Unverified certifier signatures persisted by `acquire_certificate`\n\n## Affected packages\n\nBoth `bsv-sdk` and `bsv-wallet` are published from the [sgbett/bsv-ruby-sdk](https://github.com/sgbett/bsv-ruby-sdk) repository. The vulnerable code lives in `lib/bsv/wallet_interface/wallet_client.rb`, which is **physically shipped inside both gems** (the `bsv-wallet.gemspec` `files` list bundles the entire `lib/bsv/wallet_interface/` tree). Consumers of either gem are independently vulnerable; the two packages are versioned separately, so each has its own affected range.\n\n| Package | Affected | Patched |\n| --- | --- | --- |\n| `bsv-sdk` | `>= 0.3.1, < 0.8.2` | `0.8.2` |\n| `bsv-wallet` | `>= 0.1.2, < 0.3.4` | `0.3.4` |\n\n## Summary\n\n`BSV::Wallet::WalletClient#acquire_certificate` persists certificate records to storage **without verifying the certifier's signature** over the certificate contents. Both acquisition paths are affected:\n\n- `acquisition_protocol: 'direct'` — the caller supplies all certificate fields (including `signature:`) and the record is written to storage verbatim.\n- `acquisition_protocol: 'issuance'` — the client POSTs to a certifier URL and writes whatever signature the response body contains, also without verification.\n\nAn attacker who can reach either API (or who controls a certifier endpoint targeted by the issuance path) can forge identity certificates that subsequently appear authentic to `list_certificates` and `prove_certificate`.\n\n## Details\n\nBRC-52 requires a certificate's `signature` field to be verified against the claimed certifier's public key over a canonical hashing of `(type, subject, serialNumber, revocationOutpoint, fields)` before the certificate is trusted. The reference TypeScript SDK enforces this in `Certificate.verify()`.\n\n### Direct path\n\nThe Ruby implementation's `acquire_via_direct` path (`lib/bsv/wallet_interface/wallet_client.rb`) constructs the certificate record directly from caller-supplied fields:\n\n```ruby\ndef acquire_via_direct(args)\n  {\n    type: args[:type],\n    subject: @key_deriver.identity_key,\n    serial_number: args[:serial_number],\n    certifier: args[:certifier],\n    revocation_outpoint: args[:revocation_outpoint],\n    signature: args[:signature],\n    fields: args[:fields],\n    keyring: args[:keyring_for_subject]\n  }\nend\n```\n\nThe returned record is then written to the storage adapter by `acquire_certificate`. No verification of `args[:signature]` against `args[:certifier]`'s public key occurs at any point in this path.\n\n### Issuance path\n\n`acquire_via_issuance` POSTs to a certifier-supplied URL and parses the response body into a certificate record, which is then written to storage without verifying the returned signature. A hostile or compromised certifier endpoint — or anyone able to redirect/MITM the plain HTTP request — can therefore return an arbitrary `signature` value for any subject and have it stored as authentic. This is the same class of bypass as the direct path; it was tracked separately as finding **F8.16** in the compliance review and is closed by the same fix.\n\n### Downstream impact\n\nDownstream reads via `list_certificates` and selective-disclosure via `prove_certificate` treat stored records as valid without re-verifying, so any forgery that slips past `acquire_certificate` is trusted permanently.\n\n## Impact\n\nAny caller that can invoke `acquire_certificate` — via either acquisition protocol — can forge a certificate attributed to an arbitrary certifier identity key, containing arbitrary fields, and have it persisted as authentic. Applications and downstream gems that rely on the wallet's certificate store as a source of truth for identity attributes (e.g. KYC assertions, role claims, attestations) are subject to credential forgery.\n\nThis is a credential-forgery primitive, not merely a spec divergence from BRC-52.\n\n## CVSS rationale\n\n`AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N` → **8.1 (High)**\n\n- **AV:N** — network-reachable in any wallet context that exposes `acquire_certificate` to callers.\n- **AC:L** — low attack complexity: pass arbitrary bytes as `signature:`.\n- **PR:L** — low privileges: any caller authorised to invoke `acquire_certificate`.\n- **UI:N** — no user interaction required.\n- **C:H** — forged credentials via `prove_certificate` can assert attributes about the subject.\n- **I:H** — the wallet's credential store is polluted with attacker-controlled data.\n- **A:N** — availability unaffected.\n\n## Proof of concept\n\n```ruby\nclient = BSV::Wallet::WalletClient.new(key, storage: BSV::Wallet::MemoryStore.new)\n\nclient.acquire_certificate(\n  type: 'age-over-18',\n  acquisition_protocol: 'direct',\n  certifier: claimed_trusted_pubkey_hex,\n  serial_number: 'any-serial',\n  revocation_outpoint: ('00' * 32) + '.0',\n  signature: 'deadbeef' * 16,       # arbitrary bytes — never verified\n  fields: { 'verified' => 'true' },\n  keyring_for_subject: {}\n)\n\nclient.list_certificates(\n  certifiers: [claimed_trusted_pubkey_hex],\n  types: ['age-over-18']\n)\n# => returns the forged record as if it were a real certificate from that certifier\n```\n\n## Affected versions\n\nThe vulnerable direct-path code was introduced in commit `d14dd19` (\"feat(wallet): implement BRC-100 identity certificate methods (Phase 5)\") on 2026-03-27 20:35 UTC. The vulnerable issuance-path code was added one day later in `6a4d898` (\"feat(wallet): implement certificate issuance protocol\", 2026-03-28 04:38 UTC), which removed an earlier `raise UnsupportedActionError` and replaced it with an unverified HTTP POST.\n\n**`bsv-sdk`:** the v0.3.1 chore bump (`89de3a2`) was committed 28 minutes after `d14dd19`, so the direct-path bypass shipped in the **v0.3.1** tag. The v0.3.1 release raised `UnsupportedActionError` for the issuance path, so the issuance-path bypass first shipped in **v0.3.2** (`5a335de`). Every subsequent release up to and including **v0.8.1** is affected by at least one path, and every release from v0.3.2 onwards is affected by both. Combined affected range: `>= 0.3.1, < 0.8.2`.\n\n**`bsv-wallet`:** at the time both commits landed, the wallet gem was at version 0.1.1. The first wallet release containing any of the vulnerable code was **v0.1.2** (`5a335de`, 2026-03-30), which shipped both paths simultaneously. Every subsequent release up to and including **v0.3.3** is affected on both paths. Affected range: `>= 0.1.2, < 0.3.4`.\n\n## Patches\n\nUpgrade to `bsv-sdk >= 0.8.2` **and/or** `bsv-wallet >= 0.3.4`. Both releases ship the same fix: a new module `BSV::Wallet::CertificateSignature` (`lib/bsv/wallet_interface/certificate_signature.rb`), which builds the BRC-52 canonical preimage (`type`, `serial_number`, `subject`, `certifier`, `revocation_outpoint`, lexicographically-sorted `fields`) and verifies the certifier's signature against it via `ProtoWallet#verify_signature` with protocol ID `[2, 'certificate signature']` and counterparty = the claimed certifier's public key. Both `acquire_via_direct` and `acquire_via_issuance` now call `CertificateSignature.verify!` before returning the certificate to `acquire_certificate`, so invalid certificates raise `BSV::Wallet::CertificateSignature::InvalidError` (a subclass of `InvalidSignatureError`) and are never written to storage.\n\nConsumers should upgrade whichever gem they depend on directly; they do not need both. `bsv-wallet 0.3.4` additionally tightens its dependency on `bsv-sdk` from the stale `~> 0.4` to `>= 0.8.2, < 1.0`, which forces the known-good pairing and pulls in the sibling advisory fixes (F1.3, F5.13) tracked separately.\n\nThe issuance-path fix also partially closes finding **F8.16** from the same compliance review. F8.16's second aspect — switching the issuance transport from ad-hoc JSON POST to BRC-104 AuthFetch — is not addressed here and remains deferred to a future release.\n\nFixed in sgbett/bsv-ruby-sdk#306.\n\n## Workarounds\n\nIf upgrading is not immediately possible:\n\n- Do not expose `acquire_certificate` (either acquisition protocol) to untrusted callers.\n- Do not invoke `acquire_certificate` with `acquisition_protocol: 'issuance'` against a certifier URL you do not fully trust, and require TLS for any such request.\n- Treat any record returned by `list_certificates` / `prove_certificate` as unverified and perform an out-of-band BRC-52 verification against the certifier's public key before acting on it.\n\n## Credit\n\nIdentified during the 2026-04-08 cross-SDK compliance review, tracked as findings F8.15 (direct path) and F8.16 (issuance path, partial).\n\n## References\n\n- HLR: sgbett/bsv-ruby-sdk#305\n- Fix PR: sgbett/bsv-ruby-sdk#306\n- Compliance review: [`.architecture/reviews/20260408-cross-sdk-compliance-review.md`](../../.architecture/reviews/20260408-cross-sdk-compliance-review.md)\n- BRC-52 specification: https://brc.dev/52\n- TypeScript reference: `Certificate.verify()` in `@bsv/sdk`","published":"2026-04-09T17:26:51.495Z","modified":"2026-08-12T03:51:39.685452387Z","cvss":{"score":8.1,"severity":"HIGH","vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N"},"epss":null,"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"RubyGems","name":"bsv-sdk","fixedVersion":"0.8.2"},{"ecosystem":"RubyGems","name":"bsv-wallet","fixedVersion":"0.3.4"}],"fix":{"url":"https://github.com/sgbett/bsv-ruby-sdk/commit/4992e8a265fd914a7eeb0405c69d1ff0122a84cc","label":"sgbett/bsv-ruby-sdk@4992e8a"},"references":[{"type":"WEB","url":"https://brc.dev/52"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/40xxx/CVE-2026-40070.json"},{"type":"ADVISORY","url":"https://github.com/sgbett/bsv-ruby-sdk/security/advisories/GHSA-hc36-c89j-5f4j"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-40070"},{"type":"REPORT","url":"https://github.com/sgbett/bsv-ruby-sdk/issues/305"},{"type":"FIX","url":"https://github.com/sgbett/bsv-ruby-sdk/commit/4992e8a265fd914a7eeb0405c69d1ff0122a84cc"},{"type":"FIX","url":"https://github.com/sgbett/bsv-ruby-sdk/pull/306"},{"type":"WEB","url":"https://github.com/rubysec/ruby-advisory-db/blob/master/gems/bsv-sdk/CVE-2026-40070.yml"},{"type":"WEB","url":"https://github.com/rubysec/ruby-advisory-db/blob/master/gems/bsv-wallet/CVE-2026-40070.yml"},{"type":"PACKAGE","url":"https://github.com/sgbett/bsv-ruby-sdk"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:39.685452387Z"}}