bsv-sdk and bsv-wallet persist unverified certifier signatures in acquire_certificate (direct and issuance paths)
Published: April 09, 2026
SECURITY IDENTIFIERS
- CVE: CVE-2026-40070 (NVD)
- GHSA: GHSA-hc36-c89j-5f4j
- Vendor Advisory: https://github.com/sgbett/bsv-ruby-sdk/security/advisories/GHSA-hc36-c89j-5f4j
GEM
SEVERITY
CVSS v3.x: 8.1 (High)
UNAFFECTED VERSIONS
< 0.1.2
PATCHED VERSIONS
>= 0.3.4
DESCRIPTION
Unverified certifier signatures persisted by acquire_certificate
Affected packages
Both bsv-sdk and bsv-wallet are published from the
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.
| Package | Affected | Patched |
|---|---|---|
bsv-sdk |
>= 0.3.1, < 0.8.2 |
0.8.2 |
bsv-wallet |
>= 0.1.2, < 0.3.4 |
0.3.4 |
Summary
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:
acquisition_protocol: 'direct'— the caller supplies all certificate fields (includingsignature:) and the record is written to storage verbatim.acquisition_protocol: 'issuance'— the client POSTs to a certifier URL and writes whatever signature the response body contains, also without verification.
An 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.
Details
BRC-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().
Direct path
The Ruby implementation's acquire_via_direct path
(lib/bsv/wallet_interface/wallet_client.rb) constructs the
certificate record directly from caller-supplied fields:
def acquire_via_direct(args)
{
type: args[:type],
subject: @key_deriver.identity_key,
serial_number: args[:serial_number],
certifier: args[:certifier],
revocation_outpoint: args[:revocation_outpoint],
signature: args[:signature],
fields: args[:fields],
keyring: args[:keyring_for_subject]
}
end
The 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.
Issuance path
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.
Downstream impact
Downstream 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.
Impact
Any 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.
This is a credential-forgery primitive, not merely a spec divergence from BRC-52.
CVSS rationale
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N → 8.1 (High)
- AV:N — network-reachable in any wallet context that
exposes
acquire_certificateto callers. - AC:L — low attack complexity: pass arbitrary bytes as
signature:. - PR:L — low privileges: any caller authorised to invoke
acquire_certificate. - UI:N — no user interaction required.
- C:H — forged credentials via
prove_certificatecan assert attributes about the subject. - I:H — the wallet's credential store is polluted with attacker-controlled data.
- A:N — availability unaffected.
Proof of concept
client = BSV::Wallet::WalletClient.new(key,
storage: BSV::Wallet::MemoryStore.new)
client.acquire_certificate(
type: 'age-over-18',
acquisition_protocol: 'direct',
certifier: claimed_trusted_pubkey_hex,
serial_number: 'any-serial',
revocation_outpoint: ('00' * 32) + '.0',
signature: 'deadbeef' * 16, # arbitrary bytes — never verified
fields: { 'verified' => 'true' },
keyring_for_subject: {}
)
client.list_certificates(
certifiers: [claimed_trusted_pubkey_hex],
types: ['age-over-18']
)
# => returns the forged record as if it were a real certificate
# from that certifier
Affected versions
The 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.
bsv-sdk: the v0.3.1 chore bump (89de3a2) was committed
8 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.
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.
Patches
Upgrade 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.
Consumers 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.
The 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.
Fixed in sgbett/bsv-ruby-sdk#306.
Workarounds
If upgrading is not immediately possible:
- Do not expose
acquire_certificate(either acquisition protocol) to untrusted callers. - Do not invoke
acquire_certificatewithacquisition_protocol: 'issuance'against a certifier URL you do not fully trust, and require TLS for any such request. - Treat any record returned by
list_certificates/prove_certificateas unverified and perform an out-of-band BRC-52 verification against the certifier's public key before acting on it.
Credit
Identified during the 2026-04-08 cross-SDK compliance review, tracked as findings F8.15 (direct path) and F8.16 (issuance path, partial).
RELATED
- https://nvd.nist.gov/vuln/detail/CVE-2026-40070
- https://github.com/sgbett/bsv-ruby-sdk/security/advisories/GHSA-hc36-c89j-5f4j
- https://github.com/sgbett/bsv-ruby-sdk/pull/306
- https://github.com/sgbett/bsv-ruby-sdk/commit/4992e8a265fd914a7eeb0405c69d1ff0122a84cc
- https://github.com/sgbett/bsv-ruby-sdk/issues/305
- https://bsv.brc.dev/peer-to-peer/0052
- https://advisories.gitlab.com/pkg/gem/bsv-wallet/CVE-2026-40070
- https://github.com/advisories/GHSA-hc36-c89j-5f4j
