feat(keystore): sign through the backend — the key never leaves (#46, B.2) - #67
Open
SuperstrongBE wants to merge 1 commit into
Open
feat(keystore): sign through the backend — the key never leaves (#46, B.2)#67SuperstrongBE wants to merge 1 commit into
SuperstrongBE wants to merge 1 commit into
Conversation
… B.2) The XPR signer no longer receives key material. Signing now flows through KeystoreBackend.signDigest: - keystore/k1.ts: K1 primitives over the backend's STORAGE formats — PVT_K1_/legacy-WIF decode (checksum-verified), PUB_K1/legacy-EOS derivation, and Antelope-canonical signing (RFC6979 ground via extraEntropy until r/s clear the canonicality bits). Built on @noble/curves + @scure/base (audited, zero-dep). - EncryptedFileKeystore implements the real signDigest (65-byte [recoveryId, r, s], scalar decoded per call and wiped) and verifyKeyBinding (#39: derives the pubkey in-backend and compares to the declared PUB_K1/EOS string — no export). The TRANSITIONAL withSecret escape hatch is deleted. - chains/xpr/signatureProvider.ts: KeystoreSignatureProvider computes the Antelope signing digest (chainId ‖ packed_trx ‖ cfd|zero32), asks the backend to sign, and adds the header byte + SIG_K1 encoding. - XprSignerOptions: privateKeyProvider is GONE — the signer takes the KeystoreBackend; apiFactory's seam is now (key, options). The registry's createSigner takes the backend; the daemon runner passes it straight through. PrivateKeyProvider is deleted from the registry. Crypto validated empirically before landing: 200 random digests signed with the noble path all verify AND recover through @proton/js's own Signature/PublicKey classes; pub derivation is byte-identical to the SDK's. The permanent parity suite (test/k1-signing.test.ts) pins 50 digests + derivation parity + canonicality bits + malformed-input refusals. Signer tests now run against a REAL unlocked encrypted-file backend end to end. Grep-proof: no privateKeyProvider / withSecret / wif decode / JsSignatureProvider on the signing path outside src/keystore (keygen's JsSignatureProvider remains — key GENERATION, before any keystore exists). 238 tests green. Part of #46. INV-002 now extends to "no key material across the keystore boundary at all" on the runtime signing path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RJ7hqFnC9Nnyf2Db2h3WTb
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
PR B.2 of the implementation plan (Wave 1, Track B) — the core deliverable of #46: the private key no longer crosses the keystore boundary, even locally.
What changed
privateKeyProviderhands the WIF toJsSignatureProviderKeystoreBackend; a newKeystoreSignatureProvidercomputes the Antelope digest and callsbackend.signDigestEncryptedFileKeystorewithSecretescape hatchsignDigest(65-byte[recoveryId, r, s], scalar decoded per call and wiped) +verifyKeyBinding(#39: pubkey derived in-backend, compared to the declaredPUB_K1_/legacyEOSstring)createSigner(wiring, PrivateKeyProvider)createSigner(wiring, KeystoreBackend);PrivateKeyProviderdeletedsrc/keystore/k1.ts: PVT_K1_/legacy-WIF decode (checksum-verified), PUB_K1/EOS derivation, Antelope-canonical signing (RFC6979 ground viaextraEntropyuntil r/s clear the canonicality bits)New deps: @noble/curves + @scure/base (audited, zero-dep, the standard curve stack). Existing keystore files load unchanged — both stored secret formats (
PVT_K1_…from keygen, legacy5…WIF) are supported.Crypto validation — before landing, not after
@proton/js's ownSignature/PublicKeyclasses; public-key derivation byte-identical to the SDK's (PUB_K1_6MRyAjQq8ud7…from the canonical test WIF).test/k1-signing.test.ts: 50-digest verify+recover loop, derivation parity (SDK + 5 fresh keygen pairs), canonicality bits, malformed WIF/digest refusals, legacy-EOS derivation.test/xpr-signer.test.tsnow runs the signer against a real unlocked encrypted-file backend end to end (same 9 behavioral tests — WYSIWYS round-trip guard, INV-009/013/014 — all green).Grep-proof
No
privateKeyProvider/withSecret/ WIF decode /JsSignatureProvideron the signing path outsidesrc/keystore/. The one remainingJsSignatureProvideriskeygen.ts— key generation (a brand-new key, before any keystore exists), not signing.Numbers
238/238 tests · typecheck ✅ · build + CLI smoke ✅
This completes the #46 Phase-A trio (B.1 interface #66 → B.2 here). Vault/KMS backends are now drop-ins (Phase B). INV-002 extends to "no key material across the keystore boundary at all" on the runtime signing path.
🤖 Generated with Claude Code
https://claude.ai/code/session_01RJ7hqFnC9Nnyf2Db2h3WTb