Files
alknet/tasks/integration/phase3/secret-service/secp256k1-ethereum-derivation.md
glm-5.1 83ea66b5d1 chore: prep Phase 3 tasks and workspace for alknet-secret development
- Add irpc (0.16) and irpc-derive (0.16) as workspace dependencies
- Add irpc, irpc-derive, and secp256k1 (optional) to alknet-secret Cargo.toml
- Clarify encryption-salt-kdf task: Option B (document salt as reserved) is the
  chosen path per spec update, removing Option A acceptance criteria
- Update irpc-secret-protocol-integration task with concrete irpc crate details:
  real crate on crates.io v0.16, #[rpc_requests] macro, workspace config,
  AuthProtocol pattern reference, DerivedKey serialization considerations
- Fix secp256k1-ethereum-derivation task: correct crate name is secp256k1
  (not libsecp256k1), add version pin 0.29
2026-06-10 05:57:27 +00:00

95 lines
6.1 KiB
Markdown

---
id: secp256k1-ethereum-derivation
name: Add BIP-0032 secp256k1 derivation for Ethereum keys behind feature flag
status: pending
depends_on: [spec-update-secret-service, derivedkey-zeroize-security]
scope: narrow
risk: low
impact: component
level: 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/0` at 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 `0`s), which SLIP-0010 does not support (SLIP-0010 requires all indices to be hardened).
**Implementation:**
1. Add the `secp256k1` crate (Rust bindings to libsecp256k1) as an optional dependency behind a `secp256k1` feature flag:
```toml
[features]
secp256k1 = ["dep:secp256k1"]
[dependencies]
secp256k1 = { version = "0.29", optional = true }
```
**Note**: The Rust crate is named `secp256k1` on crates.io (it wraps the C library `libsecp256k1`). Do not use `libsecp256k1` — that is the C library name, not the Rust crate name.
2. Add a `ethereum.rs` module (behind `secp256k1` feature 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
3. Update `SecretServiceHandle::derive_ethereum_key()` (behind `secp256k1` feature 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`
4. Update `DerivationError` to include `Secp256k1(String)` and `UnsupportedKeyType` variants.
5. The `parse_derivation_path` function in `derivation.rs` already supports unhardened indices (it correctly parses `/0/0` at 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
- [ ] `secp256k1` crate (Rust bindings to libsecp256k1) added behind `secp256k1` feature flag in `Cargo.toml`
- [ ] `ethereum.rs` module added (behind `secp256k1` feature flag) with BIP-0032 secp256k1 derivation
- [ ] `derive_secp256k1_master_key()` uses HMAC-SHA512 with "Bitcoin seed" key
- [ ] `derive_secp256k1_path()` supports both hardened and unhardened indices
- [ ] `SecretServiceHandle::derive_ethereum_key()` uses BIP-0032 derivation when `secp256k1` feature is enabled
- [ ] `SecretServiceHandle::derive_ethereum_key()` returns `UnsupportedKeyType` error when `secp256k1` feature is disabled
- [ ] `SecretServiceHandle::derive_ed25519()` still uses SLIP-0010 (unchanged behavior)
- [ ] `SecretServiceHandle::derive_encryption_key()` still uses SLIP-0010 (unchanged behavior)
- [ ] `derive_ethereum_key()` returns `DerivedKey { key_type: Secp256k1, private_key: 32-byte-secp256k1-key, public_key: 33-byte-compressed-point }`
- [ ] `DerivationError` gains `Secp256k1(String)` and `UnsupportedKeyType` variants
- [ ] `lib.rs` conditionally exports `ethereum` module and `Secp256k1`-related types
- [ ] Unit test: Ethereum derivation at path `m/44'/60'/0'/0/0` produces a valid secp256k1 keypair (behind `secp256k1` feature)
- [ ] Unit test: Ethereum derivation produces different keys from Ed25519 derivation at the same seed
- [ ] Unit test: Calling `derive_ethereum_key()` without `secp256k1` feature returns `UnsupportedKeyType`
- [ ] 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-security` since `derive_ethereum_key` returns `DerivedKey` which will have the zeroize changes applied.
> The `secp256k1` crate on crates.io (version 0.29+) provides Rust bindings to the C library `libsecp256k1`. The `ed25519-bip32` crate 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_key` field in `DerivedKey` is `Vec<u8>`, so 33 bytes is fine. Document this size difference from Ed25519 (32-byte public keys).
> Consider whether `derive_path_from_seed` should be renamed to `derive_ed25519_path_from_seed` for 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