9 atomic tasks for alknet-secret spec conformance and gap closure, derived from architect's implementation review. Dependencies form a 5-generation graph starting with spec update, then parallel implementation tasks, ending with a review gate. Tasks address: DerivedKey zeroize security, key caching with TTL, irpc protocol integration, password derivation, secp256k1/Ethereum derivation, encryption salt/KDF, crypto test vectors, and final spec conformance review.
5.9 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | ||
|---|---|---|---|---|---|---|---|---|---|
| secp256k1-ethereum-derivation | Add BIP-0032 secp256k1 derivation for Ethereum keys behind feature flag | pending |
|
narrow | low | component | implementation |
Description
The SecretProtocol::DeriveEthereumKey variant exists in the protocol and SecretServiceHandle::derive_ethereum_key() exists in the service, but the current implementation uses the same SLIP-0010 Ed25519 derivation for all key types. This is incorrect.
BIP-0032 secp256k1 derivation (used for Ethereum at path m/44'/60'/0'/0/0) is fundamentally different from SLIP-0010 Ed25519:
- SLIP-0010 uses hardened-only derivation with HMAC-SHA512, producing Ed25519 keys
- BIP-0032 supports both hardened and unhardened derivation for secp256k1 (the Ethereum path has unhardened indices:
/0/0at the end) - The master key derivation algorithm is different (HMAC-SHA512 with "Bitcoin seed" vs "ed25519 seed")
- The key format is different (secp256k1 private key + compressed public key)
The Ethereum path m/44'/60'/0'/0/0 has unhardened indices at positions 4 and 5 (the last two 0s), which SLIP-0010 does not support (SLIP-0010 requires all indices to be hardened).
Implementation:
-
Add
libsecp256k1as an optional dependency behind asecp256k1feature flag:[features] secp256k1 = ["dep:libsecp256k1"] [dependencies] libsecp256k1 = { version = "0.7", optional = true } -
Add a
ethereum.rsmodule (behindsecp256k1feature flag) that implements BIP-0032 secp256k1 derivation:derive_secp256k1_master_key(seed: &[u8]) -> Result<Secp256k1ExtendedKey, DerivationError>derive_secp256k1_path(seed: &[u8], path: &str]) -> Result<ExtendedPrivKey, DerivationError>- This uses HMAC-SHA512 with key "Bitcoin seed" (different from SLIP-0010's "ed25519 seed")
- Supports both hardened (≥ 0x80000000) and unhardened indices
-
Update
SecretServiceHandle::derive_ethereum_key()(behindsecp256k1feature flag):- When feature is enabled, use BIP-0032 derivation for paths starting with
m/44' - Return
DerivedKey { key_type: KeyType::Secp256k1, private_key, public_key }where public_key is compressed (33 bytes) - When feature is NOT enabled, return
SecretServiceError::UnsupportedKeyType
- When feature is enabled, use BIP-0032 derivation for paths starting with
-
Update
DerivationErrorto includeSecp256k1(String)andUnsupportedKeyTypevariants. -
The
parse_derivation_pathfunction inderivation.rsalready supports unhardened indices (it correctly parses/0/0at the end). The dispatch to SLIP-0010 vs BIP-0032 should be based on the path's coin type or an explicit parameter, not automatic path detection. Use an explicit method:derive_ethereum_key()always uses BIP-0032.
Current bug: The existing derive_ethereum_key method calls derive_path_from_seed which uses SLIP-0010 (Ed25519 only). The path m/44'/60'/0'/0/0 has unhardened indices that SLIP-0010 doesn't handle correctly. This must be fixed.
Acceptance Criteria
libsecp256k1dependency added behindsecp256k1feature flag inCargo.tomlethereum.rsmodule added (behindsecp256k1feature flag) with BIP-0032 secp256k1 derivationderive_secp256k1_master_key()uses HMAC-SHA512 with "Bitcoin seed" keyderive_secp256k1_path()supports both hardened and unhardened indicesSecretServiceHandle::derive_ethereum_key()uses BIP-0032 derivation whensecp256k1feature is enabledSecretServiceHandle::derive_ethereum_key()returnsUnsupportedKeyTypeerror whensecp256k1feature is disabledSecretServiceHandle::derive_ed25519()still uses SLIP-0010 (unchanged behavior)SecretServiceHandle::derive_encryption_key()still uses SLIP-0010 (unchanged behavior)derive_ethereum_key()returnsDerivedKey { key_type: Secp256k1, private_key: 32-byte-secp256k1-key, public_key: 33-byte-compressed-point }DerivationErrorgainsSecp256k1(String)andUnsupportedKeyTypevariantslib.rsconditionally exportsethereummodule andSecp256k1-related types- Unit test: Ethereum derivation at path
m/44'/60'/0'/0/0produces a valid secp256k1 keypair (behindsecp256k1feature) - Unit test: Ethereum derivation produces different keys from Ed25519 derivation at the same seed
- Unit test: Calling
derive_ethereum_key()withoutsecp256k1feature returnsUnsupportedKeyType - Existing Ed25519 and encryption key derivation tests still pass
- BIP-0032 known test vector: known seed → known secp256k1 master key (if available)
References
- docs/architecture/secret-service.md — secp256k1 derivation note (after spec update)
- crates/alknet-secret/src/derivation.rs — Current SLIP-0010 only derivation
- crates/alknet-secret/src/service.rs — derive_ethereum_key (currently uses SLIP-0010, needs fix)
- BIP-0032: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
- Ethereum path EIP-84:
m/44'/60'/0'/0/0
Notes
This task should be done after
derivedkey-zeroize-securitysincederive_ethereum_keyreturnsDerivedKeywhich will have the zeroize changes applied.
The
secp256k1crate in Rust islibsecp256k1(crate namesecp256k1). Theed25519-bip32crate handles SLIP-0010 Ed25519. These are different algorithms and must not be mixed.
For the Ethereum public key: BIP-0032 secp256k1 produces 33-byte compressed public keys. The
public_keyfield inDerivedKeyisVec<u8>, so 33 bytes is fine. Document this size difference from Ed25519 (32-byte public keys).
Consider whether
derive_path_from_seedshould be renamed toderive_ed25519_path_from_seedfor clarity, since it's specifically SLIP-0010 Ed25519. The public API (derive_ed25519,derive_encryption_key,derive_ethereum_key) already makes this distinction, but the internal function name could be clearer.
Summary
To be filled on completion