Derive cbk keys with BIP85 instead of an ad hoc HKDF label

The private key is now the BIP85 HEX application output for 32 bytes at
m/83696968'/128169'/32'/{index}' from the BIP32 master key of the BIP39
seed, via sneak.berlin/go/secret/pkg/bip85, so any BIP85 tool reproduces
it and one mnemonic backs many keypairs through Params.Index. Tests
check the mnemonic-to-master step and the HEX application against the
BIP85 specification vectors and pin the resulting keypair.
This commit is contained in:
2026-10-31 11:38:02 -07:00
parent ea6faaecff
commit fe6dbba428
5 changed files with 312 additions and 64 deletions
+16 -10
View File
@@ -129,29 +129,35 @@ directory is never read.
### `pkg/cbk`
Derives NaCl crypto_box keypairs (Curve25519) deterministically from BIP39
mnemonics, and nothing else. The derivation is fixed and documented so keys are
reproducible:
mnemonics, and nothing else. The entropy comes from
[BIP85](https://github.com/bitcoin/bips/blob/master/bip-0085.mediawiki) through
`sneak.berlin/go/secret/pkg/bip85`, so any BIP85-capable tool given the same
mnemonic reproduces the same key, and one mnemonic can back any number of
independent keypairs:
1. The mnemonic is validated and canonicalized with
`bip39wordlist.NormalizeWordlist`.
2. The standard 64-byte BIP39 seed is computed with PBKDF2-HMAC-SHA512 (2048
rounds, salt `"mnemonic" + passphrase`), exactly as a BIP39 wallet would, so
the optional passphrase works as the usual "25th word".
3. The 32-byte private key is
`HKDF-SHA256(seed, salt = nil, info = cbk.DerivationInfo)`.
4. The public key is the Curve25519 base-point multiple of the private key.
3. The seed becomes a BIP32 master key.
4. BIP85's HEX application derives 32 bytes of entropy at
`m/83696968'/128169'/32'/{index}'`; those bytes are the private key.
5. The public key is the Curve25519 base-point multiple of the private key.
The resulting keys are plain 32-byte crypto_box keys for use with
`golang.org/x/crypto/nacl/box`. The API:
- `cbk.New(cbk.Params{Mnemonic, Passphrase})` and `cbk.FromMnemonic(m)` return a
`CryptoBoxKeypair`.
- `cbk.New(cbk.Params{Mnemonic, Passphrase, Index})` and `cbk.FromMnemonic(m)`
(no passphrase, index 0) return a `CryptoBoxKeypair`.
- `CryptoBoxKey.Hex()`, `CryptoBoxKey.Bytes()`, and the `PublicKeyHex`,
`PrivateKeyHex`, `PublicKeyBytes`, `PrivateKeyBytes` accessors on the keypair.
The test suite pins the keypair derived from the standard all-zero-entropy BIP39
test vector. If that test ever fails, the derivation has changed and existing
keys are no longer reproducible.
The test suite checks the mnemonic-to-master-key step and the BIP85 HEX
application against the BIP85 specification's own test vectors, verifies that
the private key is exactly the BIP85 output, and pins the keypair for the
specification's mnemonic at index 0. If the pinned test ever fails, the
derivation has changed and existing keys are no longer reproducible.
### `pkg/bip39wordlist`