Repository navigation
Conversation
…and verify (3.2.2) Certificates issued before 2.8.0 may carry a `type` shorter than 32 bytes. The 2.8.0 security merge made every wallet certificate validator, the listCertificates result validator and Certificate.toBinary() require exactly 32 bytes without recording a breaking change, so one legacy certificate failed an unfiltered listCertificates as a whole and could not be listed, proven, verified or relinquished (#1085). - validateListCertificatesArgs types, validateRelinquishCertificateArgs type and validateProveCertificateArgs certificate.type accept 1-32 canonical-base64 bytes; acquireCertificate (issuance and direct) keeps exactly 32 and the length error now says "exactly 32 bytes". - Wallet result validation accepts a 1-32-byte type on list, discovery, prove and acquisition results; serial numbers stay 32 bytes. - Certificate.verify() rebuilds the historical unpadded serialization through a shared serializer; toBinary(), fromBinary() and sign() stay canonical. The certifier signing key is derived from the exact type and serial number, so a shorter type cannot be confused with a canonical one. - WalletWireTransceiver rejects a legacy type before writing a request and WalletWireProcessor rejects one in list, discovery and acquisition results with an explicit error instead of misaligning the frame. - Add SDK regression tests, a wallet-toolbox sqlite test for a stored legacy certificate, changelog, release-note, baseline and package documentation updates; bump @bsv/sdk to 3.2.2. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…wire guard - verify() returns false for a non-canonical, empty or oversized type, covering assertHistoricalCertificateType's rejection paths. - The binary wire rejects a proveCertificate request whose certificate has no type before invoking the wallet. - WalletWireProcessor's certificate type guard takes a string: result validation already rejects a non-string type, so the extra branch was unreachable. - validateCertificate checks each encoded field through a helper with a descriptive error instead of an index loop and a bare Error (Sonar S4138, S7722). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Wallet-side fix and the keyID trust argument check out: short types verify under the historical preimage, non-canonical/padded spellings fail, sign/toBinary/acquire and the binary wire stay at 32 bytes. Through Blocker
Legacy upper bound
Tests
Docs
|
|
I recommend closing this PR. [BRC-52 specifies a 32-byte certificate type](https://github.com/bsv-blockchain/BRCs/blob/master/peer-to-peer/0052.md), and I think we should enforce that consistently across the normal certificate APIs and transports. I confirmed the legacy issue with PeerCert. PeerCert was issuing non-compliant types, so we updated it to conform. Other affected apps should do the same. Older SDKs accepting those values doesn’t make the certificates compliant with BRC-52. This PR keeps new issuance strict, but restores support for short types through JSON while they remain unsupported by binary. I understand that this is intentional compatibility behavior, but that leaves the same certificate usable through one transport and unusable through another which is not a good approach. I’d rather handle legacy recovery explicitly than create this ambiguity or force a breaking change on the binary substrate. Human-readable names can stay separate from the type ID. For example: import { Hash, Utils } from '@bsv/sdk'
const schema = 'example.org/skill-endorsement/v1'
const type = Utils.toBase64(Hash.sha256(schema, 'utf8'))
const registry = {
[type]: { name: 'Skill endorsement', schema }
}That produces exactly 32 bytes before base64 encoding. Apps can maintain the label/schema mapping locally or use something like CertMap. Registration isn’t required to list, filter, or use the certificate. Existing signed certificates can’t be fixed by padding or hashing their stored type as that changes what was signed. We should document the historical validation change and provide an explicit recovery/export path if there are wallets holding non-compliant BRC-52 certificates. |
|
Agree with Brayden on direction. The goal for #1085 was a way to migrate legacy certificates, not ongoing support for them across APIs and transports, and my previous comment didn't make that clear. Dropping the Peer/BRC-103 point. Whatever the migration ends up looking like, one thing should ship with it: |
|
I agree that this should probably be closed, we just need to have a migration path so that issuers will issue valid types in the future and allow people who hold an invalid one to swap it out, then omit broken ones from the list. The 32 byte type ID should generally be a hash or randomly generated value, any metadata should be defined separately from the type as in BRC184. |
…-52 types Supersedes the transition-window approach on this branch, per review on #1100 (#1085): the normal certificate APIs and transports stay strict at the BRC-52 32-byte type; the change is how a wallet handles records it already holds from before @bsv/sdk 2.8.0. @bsv/sdk 3.3.0: - listCertificates and discovery result validation leave out a certificate whose type is canonical base64 of 1-128 bytes other than 32 and that is valid in every other respect, lowering totalCertificates, instead of rejecting the whole result; the binary wallet wire serializes the same validated result. Any other malformed certificate still rejects. - New recovery-only Certificate.verifyHistoricalSignature() rebuilds the pre-2.8.0 serialization for such a type and verifies the certifier signature; verify(), sign(), toBinary() and fromBinary() are unchanged. - An exact-length validation failure reads "exactly 32 bytes". @bsv/wallet-toolbox 2.15.0: - FindCertificatesArgs.legacyTypes ('exclude' | 'only') on Knex and IndexedDB; listCertificates excludes non-compliant records at the query. - Wallet.listLegacyCertificates() and Wallet.relinquishLegacyCertificate() (outside BRC-100, Wallet instance only); storage-level relinquish validates a legacy-tolerant reference while the BRC-100 method and the storage manager keep the strict validator. Docs: sdk/toolbox changelogs, release notes (sdk minor), package pages, @bsv/simple guide and api-reference, regenerated ledger and stack facts, repository-health baseline. Tests cover the listing omission (JSON and wire), the historical verification bounds, and the sqlite recovery flow. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…he release-notes ledger The toolbox 2.15.0 row now describes FindCertificatesArgs.legacyTypes, listLegacyCertificates/relinquishLegacyCertificate, the storage-level tolerant relinquish and the remote storage upgrade note, matching its changelog and package page; the migration ledger is regenerated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
@ty-everett @BraydenLangley @kjartan221 — reworked per your direction. The diff against
Kjartan — under this design the Peer/BRC-103 and >32-byte points fall away (requested sets stay strict; the recovery bound is 1–128 bytes so simple's pre-0.6 schema-id types are listable), and "one bad row must not fail the listing" is the core of it. The 1-byte lower bound and the discriminating Local evidence is in the PR body; hosted checks on the new head are pending. |
…ertificate-types # Conflicts: # docs/reference/package-api-migrations.md
- validateCertificatesResult: extract requested-set builders, per-entry binding and discovery validation into helpers (cognitive complexity S3776) and use an intrinsic Math.max for the adjusted total (S7766). - Certificate.verifyHistoricalSignature(): return a resolved Promise from a synchronous check instead of an async method with no await (S7503); behaviour unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…o worktree-sdk-legacy-certificate-types # Conflicts: # docs/packages/sdk/bsv-sdk.md # docs/reference/package-api-migrations.md # docs/reference/stack-facts.md # governance/package-release-notes.json # governance/repository-health/baselines.json # packages/sdk/CHANGELOG.md # packages/sdk/package.json
- legacyCertificates helpers: canonical/legacy/non-canonical/non-string types; every validateLegacyCertificateReference rejection including an off-curve certifier. - StorageIdb: legacyTypes 'exclude' / 'only' on findCertificates and countCertificates. - Wallet.listLegacyCertificates: paging, and rows an older storage server returns without the filter or fields. - Certificate.verifyHistoricalSignature: unsigned certificate is unverified. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|



Program and scope
@bsv/sdk2.8.0 under non-BRC-52 typesb3155fa2e) made every wallet certificate validator, the list/discovery result validator, BRC-103 requested sets andCertificate.toBinary()require the BRC-52 32-bytetypewithout recording a breaking change. Nothing bounded the type before 2.8.0 and@bsv/simplebefore 0.6 derived it from a schema identifier, so wallets hold certificates issued under non-compliant types. Today one such stored record makes an unfilteredlistCertificatesfail as a whole for apps that never used those types, and there is no way to find or retire the record. Reviewers (ty-everett, BraydenLangley, kjartan221) agreed on direction: keep the normal APIs strict, omit broken records from listings, and provide an explicit recovery path so issuers reissue valid types and holders swap the old record out.mainis what counts); changing the BRC-52 binary layout or the binary wallet wire; rewriting stored records; regenerating the stale ts2md reference underpackages/sdk/docs/reference.7cc959e76(b8de96fc9rework,9143944f6ledger follow-up,4d2c0fd94Sonar refactors, merges ofmainat9efb82756andda58fb725,7cc959e76coverage tests; earlier commits on the branch are superseded)What changes
@bsv/sdk4.0.0 candidate (main already carries the 4.0.0 major candidate from feat: @bsv/sdk 4.0 module balances and BRC-207 color permission module #1243; this change is additive within it and is recorded under its own "Legacy certificate recovery (4.0.0 candidate)" changelog section)validationHelpers.ts: an exact-length base64 failure readsexactly 32 bytesinstead ofat least 32 bytes. No validator accepts a non-32-byte type.WalletResultValidation.ts:listCertificates,discoverByIdentityKeyanddiscoverByAttributesresults leave out a certificate whosetypeis canonical base64 that does not decode to 32 bytes (up to 128, matching@bsv/simple's legacy tolerance) and that validates in every other respect;totalCertificatesis reduced by the omitted count, never below the entries kept. Any other malformed certificate still rejects the whole result; acquisition and proof results are unchanged.WalletWireProcessorserializes the same validated result, so JSON and binary substrates behave identically (no transport asymmetry).Certificate.ts:toBinary()and verification share one#serialize;verify(),sign(),toBinary()andfromBinary()are unchanged. New recovery-onlyverifyHistoricalSignature()rebuilds the pre-2.8.0 serialization (decoded type bytes written unpadded) for a 1–128-byte canonical-base64 type and verifies the certifier signature, whose key is still derived from the exacttypeandserialNumber; a certificate passing it is not thereby compliant.@bsv/wallet-toolbox2.15.0 (rides the existing minor candidate;client/mobilealready 2.15.0)FindCertificatesArgs.legacyTypes?: 'exclude' | 'only': Knexlength(type) = 44/<> 44, the same test on IndexedDB;countCertificatesinherits it.listCertificatespasses'exclude', so non-compliant records are omitted at the storage query.Wallet.listLegacyCertificates({ limit?, offset? })returns the non-compliant records with fields and master keyring in thelistCertificatesshape (filtered again locally for a storage server that predateslegacyTypes);Wallet.relinquishLegacyCertificate({ type, serialNumber, certifier })retires one. Both are outside BRC-100 and only reachable on theWalletinstance (WalletPermissionsManagerdoes not forward them).relinquishCertificatevalidates the reference with a legacy-tolerant type (1–128 bytes) so the recovery path works over existing remoting (findCertificatesAuth,relinquishCertificate); the BRC-100Wallet.relinquishCertificateandWalletStorageManager.relinquishCertificatekeep the strict SDK validator. A remote storage server must be upgraded together with the wallet.patch→minor, toolbox and simple rows);docs/packages/sdk/bsv-sdk.md"Legacy certificate types since 4.0.0";docs/packages/wallet/wallet-toolbox.md"Legacy certificate recovery"; corrected@bsv/simpleguide/api-reference passages; regenerated migration ledger and stack facts; repository-health baseline.Impact
Certificate.verifyHistoricalSignature(); newWallet.listLegacyCertificates()/relinquishLegacyCertificate();FindCertificatesArgs.legacyTypes; list/discovery results omit non-compliant records; exact-length error text)Affected packages/services and intended patch versions (publication occurs only
through the release workflow after approval):
@bsv/sdk3.2.1 → 4.0.0 (main's existing major candidate; this PR addsCertificate.verifyHistoricalSignature()and the listing change to it, no further bump)@bsv/wallet-toolbox2.14.9 → 2.15.0 (existing minor candidate;@bsv/wallet-toolbox-clientand-mobilealready at 2.15.0)Verification
origin/main(latest merge commit9efb82756), Node 24.15, pnpm frozen install):packages/sdk/src/wallet/__tests/NonCompliantCertificateType.test.ts(exact-length messages; listing omission with total adjustment for list, discovery and the binary wire; garbage still rejected; acquisition/proof unchanged),packages/sdk/src/auth/certificates/__tests/Certificate.historicalSignature.test.ts(1/16/39/128-byte historically signed types verify only throughverifyHistoricalSignature(); tampered, zero-padded, 129-byte, empty, non-canonical and non-base64 types rejected even when their own preimage was signed; sign/toBinary/fromBinary stay canonical),packages/wallet/wallet-toolbox/src/__tests__/Wallet.legacyCertificateRecovery.test.ts(sqlite: legacy + canonical rows →listCertificatesreturns the canonical one with total 1;listLegacyCertificatesreturns the legacy one with fields and keyring; BRC-100relinquishCertificaterejects it;relinquishLegacyCertificateretires it; serial/certifier still validated).pnpm --filter @bsv/sdk exec jeston the result-validation, wire, certificate and auth suites plus the new files — 697/697 (17 suites). Full sdk suite: 217 suites, 7677/7677 passed.pnpm --filter @bsv/wallet-toolbox exec jeston the recovery, acquireCertificate and storage-authorization suites — 26/26. Governedpnpm --filter @bsv/wallet-toolbox test: 301 suites passed, 2 governed skips; 3321/3329 passed, 7 governed skips, 1 failure (StorageClient.test.ts,fetch failedagainst its local server on port 8042 — a network flake unrelated to this change; the file passes alone, 11/11).pnpm --filter @bsv/simple exec jest: 26 suites, 371/371 passed.tsc -b(sdk),pnpm --filter @bsv/wallet-toolbox run typecheck,oxlintfor both packages, prettier — clean. Rootpnpm build,pnpm typecheck,pnpm lint,pnpm format:check,pnpm health:check: all pass on this head (pnpm build,pnpm typecheck,pnpm lint,pnpm format:check;pnpm health:checkwith 0 control errors, run alone after one load-induced flake of its local-registry peer-resolution test);pnpm --filter @bsv/sdk run pack:checkandpnpm --filter @bsv/wallet-toolbox run pack:checkpass.pnpm docs:factsregenerated the migration ledger and stack facts;pnpm docs:facts:check— pass.7cc959e76, tests only, after codecov/patch reported 89.6% onda58fb725):legacyCertificates.ts100% (incl. the off-curve certifier rejection),StorageIdblegacyTypesexclude/only on find and count under fake-indexeddb,Wallet.listLegacyCertificatespaging and the older-storage-server path,verifyHistoricalSignature()on an unsigned certificate. sdk certificate suites 70/70; toolbox recovery/helper/Idb suites green; lint and prettier clean.9efb82756failed in the shared prepare step atpnpm audit:securityon two HIGH advisories for transitivefollow-redirects1.16.0 (GHSA-q3jh-wrc6-jgq8, GHSA-8r9p-f939-6h3c, viabtms-backend → mongodb-memory-server), unrelated to this change and remediated onmainby fix(deps): lock follow-redirects 1.16.1 for the 2026-10-20 high advisories #1270; the three new Sonar findings on that head (S3776, S7503, S7766) are fixed in4d2c0fd94.mainwill be merged again once fix(deps): lock follow-redirects 1.16.1 for the 2026-10-20 high advisories #1270 lands so the full matrix runs on the final head.pnpm --filter @bsv/sdk run pack:checkandscripts/check-browser-package.mjspassed on the earlier head;packages/sdk/scripts/check-umd-contract.mjsfails locally only because npm 12 printsnpm pack --jsonas an object (environmental; the hostedtest:browserjob is authoritative).compatibility, public API, artifacts, dependencies, docs, and operations
scope-based skip is expected and validated by the merge gate
Security and dependencies
audit results were reviewed (not applicable: no dependency change)
(including accepted or false-positive issue states) and zero unreviewed hotspots;
Sonar's aggregate
Quality Gate passedverdict alone is not merge evidence (pending)and removal condition (none added)
Dependency evidence
Not applicable: no dependency change.
@bsv/sdk4.0.0 (existing candidate),@bsv/wallet-toolbox2.15.0Release and operations
controlling program
Completion evidence
passing checks, resolved alerts, measurements, or an approved exception
guidance are current or concretely not applicable
being handed to another contributor as “complete”
restriction is assumed
🤖 Generated with Claude Code