From 888b64cfeac7cdc72dd911652223a8dd404b6aae Mon Sep 17 00:00:00 2001 From: Gustavo Stingelin Date: Wed, 24 Jun 2026 17:06:08 -0300 Subject: [PATCH 1/4] wallet: move vault mock under mock namespace --- wallet/account_derive_test.go | 12 ++++++------ wallet/common_test.go | 5 +++-- wallet/internal/bwtest/mock/{ => vault}/vault.go | 2 +- wallet/signer_test.go | 6 +++--- 4 files changed, 13 insertions(+), 12 deletions(-) rename wallet/internal/bwtest/mock/{ => vault}/vault.go (99%) diff --git a/wallet/account_derive_test.go b/wallet/account_derive_test.go index ca75c82f57..40960409e7 100644 --- a/wallet/account_derive_test.go +++ b/wallet/account_derive_test.go @@ -12,7 +12,7 @@ import ( "github.com/btcsuite/btcd/btcutil/v2/hdkeychain" "github.com/btcsuite/btcd/chaincfg/v2" "github.com/btcsuite/btcwallet/waddrmgr" - walletmock "github.com/btcsuite/btcwallet/wallet/internal/bwtest/mock" + vaultmock "github.com/btcsuite/btcwallet/wallet/internal/bwtest/mock/vault" "github.com/btcsuite/btcwallet/wallet/internal/db" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -22,8 +22,8 @@ import ( // crypt — Encrypt and Decrypt both return a fresh copy of the input bytes // with no error. This lets tests roundtrip derived account material // without bringing in real cryptoKey wiring. -func newIdentityVault() *walletmock.Vault { - vault := &walletmock.Vault{} +func newIdentityVault() *vaultmock.Vault { + vault := &vaultmock.Vault{} identity := func(_ waddrmgr.CryptoKeyType, b []byte) []byte { out := make([]byte, len(b)) copy(out, b) @@ -45,8 +45,8 @@ var errEncryptForTest = errors.New("encrypt boom") // newEncryptErrorVault returns a Vault whose Encrypt call always fails // with errEncryptForTest. Decrypt is left unconfigured because the tests // that consume this vault never reach a decrypt path. -func newEncryptErrorVault() *walletmock.Vault { - vault := &walletmock.Vault{} +func newEncryptErrorVault() *vaultmock.Vault { + vault := &vaultmock.Vault{} vault.On("Encrypt", mock.Anything, mock.Anything).Return( nil, errEncryptForTest, ) @@ -111,7 +111,7 @@ func TestNewAccountDeriveFn_MaxAccountNumber(t *testing.T) { masterKey := testMasterKey(t) scope := db.KeyScope{Purpose: 84, Coin: 0} - derive := newAccountDeriveFn(masterKey, &walletmock.Vault{}, 0) + derive := newAccountDeriveFn(masterKey, &vaultmock.Vault{}, 0) data, err := derive(t.Context(), scope, db.MaxAccountNumber+1, false) require.Nil(t, data) diff --git a/wallet/common_test.go b/wallet/common_test.go index f5d5a29d39..d061efb818 100644 --- a/wallet/common_test.go +++ b/wallet/common_test.go @@ -11,6 +11,7 @@ import ( bwmock "github.com/btcsuite/btcwallet/bwtest/mock" "github.com/btcsuite/btcwallet/waddrmgr" walletmock "github.com/btcsuite/btcwallet/wallet/internal/bwtest/mock" + vaultmock "github.com/btcsuite/btcwallet/wallet/internal/bwtest/mock/vault" "github.com/btcsuite/btcwallet/wallet/internal/db" "github.com/btcsuite/btcwallet/walletdb" _ "github.com/btcsuite/btcwallet/walletdb/bdb" @@ -97,7 +98,7 @@ func setupTestDB(t *testing.T) (walletdb.DB, func()) { // mockWalletDeps holds the mocked dependencies for the Wallet. type mockWalletDeps struct { addrStore *bwmock.AddrStore - vault *walletmock.Vault + vault *vaultmock.Vault store *walletmock.Store txStore *bwmock.TxStore syncer *mockChainSyncer @@ -118,7 +119,7 @@ func createTestWalletWithMocks(t *testing.T) (*Wallet, *mockWalletDeps) { t.Cleanup(cleanup) mockAddrStore := &bwmock.AddrStore{} - mockVault := &walletmock.Vault{} + mockVault := &vaultmock.Vault{} mockStore := &walletmock.Store{} mockTxStore := &bwmock.TxStore{} mockSyncer := &mockChainSyncer{} diff --git a/wallet/internal/bwtest/mock/vault.go b/wallet/internal/bwtest/mock/vault/vault.go similarity index 99% rename from wallet/internal/bwtest/mock/vault.go rename to wallet/internal/bwtest/mock/vault/vault.go index e05e63f4b6..a65c652f20 100644 --- a/wallet/internal/bwtest/mock/vault.go +++ b/wallet/internal/bwtest/mock/vault/vault.go @@ -2,7 +2,7 @@ // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. -package mock +package vaultmock import ( "context" diff --git a/wallet/signer_test.go b/wallet/signer_test.go index bc7ee98291..3856a6a000 100644 --- a/wallet/signer_test.go +++ b/wallet/signer_test.go @@ -24,7 +24,7 @@ import ( "github.com/btcsuite/btcd/wire/v2" bwmock "github.com/btcsuite/btcwallet/bwtest/mock" "github.com/btcsuite/btcwallet/waddrmgr" - walletmock "github.com/btcsuite/btcwallet/wallet/internal/bwtest/mock" + vaultmock "github.com/btcsuite/btcwallet/wallet/internal/bwtest/mock/vault" "github.com/btcsuite/btcwallet/wallet/internal/db" sqlitedb "github.com/btcsuite/btcwallet/wallet/internal/db/sqlite" "github.com/btcsuite/btcwallet/walletdb" @@ -1332,7 +1332,7 @@ func createDummyTestTx(pkScript []byte) (*wire.TxOut, *wire.MsgTx) { // newSQLAddressSigningWallet returns a started, unlocked wallet whose address // records are stored in SQLite while signing keys come from legacy waddrmgr. func newSQLAddressSigningWallet(t *testing.T) (*Wallet, *bwmock.Chain, - *walletmock.Vault) { + *vaultmock.Vault) { t.Helper() @@ -1384,7 +1384,7 @@ func newSQLAddressSigningWallet(t *testing.T) (*Wallet, *bwmock.Chain, require.NoError(t, err) chain := &bwmock.Chain{} - vault := &walletmock.Vault{} + vault := &vaultmock.Vault{} w = &Wallet{ addrStore: addrStore, From b54b11f1a014b163cf9479e2393af12d01f9c141 Mon Sep 17 00:00:00 2001 From: Gustavo Stingelin Date: Wed, 8 Jul 2026 18:31:58 -0300 Subject: [PATCH 2/4] wallet: add signing request types --- wallet/signing/doc.go | 3 + wallet/signing/key_locator.go | 48 ++++++++++++++++ wallet/signing/request.go | 103 ++++++++++++++++++++++++++++++++++ 3 files changed, 154 insertions(+) create mode 100644 wallet/signing/doc.go create mode 100644 wallet/signing/key_locator.go create mode 100644 wallet/signing/request.go diff --git a/wallet/signing/doc.go b/wallet/signing/doc.go new file mode 100644 index 0000000000..24a027e5bb --- /dev/null +++ b/wallet/signing/doc.go @@ -0,0 +1,3 @@ +// Package signing defines shared key locators, primitive digest signing +// requests, and signature results for wallet signing boundaries. +package signing diff --git a/wallet/signing/key_locator.go b/wallet/signing/key_locator.go new file mode 100644 index 0000000000..c6cb10b5e5 --- /dev/null +++ b/wallet/signing/key_locator.go @@ -0,0 +1,48 @@ +package signing + +import "github.com/btcsuite/btcwallet/waddrmgr" + +// KeyLocator identifies a key using one of the supported locator variants. +type KeyLocator interface { + // isKeyLocator seals KeyLocator to variants defined in this package. + isKeyLocator() +} + +// FullHDKeyLocator identifies a wallet-root derivation address using the +// existing waddrmgr address scope type. +type FullHDKeyLocator struct { + // AddrScope identifies the wallet-root derivation address. + AddrScope waddrmgr.AddrScope +} + +// isKeyLocator implements the sealed KeyLocator interface. +func (FullHDKeyLocator) isKeyLocator() {} + +// AccountKeyLocator identifies a child under a persisted account row. +type AccountKeyLocator struct { + // AccountID is the persisted account row identifier. + AccountID uint32 + + // Branch is the branch within the account. + Branch uint32 + + // Index is the key index within the branch. + Index uint32 +} + +// isKeyLocator implements the sealed KeyLocator interface. +func (AccountKeyLocator) isKeyLocator() {} + +// ScriptPubKeyLocator identifies a raw imported row by script pubkey. +type ScriptPubKeyLocator struct { + // ScriptPubKey is the raw locking script. + ScriptPubKey []byte +} + +// isKeyLocator implements the sealed KeyLocator interface. +func (ScriptPubKeyLocator) isKeyLocator() {} + +// Ensure the locator variants implement KeyLocator. +var _ KeyLocator = FullHDKeyLocator{} +var _ KeyLocator = AccountKeyLocator{} +var _ KeyLocator = ScriptPubKeyLocator{} diff --git a/wallet/signing/request.go b/wallet/signing/request.go new file mode 100644 index 0000000000..2b8753828e --- /dev/null +++ b/wallet/signing/request.go @@ -0,0 +1,103 @@ +package signing + +import ( + "github.com/btcsuite/btcd/btcec/v2" +) + +// Request identifies a signing operation using one of the supported request +// variants. +type Request interface { + // isRequest seals Request to variants defined in this package. + isRequest() +} + +// ECDSARequest requests a normal ECDSA signature over a digest. +type ECDSARequest struct { + // KeyLocator identifies the key used to sign Digest. + KeyLocator KeyLocator + + // Digest is the 32-byte hash to sign. + Digest [32]byte +} + +// isRequest implements the sealed Request interface. +func (ECDSARequest) isRequest() {} + +// CompactKeyFormat describes the public-key header semantics for compact ECDSA +// signatures. Implementations must reject CompactKeyFormatUnknown. +type CompactKeyFormat uint8 + +const ( + // CompactKeyFormatUnknown represents an omitted compact key format. + // Implementations must reject this value. + CompactKeyFormatUnknown CompactKeyFormat = iota + + // CompactKeyUncompressed requests compact signature header semantics for an + // uncompressed public key. + CompactKeyUncompressed + + // CompactKeyCompressed requests compact signature header semantics for a + // compressed public key. + CompactKeyCompressed +) + +// CompactECDSARequest requests a compact recoverable ECDSA signature over a +// digest. +type CompactECDSARequest struct { + // KeyLocator identifies the key used to sign Digest. + KeyLocator KeyLocator + + // Digest is the 32-byte hash to sign. + Digest [32]byte + + // KeyFormat selects the compact signature header semantics. + KeyFormat CompactKeyFormat +} + +// isRequest implements the sealed Request interface. +func (CompactECDSARequest) isRequest() {} + +// SchnorrRequest requests a BIP340 Schnorr signature over a digest. +type SchnorrRequest struct { + // KeyLocator identifies the key used to sign Digest. + KeyLocator KeyLocator + + // Digest is the 32-byte hash to sign. + Digest [32]byte +} + +// isRequest implements the sealed Request interface. +func (SchnorrRequest) isRequest() {} + +// SchnorrKeyTweak describes an additive secp256k1 tweak for Schnorr signing. +type SchnorrKeyTweak struct { + // Scalar is the already-computed additive secp256k1 scalar. It is not a + // Taproot script root, merkle root, or BIP341 tweak input byte string. + Scalar btcec.ModNScalar + + // NormalizeEvenY reports whether the key should be normalized to the BIP340 + // even-Y representative before the tweak is applied. + NormalizeEvenY bool +} + +// TweakedSchnorrRequest requests a BIP340 Schnorr signature over a digest after +// applying an additive key tweak. +type TweakedSchnorrRequest struct { + // KeyLocator identifies the key used to sign Digest. + KeyLocator KeyLocator + + // Digest is the 32-byte hash to sign. + Digest [32]byte + + // Tweak is the additive secp256k1 scalar applied before signing. + Tweak SchnorrKeyTweak +} + +// isRequest implements the sealed Request interface. +func (TweakedSchnorrRequest) isRequest() {} + +// Ensure the request variants implement Request. +var _ Request = ECDSARequest{} +var _ Request = CompactECDSARequest{} +var _ Request = SchnorrRequest{} +var _ Request = TweakedSchnorrRequest{} From 4cb84c7b0a139e976b5100d7c81925da17c4b30b Mon Sep 17 00:00:00 2001 From: Gustavo Stingelin Date: Wed, 8 Jul 2026 18:32:12 -0300 Subject: [PATCH 3/4] wallet: add signing signature types --- wallet/signing/signature.go | 63 ++++++++++++++++++++++++++ wallet/signing/signature_test.go | 76 ++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 wallet/signing/signature.go create mode 100644 wallet/signing/signature_test.go diff --git a/wallet/signing/signature.go b/wallet/signing/signature.go new file mode 100644 index 0000000000..feb5d4db2f --- /dev/null +++ b/wallet/signing/signature.go @@ -0,0 +1,63 @@ +package signing + +import ( + "github.com/btcsuite/btcd/btcec/v2/ecdsa" + "github.com/btcsuite/btcd/btcec/v2/schnorr" +) + +// Signature describes one supported primitive signing result. +type Signature interface { + // Serialize returns the raw signature bytes without any appended sighash + // byte; the wallet layer owns sighash semantics. + Serialize() []byte + + // isSignature seals Signature to variants defined in this package. + isSignature() +} + +// ECDSASignature wraps a normal ECDSA signature result. +type ECDSASignature struct { + // Signature is the generated ECDSA signature. + Signature ecdsa.Signature +} + +// isSignature implements the sealed Signature interface. +func (ECDSASignature) isSignature() {} + +// Serialize returns the DER-encoded ECDSA signature bytes. +func (s ECDSASignature) Serialize() []byte { + return s.Signature.Serialize() +} + +// CompactECDSASignature wraps a compact recoverable ECDSA signature result. +type CompactECDSASignature struct { + // Signature is the compact recoverable ECDSA signature bytes. + Signature []byte +} + +// isSignature implements the sealed Signature interface. +func (CompactECDSASignature) isSignature() {} + +// Serialize returns a copy of the compact recoverable ECDSA signature bytes. +func (s CompactECDSASignature) Serialize() []byte { + return append([]byte(nil), s.Signature...) +} + +// SchnorrSignature wraps a BIP340 Schnorr signature result. +type SchnorrSignature struct { + // Signature is the generated Schnorr signature. + Signature schnorr.Signature +} + +// isSignature implements the sealed Signature interface. +func (SchnorrSignature) isSignature() {} + +// Serialize returns the BIP340 Schnorr signature bytes. +func (s SchnorrSignature) Serialize() []byte { + return s.Signature.Serialize() +} + +// Ensure the signature variants implement Signature. +var _ Signature = ECDSASignature{} +var _ Signature = CompactECDSASignature{} +var _ Signature = SchnorrSignature{} diff --git a/wallet/signing/signature_test.go b/wallet/signing/signature_test.go new file mode 100644 index 0000000000..fa41d18cc1 --- /dev/null +++ b/wallet/signing/signature_test.go @@ -0,0 +1,76 @@ +package signing + +import ( + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/ecdsa" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/stretchr/testify/require" +) + +// TestECDSASignatureSerializeDER checks that the typed wrapper preserves the +// DER-encoded signature bytes. +func TestECDSASignatureSerializeDER(t *testing.T) { + t.Parallel() + + privKey, _ := btcec.PrivKeyFromBytes([]byte{ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, + }) + expectedSignature := ecdsa.Sign(privKey, []byte{ + 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, + 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, + 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, + 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40, + }) + sig := ECDSASignature{Signature: *expectedSignature} + + got := sig.Serialize() + + require.Equal(t, expectedSignature.Serialize(), got) +} + +// TestCompactECDSASignatureSerializeBytes checks that compact signatures are +// serialized as an isolated byte copy. +func TestCompactECDSASignatureSerializeBytes(t *testing.T) { + t.Parallel() + + expected := []byte{0x01, 0x02, 0x03, 0x04} + sig := CompactECDSASignature{Signature: expected} + + got := sig.Serialize() + + require.Equal(t, expected, got) + got[0] = 0xff + + require.Equal(t, expected, sig.Serialize()) +} + +// TestSchnorrSignatureSerializeBIP340 checks that the typed wrapper preserves +// the BIP340-encoded signature bytes. +func TestSchnorrSignatureSerializeBIP340(t *testing.T) { + t.Parallel() + + privKey, _ := btcec.PrivKeyFromBytes([]byte{ + 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, + 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, + 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, + 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x60, + }) + expectedSignature, err := schnorr.Sign(privKey, []byte{ + 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, + 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, + 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, + 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f, 0x80, + }) + require.NoError(t, err) + + sig := SchnorrSignature{Signature: *expectedSignature} + + got := sig.Serialize() + + require.Equal(t, expectedSignature.Serialize(), got) +} From 96241f0a22ab2c34bd0f767b67108317fe760417 Mon Sep 17 00:00:00 2001 From: Gustavo Stingelin Date: Wed, 8 Jul 2026 18:32:26 -0300 Subject: [PATCH 4/4] wallet: add keyvault digest signing interface --- .../adr/0010-keyvault-encryption-layer.md | 69 ++++++++++++++++++- wallet/internal/bwtest/mock/vault/vault.go | 42 +++++++++++ wallet/internal/keyvault/keyvault.go | 28 ++++++++ wallet/internal/keyvault/vault.go | 17 +++++ 4 files changed, 153 insertions(+), 3 deletions(-) diff --git a/docs/developer/adr/0010-keyvault-encryption-layer.md b/docs/developer/adr/0010-keyvault-encryption-layer.md index 8196375a97..33ef775adc 100644 --- a/docs/developer/adr/0010-keyvault-encryption-layer.md +++ b/docs/developer/adr/0010-keyvault-encryption-layer.md @@ -30,6 +30,44 @@ to each lock, unlock, encrypt, decrypt, or derivation operation. Keyvault accesses persistence through `db.Store`. It does not talk directly to SQL backends. +The vault API also includes a digest level signing method. `DerivePubKey` and +`Sign` keep private key use inside the same wallet scoped boundary that owns +lock state and key lifecycle. Callers identify keys with a sealed +`wallet/signing.KeyLocator` variant: `FullHDKeyLocator` for a full HD +derivation path, `AccountKeyLocator` for an account row child reference, or +`ScriptPubKeyLocator` for raw or imported rows without exposing private key +material or database routing. + +Wallet code builds a sealed `wallet/signing.Request` containing both the key +locator and a caller computed `[32]byte` digest, then calls `Sign` with that +request. Requests do not accept transactions, PSBTs, scripts, witnesses, or +sighash types. Transaction sighash construction, PSBT assembly, signature +serialization, appended sighash bytes, witness construction, and Taproot tree +construction remain wallet layer responsibilities. + +`Sign` accepts a sealed `wallet/signing.Request` variant instead of a +flag-heavy parameter struct. The sealed variants describe ECDSA, compact +recoverable ECDSA, Schnorr, and tweaked Schnorr primitive operations, making +invalid mode combinations unrepresentable outside the shared signing package. +The shared package also defines sealed signature result variants so keyvault +and later public wallet signing code can use the same primitive request and +result machinery. + +The Schnorr tweak request uses BIP340 terminology for even-Y key normalization +and carries an already computed additive secp256k1 scalar. This scalar is the +final key-tweak value, not a Taproot script root, merkle root, or BIP341 tweak +input byte string. These references describe the key and tweak concepts only. +They do not move transaction, script, or Taproot tree knowledge into the vault. + +`wallet/signing.CompactECDSARequest` remains digest-level, and the returned +compact recoverable ECDSA bytes remain the caller's responsibility for compact +signature use and wallet-layer transaction assembly. + +`wallet/signing.SchnorrKeyTweak` is a value descriptor with a +`Scalar btcec.ModNScalar` and `NormalizeEvenY bool`. Passing the additive scalar +as data lets the vault apply the private key operation internally without +accepting a private key callback from the wallet layer. + ```mermaid flowchart TD wallet[Wallet] @@ -85,9 +123,10 @@ wallet facing lock, unlock, encryption, decryption, and derivation APIs. zeroing. 2. **Expose typed domain interfaces** - Keyvault returns domain types such as `*btcec.PrivateKey`, - `*btcec.PublicKey`, and `btcutil.Address` instead of exposing encrypted byte - slices to wallet code. + Keyvault exposes typed domain operations and results such as public keys, + signatures, and encrypted bytes as appropriate. Private key material remains + inside the vault instead of being returned to wallet code as raw key bytes or + blobs. 3. **Handle HD derivation** Keyvault uses `btcutil/hdkeychain` for BIP32 and BIP44 derivation and returns @@ -109,6 +148,11 @@ wallet facing lock, unlock, encryption, decryption, and derivation APIs. New code uses keyvault while legacy code continues to rely on `waddrmgr` until the migration is complete. +8. **Sign caller computed digests** + Keyvault signs sealed ECDSA, compact recoverable ECDSA, Schnorr, and tweaked + Schnorr digest requests for located wallet keys while keeping transaction, + PSBT, script, witness, sighash, and Taproot tree logic in the wallet layer. + ## 3. Consequences The wallet facing `keyvault.Vault` API intentionally does not expose wallet ID @@ -128,6 +172,13 @@ code. Auto lock timeout scheduling is a wallet or controller lifecycle policy, not part of `keyvault.Vault`. +The signing surface follows the same boundary. Wallet code chooses the key, +constructs the 32-byte digest, builds the appropriate sealed +`wallet/signing.Request` carrying both values, calls `Sign`, then serializes and +attaches the returned sealed `wallet/signing.Signature` in the format required by +the transaction or PSBT flow. This keeps private key access inside the vault +without making the vault a Bitcoin transaction signer. + ### Pros 1. **Separation of concerns** @@ -163,6 +214,18 @@ part of `keyvault.Vault`. Keyvault can coexist with `waddrmgr`, allowing new SQL backed paths to move behind the new boundary before legacy paths are removed. +9. **Narrow signing boundary** + Digest level signing keeps private key operations in one locked component + without duplicating transaction, PSBT, script, witness, or Taproot tree + responsibilities inside keyvault. + +10. **Tweak data stays explicit** + `wallet/signing.SchnorrKeyTweak` carries the already computed additive + secp256k1 scalar and even-Y normalization flag as data, so callers can + describe Taproot key tweaks without passing script roots, merkle roots, + BIP341 tweak input bytes, or private key callbacks across the vault + boundary. + ### Cons 1. **Additional abstraction** diff --git a/wallet/internal/bwtest/mock/vault/vault.go b/wallet/internal/bwtest/mock/vault/vault.go index a65c652f20..180913a2ec 100644 --- a/wallet/internal/bwtest/mock/vault/vault.go +++ b/wallet/internal/bwtest/mock/vault/vault.go @@ -7,7 +7,10 @@ package vaultmock import ( "context" + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/wallet/internal/keyvault" + "github.com/btcsuite/btcwallet/wallet/signing" "github.com/stretchr/testify/mock" ) @@ -16,6 +19,9 @@ type Vault struct { mock.Mock } +// Ensure Vault implements the keyvault.Vault interface. +var _ keyvault.Vault = (*Vault)(nil) + // Encrypt forwards to the configured testify expectations. func (m *Vault) Encrypt(keyType waddrmgr.CryptoKeyType, plaintext []byte) ([]byte, error) { @@ -34,6 +40,42 @@ func (m *Vault) Decrypt(keyType waddrmgr.CryptoKeyType, return returnBytes(args, keyType, ciphertext), args.Error(1) } +// DerivePubKey forwards to the configured testify expectations. +func (m *Vault) DerivePubKey(ctx context.Context, + locator signing.KeyLocator) (*btcec.PublicKey, error) { + + args := m.Called(ctx, locator) + + if args.Get(0) == nil { + return nil, args.Error(1) + } + + pk, ok := args.Get(0).(*btcec.PublicKey) + if !ok { + return nil, args.Error(1) + } + + return pk, args.Error(1) +} + +// Sign forwards to the configured testify expectations. +func (m *Vault) Sign(ctx context.Context, + req signing.Request) (signing.Signature, error) { + + args := m.Called(ctx, req) + + if args.Get(0) == nil { + return nil, args.Error(1) + } + + sig, ok := args.Get(0).(signing.Signature) + if !ok { + return nil, args.Error(1) + } + + return sig, args.Error(1) +} + // Unlock forwards to the configured testify expectations. func (m *Vault) Unlock(ctx context.Context, passphrase []byte) error { args := m.Called(ctx, passphrase) diff --git a/wallet/internal/keyvault/keyvault.go b/wallet/internal/keyvault/keyvault.go index 6439ff7173..2eaf0690fb 100644 --- a/wallet/internal/keyvault/keyvault.go +++ b/wallet/internal/keyvault/keyvault.go @@ -5,7 +5,9 @@ import ( "context" "errors" + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/wallet/signing" ) // ErrInvalidPassphrase reports that the provided vault passphrase is wrong. @@ -19,6 +21,12 @@ var ErrVaultLocked = errors.New("vault is locked") // vault was already unlocked. var ErrVaultUnlocked = errors.New("vault is already unlocked") +// ErrVaultKeyOpsUnimplemented reports that the vault does not yet implement +// key derivation or signing operations. +var ErrVaultKeyOpsUnimplemented = errors.New( + "vault key operations are unimplemented", +) + // Vault manages the lock lifecycle and cryptographic operations for wallet key // material. type Vault interface { @@ -49,4 +57,24 @@ type Vault interface { // private passphrase. The vault must already be unlocked when this method // is called. ChangePassphrase(ctx context.Context, newPassphrase []byte) error + + // DerivePubKey derives the public key for the provided key locator. + // + // The caller provides a wallet key locator, and the vault returns the + // corresponding public key without exposing private key material. The vault + // must already be unlocked; implementations must return ErrVaultLocked if + // called while locked. + DerivePubKey(ctx context.Context, + keyLocator signing.KeyLocator) (*btcec.PublicKey, error) + + // Sign signs the caller-computed 32-byte digest for the located key using + // the primitive signing mode described by req. + // + // The caller is responsible for digest construction, sighash selection, + // signature serialization, transaction assembly, and any appended sighash + // byte. Signing requests do not carry transactions, PSBTs, scripts, + // witnesses, sighash types, or Taproot tree data. The vault must already be + // unlocked; implementations must return ErrVaultLocked if called while + // locked. + Sign(ctx context.Context, req signing.Request) (signing.Signature, error) } diff --git a/wallet/internal/keyvault/vault.go b/wallet/internal/keyvault/vault.go index f4182b6d29..05235d29c3 100644 --- a/wallet/internal/keyvault/vault.go +++ b/wallet/internal/keyvault/vault.go @@ -1,12 +1,15 @@ package keyvault import ( + "context" "errors" "sync" + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil/v2/hdkeychain" "github.com/btcsuite/btcwallet/snacl" "github.com/btcsuite/btcwallet/wallet/internal/db" + "github.com/btcsuite/btcwallet/wallet/signing" ) var ( @@ -63,6 +66,20 @@ func NewWalletVault(store db.Store, walletID uint32, } } +// DerivePubKey reports that WalletVault does not yet expose key operations. +func (v *WalletVault) DerivePubKey(context.Context, + signing.KeyLocator) (*btcec.PublicKey, error) { + + return nil, ErrVaultKeyOpsUnimplemented +} + +// Sign reports that WalletVault does not yet expose key operations. +func (v *WalletVault) Sign(context.Context, signing.Request) (signing.Signature, + error) { + + return nil, ErrVaultKeyOpsUnimplemented +} + // zero clears the runtime secret material held by the unlocked state. func (s *unlockedState) zero() { if s == nil {