Skip to content

Commit a194f51

Browse files
committed
Merge remote-tracking branch 'benma/ed25519-seed'
2 parents 570ca18 + 93af9da commit a194f51

File tree

8 files changed

+119
-250
lines changed

8 files changed

+119
-250
lines changed

src/keystore.c

Lines changed: 0 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@
2828

2929
#include <rust/rust.h>
3030
#include <secp256k1_ecdsa_s2c.h>
31-
#include <secp256k1_extrakeys.h>
32-
#include <secp256k1_schnorrsig.h>
3331

3432
// Change this ONLY via keystore_unlock() or keystore_lock()
3533
static bool _is_unlocked_device = false;
@@ -549,94 +547,6 @@ bool keystore_get_u2f_seed(uint8_t* seed_out)
549547
return true;
550548
}
551549

552-
bool keystore_get_ed25519_seed(uint8_t* seed_out)
553-
{
554-
uint8_t bip39_seed[64] = {0};
555-
UTIL_CLEANUP_64(bip39_seed);
556-
if (!keystore_copy_bip39_seed(bip39_seed)) {
557-
return false;
558-
}
559-
560-
const uint8_t key[] = "ed25519 seed";
561-
562-
// Derive a 64 byte expanded ed25519 private key and put it into seed_out.
563-
memcpy(seed_out, bip39_seed, 64);
564-
do {
565-
rust_hmac_sha512(key, sizeof(key), seed_out, 64, seed_out);
566-
} while (seed_out[31] & 0x20);
567-
568-
seed_out[0] &= 248;
569-
seed_out[31] &= 127;
570-
seed_out[31] |= 64;
571-
572-
// Compute chain code and put it into seed_out at offset 64.
573-
uint8_t message[65] = {0};
574-
message[0] = 0x01;
575-
memcpy(&message[1], bip39_seed, 64);
576-
util_zero(bip39_seed, sizeof(bip39_seed));
577-
rust_hmac_sha256(key, sizeof(key), message, sizeof(message), &seed_out[64]);
578-
util_zero(message, sizeof(message));
579-
return true;
580-
}
581-
582-
static bool _schnorr_keypair(
583-
const secp256k1_context* ctx,
584-
const uint32_t* keypath,
585-
size_t keypath_len,
586-
const uint8_t* tweak,
587-
secp256k1_keypair* keypair_out,
588-
secp256k1_xonly_pubkey* pubkey_out)
589-
{
590-
if (keystore_is_locked()) {
591-
return false;
592-
}
593-
uint8_t private_key[32] = {0};
594-
UTIL_CLEANUP_32(private_key);
595-
if (!rust_secp256k1_get_private_key(
596-
keypath, keypath_len, rust_util_bytes_mut(private_key, sizeof(private_key)))) {
597-
return false;
598-
}
599-
600-
if (!secp256k1_keypair_create(ctx, keypair_out, private_key)) {
601-
return false;
602-
}
603-
if (tweak != NULL) {
604-
if (secp256k1_keypair_xonly_tweak_add(ctx, keypair_out, tweak) != 1) {
605-
return false;
606-
}
607-
}
608-
if (!secp256k1_keypair_xonly_pub(ctx, pubkey_out, NULL, keypair_out)) {
609-
return false;
610-
}
611-
return true;
612-
}
613-
614-
static void _cleanup_keypair(secp256k1_keypair* keypair)
615-
{
616-
util_zero(keypair, sizeof(secp256k1_keypair));
617-
}
618-
619-
bool keystore_secp256k1_schnorr_sign(
620-
const secp256k1_context* ctx,
621-
const uint32_t* keypath,
622-
size_t keypath_len,
623-
const uint8_t* msg32,
624-
const uint8_t* tweak,
625-
uint8_t* sig64_out)
626-
{
627-
secp256k1_keypair __attribute__((__cleanup__(_cleanup_keypair))) keypair = {0};
628-
secp256k1_xonly_pubkey pubkey = {0};
629-
if (!_schnorr_keypair(ctx, keypath, keypath_len, tweak, &keypair, &pubkey)) {
630-
return false;
631-
}
632-
uint8_t aux_rand[32] = {0};
633-
random_32_bytes(aux_rand);
634-
if (secp256k1_schnorrsig_sign32(ctx, sig64_out, msg32, &keypair, aux_rand) != 1) {
635-
return false;
636-
}
637-
return secp256k1_schnorrsig_verify(ctx, sig64_out, msg32, 32, &pubkey) == 1;
638-
}
639-
640550
#ifdef TESTING
641551
void keystore_mock_unlocked(const uint8_t* seed, size_t seed_len, const uint8_t* bip39_seed)
642552
{

src/keystore.h

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -186,35 +186,6 @@ USE_RESULT bool keystore_secp256k1_sign(
186186
*/
187187
USE_RESULT bool keystore_get_u2f_seed(uint8_t* seed_out);
188188

189-
/**
190-
* Get the seed to be used for ed25519 applications such as Cardano. The output is the root key to
191-
* BIP32-ED25519.
192-
* This implements a derivation compatible with Ledger according to
193-
* https://github.com/LedgerHQ/orakolo/blob/0b2d5e669ec61df9a824df9fa1a363060116b490/src/python/orakolo/HDEd25519.py.
194-
* @param seed_out Buffer for the seed. Must be 96 bytes. It will contain a 64 byte expanded
195-
* ed25519 private key followed by a 32 byte chain code.
196-
*/
197-
USE_RESULT bool keystore_get_ed25519_seed(uint8_t* seed_out);
198-
199-
/**
200-
* Sign a message that verifies against the pubkey tweaked using BIP-86.
201-
*
202-
* @param[in] ctx secp256k1 context
203-
* @param[in] keypath derivation keypath
204-
* @param[in] keypath_len number of elements in keypath
205-
* @param[in] msg32 32 byte message to sign
206-
* @param[in] tweak 32 bytes, tweak private key before signing with this tweak. Use NULL to not
207-
* tweak.
208-
* @param[out] sig64_out resulting 64 byte signature
209-
*/
210-
USE_RESULT bool keystore_secp256k1_schnorr_sign(
211-
const secp256k1_context* ctx,
212-
const uint32_t* keypath,
213-
size_t keypath_len,
214-
const uint8_t* msg32,
215-
const uint8_t* tweak,
216-
uint8_t* sig64_out);
217-
218189
#ifdef TESTING
219190
/**
220191
* convenience to mock the keystore state (locked, seed) in tests.

src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1173,8 +1173,7 @@ async fn _process(
11731173
});
11741174

11751175
next_response.next.has_signature = true;
1176-
next_response.next.signature = bitbox02::keystore::secp256k1_schnorr_sign(
1177-
SECP256K1,
1176+
next_response.next.signature = crate::keystore::secp256k1_schnorr_sign(
11781177
&tx_input.keypath,
11791178
&sighash,
11801179
if let TaprootSpendInfo::KeySpend(tweak_hash) = &spend_info {

src/rust/bitbox02-rust/src/keystore.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,34 @@ pub fn bip85_ln(index: u32) -> Result<zeroize::Zeroizing<Vec<u8>>, ()> {
156156
Ok(entropy)
157157
}
158158

159+
/// Sign a message using the private key at the keypath, which is optionally tweaked with the given
160+
/// tweak.
161+
pub fn secp256k1_schnorr_sign(
162+
keypath: &[u32],
163+
msg: &[u8; 32],
164+
tweak: Option<&[u8; 32]>,
165+
) -> Result<[u8; 64], ()> {
166+
let private_key = secp256k1_get_private_key(keypath)?;
167+
let mut keypair =
168+
bitcoin::secp256k1::Keypair::from_seckey_slice(SECP256K1, &private_key).map_err(|_| ())?;
169+
170+
if let Some(tweak) = tweak {
171+
keypair = keypair
172+
.add_xonly_tweak(
173+
SECP256K1,
174+
&bitcoin::secp256k1::Scalar::from_be_bytes(*tweak).map_err(|_| ())?,
175+
)
176+
.map_err(|_| ())?;
177+
}
178+
179+
let sig = SECP256K1.sign_schnorr_with_aux_rand(
180+
&bitcoin::secp256k1::Message::from_digest(*msg),
181+
&keypair,
182+
&bitbox02::random::random_32_bytes(),
183+
);
184+
Ok(sig.serialize())
185+
}
186+
159187
#[cfg(test)]
160188
mod tests {
161189
use super::*;
@@ -164,6 +192,8 @@ mod tests {
164192
TEST_MNEMONIC, mock_memory, mock_unlocked, mock_unlocked_using_mnemonic,
165193
};
166194

195+
use bitcoin::secp256k1;
196+
167197
#[test]
168198
fn test_secp256k1_get_private_key() {
169199
keystore::lock();
@@ -433,4 +463,54 @@ mod tests {
433463
);
434464
}
435465
}
466+
467+
#[test]
468+
fn test_secp256k1_schnorr_sign() {
469+
mock_unlocked_using_mnemonic(
470+
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
471+
"",
472+
);
473+
let keypath = [86 + HARDENED, 0 + HARDENED, 0 + HARDENED, 0, 0];
474+
let msg = [0x88u8; 32];
475+
476+
let expected_pubkey = {
477+
let pubkey =
478+
hex::decode("cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115")
479+
.unwrap();
480+
secp256k1::XOnlyPublicKey::from_slice(&pubkey).unwrap()
481+
};
482+
483+
// Test without tweak
484+
bitbox02::random::fake_reset();
485+
let sig = secp256k1_schnorr_sign(&keypath, &msg, None).unwrap();
486+
assert!(
487+
SECP256K1
488+
.verify_schnorr(
489+
&secp256k1::schnorr::Signature::from_slice(&sig).unwrap(),
490+
&secp256k1::Message::from_digest_slice(&msg).unwrap(),
491+
&expected_pubkey
492+
)
493+
.is_ok()
494+
);
495+
496+
// Test with tweak
497+
bitbox02::random::fake_reset();
498+
let tweak = {
499+
let tweak =
500+
hex::decode("a39fb163dbd9b5e0840af3cc1ee41d5b31245c5dd8d6bdc3d026d09b8964997c")
501+
.unwrap();
502+
secp256k1::Scalar::from_be_bytes(tweak.try_into().unwrap()).unwrap()
503+
};
504+
let (tweaked_pubkey, _) = expected_pubkey.add_tweak(SECP256K1, &tweak).unwrap();
505+
let sig = secp256k1_schnorr_sign(&keypath, &msg, Some(&tweak.to_be_bytes())).unwrap();
506+
assert!(
507+
SECP256K1
508+
.verify_schnorr(
509+
&secp256k1::schnorr::Signature::from_slice(&sig).unwrap(),
510+
&secp256k1::Message::from_digest(msg),
511+
&tweaked_pubkey
512+
)
513+
.is_ok()
514+
);
515+
}
436516
}

src/rust/bitbox02-rust/src/keystore/ed25519.rs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,45 @@ use alloc::vec::Vec;
1717
use crate::hash::Sha512;
1818
use bip32_ed25519::{ED25519_EXPANDED_SECRET_KEY_SIZE, Xprv, Xpub};
1919

20+
use bitcoin::hashes::{Hash, HashEngine, Hmac, HmacEngine, sha256, sha512};
21+
22+
fn hmac_sha512(key: &[u8], msg: &[u8]) -> [u8; 64] {
23+
let mut engine = HmacEngine::<sha512::Hash>::new(key);
24+
engine.input(msg);
25+
Hmac::from_engine(engine).to_byte_array()
26+
}
27+
28+
/// Get the seed to be used for ed25519 applications such as Cardano. The output is the root key to
29+
/// BIP32-ED25519.
30+
/// This implements a derivation compatible with Ledger according to
31+
/// https://github.com/LedgerHQ/orakolo/blob/0b2d5e669ec61df9a824df9fa1a363060116b490/src/python/orakolo/HDEd25519.py.
32+
/// Returns 96 bytes. It will contain a 64 byte expanded ed25519 private key followed by a 32 byte chain code.
2033
fn get_seed() -> Result<zeroize::Zeroizing<Vec<u8>>, ()> {
21-
bitbox02::keystore::get_ed25519_seed()
34+
let bip39_seed = bitbox02::keystore::copy_bip39_seed()?;
35+
let mut seed_out = zeroize::Zeroizing::new(vec![0u8; 96]);
36+
let first64: &mut [u8] = &mut seed_out.as_mut_slice()[..64];
37+
first64.copy_from_slice(&bip39_seed);
38+
39+
let key = b"ed25519 seed";
40+
41+
loop {
42+
first64.copy_from_slice(&hmac_sha512(key, first64));
43+
if first64[31] & 0x20 == 0 {
44+
break;
45+
}
46+
}
47+
48+
seed_out[0] &= 248;
49+
seed_out[31] &= 127;
50+
seed_out[31] |= 64;
51+
52+
// Compute chain code and put it into seed_out at offset 64.
53+
let mut engine = HmacEngine::<sha256::Hash>::new(key);
54+
engine.input(&[0x01]);
55+
engine.input(&bip39_seed);
56+
let hmac_result: [u8; 32] = Hmac::from_engine(engine).to_byte_array();
57+
seed_out[64..].copy_from_slice(&hmac_result);
58+
Ok(seed_out)
2259
}
2360

2461
fn get_xprv(keypath: &[u32]) -> Result<Xprv<Sha512>, ()> {

src/rust/bitbox02-sys/build.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,11 @@ const ALLOWLIST_FNS: &[&str] = &[
7373
"keystore_create_and_store_seed",
7474
"keystore_encrypt_and_store_seed",
7575
"keystore_get_bip39_word",
76-
"keystore_get_ed25519_seed",
7776
"keystore_get_u2f_seed",
7877
"keystore_is_locked",
7978
"keystore_lock",
8079
"keystore_mock_unlocked",
8180
"keystore_secp256k1_nonce_commit",
82-
"keystore_secp256k1_schnorr_sign",
8381
"keystore_secp256k1_sign",
8482
"keystore_unlock",
8583
"keystore_unlock_bip39",

0 commit comments

Comments
 (0)