diff --git a/app-sdk/Cargo.toml b/app-sdk/Cargo.toml index 700e850..7d6c8d8 100644 --- a/app-sdk/Cargo.toml +++ b/app-sdk/Cargo.toml @@ -9,13 +9,14 @@ critical-section = "1.1.2" ctor = "0.2.8" embedded-alloc = "0.5.1" subtle = { version="2.6.1", default-features = false } +hex-literal = "0.4.1" +zeroize = "1.8.1" [target.'cfg(target_arch = "x86_64")'.dependencies] +bip32 = "0.5.2" hex = "0.4.3" +k256 = { version = "0.13.4", default-features = false, features = ["alloc", "ecdsa-core", "schnorr"] } num-bigint = "0.4.6" num-traits = "0.2.19" -sha2 = "0.10.8" ripemd = "0.1.3" - -[dev-dependencies] -hex-literal = "0.4.1" +sha2 = "0.10.8" diff --git a/app-sdk/src/curve.rs b/app-sdk/src/curve.rs new file mode 100644 index 0000000..50e7f7d --- /dev/null +++ b/app-sdk/src/curve.rs @@ -0,0 +1,574 @@ +use alloc::vec::Vec; +use core::{ + marker::PhantomData, + ops::{Add, Deref, Mul}, +}; + +use hex_literal::hex; +use zeroize::Zeroizing; + +use common::ecall_constants::{CurveKind, EcdsaSignMode, HashId, SchnorrSignMode}; + +use crate::ecalls::{Ecall, EcallsInterface}; + +/// A trait representing a cryptographic curve with hierarchical deterministic (HD) key derivation capabilities. +/// +/// # Constants +/// - `SCALAR_LENGTH`: The length of the scalar in bytes. +/// +/// # Required Methods +/// +/// ## `derive_hd_node` +/// Derives an HD node (a pair of private and public keys) from a given path. +/// +/// - `path`: A slice of `u32` values representing the derivation path. +/// - Returns: A `Result` containing a tuple with a 32-byte array (private key) and an array of `SCALAR_LENGTH` bytes (public key) on success, or a static string slice error message on failure. +/// +/// ## `get_master_fingerprint` +/// Retrieves the fingerprint of the master key. +/// +/// - Returns: A `u32` value representing the fingerprint of the master key. +pub trait Curve: Sized { + fn derive_hd_node(path: &[u32]) -> Result, &'static str>; + fn get_master_fingerprint() -> u32; +} + +/// A struct representing a Hierarchical Deterministic (HD) node composed of a private key, and a 32-byte chaincode. +/// +/// # Type Parameters +/// +/// * `SCALAR_LENGTH` - The length of the private key scalar. +/// +/// # Fields +/// +/// * `chaincode` - A 32-byte array representing the chain code. +/// * `privkey` - An array of bytes representing the private key, with a length defined by `SCALAR_LENGTH`. +pub struct HDPrivNode +where + C: Curve, +{ + curve_marker: PhantomData, + pub chaincode: [u8; 32], + pub privkey: Zeroizing<[u8; SCALAR_LENGTH]>, +} + +impl Default for HDPrivNode +where + C: Curve, +{ + fn default() -> Self { + Self { + curve_marker: PhantomData, + chaincode: [0u8; 32], + privkey: Zeroizing::new([0u8; SCALAR_LENGTH]), + } + } +} + +impl core::fmt::Debug for HDPrivNode +where + C: Curve, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!( + f, + "HDPrivNode {{ chaincode: {:?}, privkey: [REDACTED] }}", + self.chaincode + ) + } +} + +// A trait to simplify the implementation of `Curve` for different curves. +trait HasCurveKind { + // Returns the value that represents this curve in ECALLs. + fn get_curve_kind() -> CurveKind; +} + +impl Curve for C +where + C: HasCurveKind, +{ + fn derive_hd_node(path: &[u32]) -> Result, &'static str> { + let curve_kind = C::get_curve_kind(); + let mut result = HDPrivNode::default(); + + if 1 != Ecall::derive_hd_node( + curve_kind as u32, + path.as_ptr(), + path.len(), + result.privkey.as_mut_ptr(), + result.chaincode.as_mut_ptr(), + ) { + return Err("Failed to derive HD node"); + } + + Ok(result) + } + + fn get_master_fingerprint() -> u32 { + let curve_kind = C::get_curve_kind(); + Ecall::get_master_fingerprint(curve_kind as u32) + } +} + +/// A representation of an elliptic curve point in uncompressed form. +/// +/// The format is: +/// +/// `prefix | X-coordinate | Y-coordinate` +/// +/// Where `prefix` is always `0x04` for uncompressed points, and `X` and `Y` are `SCALAR_LENGTH` +/// byte arrays representing the coordinates. +/// +/// # Type Parameters +/// * `C` - The curve type implementing `Curve`. +/// * `SCALAR_LENGTH` - The byte length of the scalar and coordinate elements. +#[repr(C)] +pub struct Point +where + C: Curve, +{ + curve_marker: PhantomData, + prefix: u8, + x: [u8; SCALAR_LENGTH], + y: [u8; SCALAR_LENGTH], +} + +impl Default for Point +where + C: Curve, +{ + fn default() -> Self { + Self { + curve_marker: PhantomData, + prefix: 0x04, + x: [0u8; SCALAR_LENGTH], + y: [0u8; SCALAR_LENGTH], + } + } +} + +impl Point +where + C: Curve, +{ + /// Returns a mutable pointer to the beginning of the point's data. + pub fn as_mut_ptr(&mut self) -> *mut u8 { + &mut self.prefix as *mut u8 + } + /// Returns a pointer to the beginning of the point's data. + pub fn as_ptr(&self) -> *const u8 { + &self.prefix as *const u8 + } + + /// Creates a new Point with the given coordinates. + /// + /// # Arguments + /// + /// * `x` - The x-coordinate of the point. + /// * `y` - The y-coordinate of the point. + /// + /// # Returns + /// + /// A new `Point` instance. + pub fn new(x: [u8; SCALAR_LENGTH], y: [u8; SCALAR_LENGTH]) -> Self { + Self { + curve_marker: PhantomData, + prefix: 0x04, + x, + y, + } + } +} + +pub struct EcfpPublicKey +where + C: Curve, +{ + public_key: Point, +} + +impl EcfpPublicKey +where + C: Curve, +{ + /// Creates a new EcfpPublicKey from the given coordinates. + /// + /// # Arguments + /// + /// * `x` - The x-coordinate of the public key. + /// * `y` - The y-coordinate of the public key. + /// + /// # Returns + /// + /// A new `EcfpPublicKey` instance. + pub fn new(x: [u8; SCALAR_LENGTH], y: [u8; SCALAR_LENGTH]) -> Self { + Self { + public_key: Point::new(x, y), + } + } +} + +impl From> + for EcfpPublicKey +where + C: Curve, +{ + fn from(point: Point) -> Self { + Self { public_key: point } + } +} + +impl From> + for Point +where + C: Curve, +{ + fn from(public_key: EcfpPublicKey) -> Self { + public_key.public_key + } +} + +pub struct EcfpPrivateKey +where + C: Curve, +{ + curve_marker: PhantomData, + private_key: Zeroizing<[u8; SCALAR_LENGTH]>, +} + +impl EcfpPrivateKey +where + C: Curve, +{ + pub fn new(private_key: [u8; SCALAR_LENGTH]) -> Self { + Self { + curve_marker: PhantomData, + private_key: Zeroizing::new(private_key), + } + } +} + +pub trait ToPublicKey +where + C: Curve, +{ + fn to_public_key(&self) -> EcfpPublicKey; +} + +// We could implement this for any SCALAR_LENGTH, but this currently requires +// the #![feature(generic_const_exprs)], as the byte size is 1 + 2*SCALAR_LENGTH. +impl> Point { + /// Converts the point to a byte array. + /// + /// # Returns + /// + /// A byte array of length `1 + 2 * 32` representing the point. + pub fn to_bytes(&self) -> &[u8; 65] { + // SAFETY: `Point` is `#[repr(C)]` with a known layout: + // prefix (1 byte), x (32 bytes), y (32 bytes) = 65 bytes total. + // Therefore, we can safely reinterpret the memory as a [u8; 65]. + unsafe { &*(self as *const Self as *const [u8; 65]) } + } + + /// Creates a point from a byte array. + /// + /// # Arguments + /// + /// * `bytes` - A byte array of length `1 + 2 * 32` representing the point. + /// + /// # Returns + /// + /// A new instance of `Self`. + pub fn from_bytes(bytes: &[u8; 65]) -> &Self { + if bytes[0] != 0x04 { + panic!("Invalid point prefix. Expected 0x04"); + } + // SAFETY: The input slice has exactly 65 bytes and must match + // the memory layout of `Point`. The prefix is validated. + unsafe { &*(bytes as *const [u8; 65] as *const Self) } + } +} + +impl Add for &Point +where + C: Curve + HasCurveKind, +{ + type Output = Point; + + fn add(self, other: Self) -> Self::Output { + let mut result = Point::default(); + + if 1 != Ecall::ecfp_add_point( + C::get_curve_kind() as u32, + result.as_mut_ptr(), + self.as_ptr(), + other.as_ptr(), + ) { + panic!("Failed to add points"); + } + + result + } +} + +impl Mul<&[u8; SCALAR_LENGTH]> for &Point +where + C: Curve + HasCurveKind, +{ + type Output = Point; + + fn mul(self, scalar: &[u8; SCALAR_LENGTH]) -> Self::Output { + let mut result = Point::default(); + + if 1 != Ecall::ecfp_scalar_mult( + C::get_curve_kind() as u32, + result.as_mut_ptr(), + self.as_ptr(), + scalar.as_ptr(), + SCALAR_LENGTH, + ) { + panic!("Failed to multiply point by scalar"); + } + + result + } +} + +pub struct Secp256k1; + +impl HasCurveKind<32> for Secp256k1 { + fn get_curve_kind() -> CurveKind { + CurveKind::Secp256k1 + } +} + +pub type Secp256k1Point = Point; + +impl Secp256k1 { + pub fn get_generator() -> Secp256k1Point { + Point::new( + hex!("79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798"), + hex!("483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8"), + ) + } +} + +impl EcfpPrivateKey { + /// Signs a 32-byte message hash using the ECDSA algorithm, with deterministic signing + /// per RFC 6979. + /// + /// # Arguments + /// + /// * `msg_hash` - A reference to a 32-byte array containing the message hash to be signed. + /// + /// # Returns + /// + /// * `Ok(Vec)` - A vector containing the ECDSA signature if the signing is successful. + /// The signature is DER-encoded as per the bitcoin standard, and up to 71 bytes long. + /// * `Err(&'static str)` - An error message if the signing fails. + pub fn ecdsa_sign_hash(&self, msg_hash: &[u8; 32]) -> Result, &'static str> { + let mut result = [0u8; 71]; + let sig_size = Ecall::ecdsa_sign( + Secp256k1::get_curve_kind() as u32, + EcdsaSignMode::RFC6979 as u32, + HashId::Sha256 as u32, + self.private_key.as_ptr(), + msg_hash.as_ptr(), + result.as_mut_ptr(), + ); + if sig_size == 0 { + return Err("Failed to sign hash with ecdsa"); + } + Ok(result[0..sig_size].to_vec()) + } + + /// Signs a message using the Schnorr signature algorithm, as defined in BIP-0340. + /// + /// # Arguments + /// + /// * `msg` - A reference to a byte slice containing the message to be signed. + /// + /// # Returns + /// + /// * `Ok(Vec)` - A vector containing the Schnorr signature if the signing is successful. + /// The length of the signature is always 64 bytes. + /// + /// * `Err(&'static str)` - An error message if the signing fails. + pub fn schnorr_sign(&self, msg: &[u8]) -> Result, &'static str> { + let mut result = [0u8; 64]; + let sig_size = Ecall::schnorr_sign( + Secp256k1::get_curve_kind() as u32, + SchnorrSignMode::BIP340 as u32, + HashId::Sha256 as u32, + self.private_key.as_ptr(), + msg.as_ptr(), + msg.len(), + result.as_mut_ptr(), + ); + if sig_size != 64 { + panic!("Schnorr signatures per BIP-340 must be exactly 64 bytes"); + } + Ok(result.to_vec()) + } +} + +impl EcfpPublicKey { + pub fn ecdsa_verify_hash( + &self, + msg_hash: &[u8; 32], + signature: &[u8], + ) -> Result<(), &'static str> { + if 1 != Ecall::ecdsa_verify( + Secp256k1::get_curve_kind() as u32, + self.public_key.as_ptr(), + msg_hash.as_ptr(), + signature.as_ptr(), + signature.len(), + ) { + return Err("Failed to verify hash with ecdsa"); + } + Ok(()) + } + + pub fn schnorr_verify(&self, msg: &[u8], signature: &[u8]) -> Result<(), &'static str> { + if 1 != Ecall::schnorr_verify( + Secp256k1::get_curve_kind() as u32, + SchnorrSignMode::BIP340 as u32, + HashId::Sha256 as u32, + self.public_key.as_ptr(), + msg.as_ptr(), + msg.len(), + signature.as_ptr(), + signature.len(), + ) { + return Err("Failed to verify schnorr signature"); + } + Ok(()) + } +} + +// TODO: can we generalize this to all curves? +impl ToPublicKey for EcfpPrivateKey { + fn to_public_key(&self) -> EcfpPublicKey { + (&Secp256k1::get_generator() * self.private_key.deref()).into() + } +} + +#[cfg(test)] +mod tests { + use crate::hash::Hasher; + + use super::*; + + #[test] + fn test_secp256k1_get_master_fingerprint() { + assert_eq!(Secp256k1::get_master_fingerprint(), 0xf5acc2fdu32); + } + + #[test] + fn test_derive_hd_node_secp256k1() { + let node = Secp256k1::derive_hd_node(&[]).unwrap(); + assert_eq!( + node.chaincode, + hex!("eb473a0fa0af5031f14db9fe7c37bb8416a4ff01bb69dae9966dc83b5e5bf921") + ); + assert_eq!( + node.privkey[..], + hex!("34ac5d784ebb4df4727bcddf6a6743f5d5d46d83dd74aa825866390c694f2938") + ); + + let path = [0x8000002c, 0x80000000, 0x80000001, 0, 3]; + let node = Secp256k1::derive_hd_node(&path).unwrap(); + assert_eq!( + node.chaincode, + hex!("6da5f32f47232b3b9b2d6b59b802e2b313afa7cbda242f73da607139d8e04989") + ); + assert_eq!( + node.privkey[..], + hex!("239841e64103fd024b01283e752a213fee1a8969f6825204ee3617a45c5e4a91") + ); + } + + #[test] + fn test_secp256k1_point_addition() { + let point1 = Secp256k1Point::new( + hex!("c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5"), + hex!("1ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a"), + ); + let point2 = Secp256k1Point::new( + hex!("f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9"), + hex!("388f7b0f632de8140fe337e62a37f3566500a99934c2231b6cb9fd7584b8e672"), + ); + + let result = &point1 + &point2; + + assert_eq!( + result.x, + hex!("2f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4") + ); + assert_eq!( + result.y, + hex!("d8ac222636e5e3d6d4dba9dda6c9c426f788271bab0d6840dca87d3aa6ac62d6") + ); + } + + #[test] + fn test_secp256k1_point_scalarmul() { + let point1 = Secp256k1Point::new( + hex!("79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"), + hex!("483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8"), + ); + let scalar = hex!("22445566778899aabbccddeeff0011223344556677889900aabbccddeeff0011"); + + let result = &point1 * &scalar; + + assert_eq!( + result.x, + hex!("2748bce8ffc3f815e69e594ae974be5e9a3be69a233d5557ea9c92b71d69367b") + ); + assert_eq!( + result.y, + hex!("747206115143153c85f3e8bb94d392bd955d36f1f0204921e6dd7684e81bdaab") + ); + } + + #[test] + fn test_secp256k1_ecdsa_sign_verify() { + let privkey = EcfpPrivateKey:: { + curve_marker: PhantomData, + private_key: Zeroizing::new(hex!( + "4242424242424242424242424242424242424242424242424242424242424242" + )), + }; + let msg = "If you don't believe me or don't get it, I don't have time to try to convince you, sorry."; + let msg_hash = crate::hash::Sha256::hash(msg.as_bytes()); + + let signature = privkey.ecdsa_sign_hash(&msg_hash).unwrap(); + + println!("Signature: {:?}", signature); + + let pubkey = privkey.to_public_key(); + println!("Public key: {:?}", pubkey.public_key.to_bytes()); + pubkey.ecdsa_verify_hash(&msg_hash, &signature).unwrap(); + } + + #[test] + fn test_secp256k1_schnorr_sign_verify() { + let privkey = EcfpPrivateKey:: { + curve_marker: PhantomData, + private_key: Zeroizing::new(hex!( + "4242424242424242424242424242424242424242424242424242424242424242" + )), + }; + let msg = "If you don't believe me or don't get it, I don't have time to try to convince you, sorry."; + + let signature = privkey.schnorr_sign(msg.as_bytes()).unwrap(); + println!("Signature: {:?}", signature); + + let pubkey = privkey.to_public_key(); + pubkey.schnorr_verify(msg.as_bytes(), &signature).unwrap(); + + println!("pubkey: {:?}", pubkey.public_key.to_bytes()); + } +} diff --git a/app-sdk/src/ecalls.rs b/app-sdk/src/ecalls.rs index 3c26150..9d313d4 100644 --- a/app-sdk/src/ecalls.rs +++ b/app-sdk/src/ecalls.rs @@ -117,6 +117,166 @@ pub(crate) trait EcallsInterface { m: *const u8, len: usize, ) -> u32; + + /// Derives a hierarchical deterministic (HD) node, made of the private key and the corresponding chain code. + /// + /// # Parameters + /// - `curve`: The elliptic curve identifier. Currently only `Secp256k1` is supported. + /// - `path`: Pointer to the derivation path array. + /// - `path_len`: Length of the derivation path array. + /// - `privkey`: Pointer to the buffer to store the derived private key. + /// - `chain_code`: Pointer to the buffer to store the derived chain code. + /// + /// # Returns + /// 1 on success, 0 on error. + /// + /// # Panics + /// This function panics if the curve is not supported. + fn derive_hd_node( + curve: u32, + path: *const u32, + path_len: usize, + privkey: *mut u8, + chain_code: *mut u8, + ) -> u32; + + /// Retrieves the fingerprint for the master public key for the specified curve. + /// + /// # Parameters + /// - `curve`: The elliptic curve identifier. Currently only `Secp256k1` is supported. + /// + /// # Returns + /// The master fingerprint as a 32-bit unsigned integer, computed as the first 32 bits of `ripemd160(sha256(pk))`, + /// where `pk` is the public key in compressed form. + /// + /// # Panics + /// This function panics if the curve is not supported. + fn get_master_fingerprint(curve: u32) -> u32; + + /// Adds two elliptic curve points `p` and `q`, storing the result in `r`. + /// + /// # Parameters + /// - `curve`: The elliptic curve identifier. Currently only `Secp256k1` is supported. + /// - `r`: Pointer to the result buffer. + /// - `p`: Pointer to the first point buffer. + /// - `q`: Pointer to the second point buffer. + /// + /// # Returns + /// 1 on success, 0 on error. + fn ecfp_add_point(curve: u32, r: *mut u8, p: *const u8, q: *const u8) -> u32; + + /// Multiplies an elliptic curve point `p` by a scalar `k`, storing the result in `r`. + /// + /// # Parameters + /// - `curve`: The elliptic curve identifier. Currently only `Secp256k1` is supported. + /// - `r`: Pointer to the result buffer. + /// - `p`: Pointer to the point buffer. + /// - `k`: Pointer to the scalar buffer. + /// - `k_len`: Length of the scalar buffer. + /// + /// # Returns + /// 1 on success, 0 on error. + fn ecfp_scalar_mult(curve: u32, r: *mut u8, p: *const u8, k: *const u8, k_len: usize) -> u32; + + /// Signs a message hash using ECDSA. + /// + /// # Warning + /// **This ecall is unstable and subject to change in future versions.** + /// + /// # Parameters + /// - `curve`: The elliptic curve identifier. Currently only `Secp256k1` is supported. + /// - `mode`: The signing mode. Only `RFC6979` is supported. + /// - `hash_id`: The hash identifier. Only `Sha256` is supported. + /// - `privkey`: Pointer to the private key buffer. + /// - `msg_hash`: Pointer to the message hash buffer. + /// - `signature`: Pointer to the buffer to store the signature. + /// + /// # Returns + /// The length of the signature on success, 0 on error. + fn ecdsa_sign( + curve: u32, + mode: u32, + hash_id: u32, + privkey: *const u8, + msg_hash: *const u8, + signature: *mut u8, + ) -> usize; + + /// Verifies an ECDSA signature for a message hash. + /// + /// # Warning + /// **This ecall is unstable and subject to change in future versions.** + /// + /// # Parameters + /// - `curve`: The elliptic curve identifier. Currently only `Secp256k1` is supported. + /// - `pubkey`: Pointer to the public key buffer. + /// - `msg_hash`: Pointer to the message hash buffer. + /// - `signature`: Pointer to the signature buffer. + /// - `signature_len`: Length of the signature buffer. + /// + /// # Returns + /// 1 on success, 0 on error. + fn ecdsa_verify( + curve: u32, + pubkey: *const u8, + msg_hash: *const u8, + signature: *const u8, + signature_len: usize, + ) -> u32; + + /// Signs a message using Schnorr signature. + /// + /// # Warning + /// **This ecall is unstable and subject to change in future versions.** + /// + /// # Parameters + /// - `curve`: The elliptic curve identifier. Currently only `Secp256k1` is supported. + /// - `mode`: The signing mode. Only `BIP340` is supported. + /// - `hash_id`: The hash identifier. + /// - `privkey`: Pointer to the private key buffer. + /// - `msg`: Pointer to the message buffer. + /// - `msg_len`: Length of the message buffer. + /// - `signature`: Pointer to the buffer to store the signature. + /// + /// # Returns + /// The length of the signature (always 64) on success, 0 on error. + fn schnorr_sign( + curve: u32, + mode: u32, + hash_id: u32, + privkey: *const u8, + msg: *const u8, + msg_len: usize, + signature: *mut u8, + ) -> usize; + + /// Verifies a Schnorr signature for a message. + /// + /// # Warning + /// **This ecall is unstable and subject to change in future versions.** + /// + /// # Parameters + /// - `curve`: The elliptic curve identifier. Currently only `Secp256k1` is supported. + /// - `mode`: The verification mode. It must match the mode used for signing. + /// - `hash_id`: The hash identifier. Only `Sha256` is supported. + /// - `pubkey`: Pointer to the public key buffer. + /// - `msg`: Pointer to the message buffer. + /// - `msg_len`: Length of the message buffer. + /// - `signature`: Pointer to the signature buffer. + /// - `signature_len`: Length of the signature buffer. + /// + /// # Returns + /// 1 on success, 0 on error. + fn schnorr_verify( + curve: u32, + mode: u32, + hash_id: u32, + pubkey: *const u8, + msg: *const u8, + msg_len: usize, + signature: *const u8, + signature_len: usize, + ) -> u32; } pub(crate) use ecalls_module::*; diff --git a/app-sdk/src/ecalls_native.rs b/app-sdk/src/ecalls_native.rs index 2250e01..ebaeb8e 100644 --- a/app-sdk/src/ecalls_native.rs +++ b/app-sdk/src/ecalls_native.rs @@ -2,7 +2,23 @@ use std::io; use std::io::Write; use crate::ecalls::EcallsInterface; -use common::ecall_constants::MAX_BIGNUMBER_SIZE; +use common::ecall_constants::{CurveKind, MAX_BIGNUMBER_SIZE}; + +use bip32::{ChildNumber, XPrv}; +use hex_literal::hex; +use k256::{ + ecdsa::{ + self, + signature::{hazmat::PrehashVerifier, SignerMut}, + }, + elliptic_curve::{ + sec1::{FromEncodedPoint, ToEncodedPoint}, + PrimeField, + }, + schnorr::{self, signature::Verifier}, + EncodedPoint, ProjectivePoint, Scalar, +}; + use num_bigint::BigUint; use num_traits::Zero; @@ -241,4 +257,284 @@ impl EcallsInterface for Ecall { 1 } + + fn derive_hd_node( + curve: u32, + path: *const u32, + path_len: usize, + privkey: *mut u8, + chain_code: *mut u8, + ) -> u32 { + if curve != CurveKind::Secp256k1 as u32 { + panic!("Unsupported curve"); + } + let mut key = Ecall::get_master_bip32_key(); + + let path_slice = unsafe { std::slice::from_raw_parts(path, path_len) }; + for path_step in path_slice { + let child = ChildNumber::from(*path_step); + key = match key.derive_child(child) { + Ok(k) => k, + Err(_) => return 0, + }; + } + + // Copy the private key and chain code to the output buffers + let privkey_bytes = key.private_key().to_bytes(); + let chain_code_bytes = key.attrs().chain_code; + + unsafe { + std::ptr::copy_nonoverlapping(privkey_bytes.as_ptr(), privkey, privkey_bytes.len()); + std::ptr::copy_nonoverlapping( + chain_code_bytes.as_ptr(), + chain_code, + chain_code_bytes.len(), + ); + } + + 1 + } + + fn get_master_fingerprint(curve: u32) -> u32 { + if curve != CurveKind::Secp256k1 as u32 { + panic!("Unsupported curve"); + } + + u32::from_be_bytes(Ecall::get_master_bip32_key().public_key().fingerprint()) + } + + fn ecfp_add_point(curve: u32, r: *mut u8, p: *const u8, q: *const u8) -> u32 { + if curve != CurveKind::Secp256k1 as u32 { + panic!("Unsupported curve"); + } + + let p_slice = unsafe { std::slice::from_raw_parts(p, 65) }; + let q_slice = unsafe { std::slice::from_raw_parts(q, 65) }; + + let p_point = EncodedPoint::from_bytes(p_slice).expect("Invalid point P"); + let q_point = EncodedPoint::from_bytes(q_slice).expect("Invalid point Q"); + + let p_point = ProjectivePoint::from_encoded_point(&p_point).unwrap(); + let q_point = ProjectivePoint::from_encoded_point(&q_point).unwrap(); + + let result_point = p_point + q_point; + + let result_encoded = result_point.to_encoded_point(false); + let result_bytes = result_encoded.as_bytes(); + + unsafe { + std::ptr::copy_nonoverlapping(result_bytes.as_ptr(), r, result_bytes.len()); + } + + 1 + } + + fn ecfp_scalar_mult(curve: u32, r: *mut u8, p: *const u8, k: *const u8, k_len: usize) -> u32 { + if curve != CurveKind::Secp256k1 as u32 { + panic!("Unsupported curve"); + } + if k_len > 32 { + panic!("k_len is too large"); + } + + let p_slice = unsafe { std::slice::from_raw_parts(p, 65) }; + let k_slice = unsafe { std::slice::from_raw_parts(k, k_len) }; + + let p_point = EncodedPoint::from_bytes(p_slice).expect("Invalid point P"); + let p_point = ProjectivePoint::from_encoded_point(&p_point).unwrap(); + + // pad k_scalar to 32 bytes with initial zeros without using unsafe code + let mut k_scalar = [0u8; 32]; + k_scalar[32 - k_len..].copy_from_slice(k_slice); + let k_scalar = Scalar::from_repr(k_scalar.into()).unwrap(); + + let result_point = p_point * k_scalar; + let result_encoded = result_point.to_encoded_point(false); + + let result_bytes = result_encoded.as_bytes(); + + unsafe { + std::ptr::copy_nonoverlapping(result_bytes.as_ptr(), r, result_bytes.len()); + } + + 1 + } + + fn ecdsa_sign( + curve: u32, + mode: u32, + hash_id: u32, + privkey: *const u8, + msg_hash: *const u8, + signature: *mut u8, + ) -> usize { + if curve != CurveKind::Secp256k1 as u32 { + panic!("Unsupported curve"); + } + + if mode != common::ecall_constants::EcdsaSignMode::RFC6979 as u32 { + panic!("Invalid or unsupported ecdsa signing mode"); + } + + if hash_id != common::ecall_constants::HashId::Sha256 as u32 { + panic!("Invalid or unsupported hash id"); + } + + let privkey_slice = unsafe { std::slice::from_raw_parts(privkey, 32) }; + let msg_hash_slice = unsafe { std::slice::from_raw_parts(msg_hash, 32) }; + + let mut privkey_bytes = [0u8; 32]; + privkey_bytes[..].copy_from_slice(privkey_slice); + let signing_key = + ecdsa::SigningKey::from_bytes(&privkey_bytes.into()).expect("Invalid private key"); + let (signature_local, _) = signing_key + .sign_prehash_recoverable(msg_hash_slice) + .expect("Signing failed"); + + let signature_der = ecdsa::DerSignature::from(signature_local); + + let signature_bytes = signature_der.to_bytes(); + + unsafe { + std::ptr::copy_nonoverlapping( + signature_bytes.as_ptr(), + signature, + signature_bytes.len(), + ); + } + + signature_bytes.len() + } + + fn ecdsa_verify( + curve: u32, + pubkey: *const u8, + msg_hash: *const u8, + signature: *const u8, + signature_len: usize, + ) -> u32 { + if curve != CurveKind::Secp256k1 as u32 { + panic!("Unsupported curve"); + } + + if signature_len > 72 { + panic!("signature_len is too large"); + } + + let pubkey_slice = unsafe { std::slice::from_raw_parts(pubkey, 65) }; + let msg_hash_slice = unsafe { std::slice::from_raw_parts(msg_hash, 32) }; + let signature_slice = unsafe { std::slice::from_raw_parts(signature, signature_len) }; + + let pubkey_point = EncodedPoint::from_bytes(pubkey_slice).expect("Invalid public key"); + let verifying_key = ecdsa::VerifyingKey::from_encoded_point(&pubkey_point) + .expect("Failed to create verifying key"); + + let signature = + ecdsa::DerSignature::from_bytes(signature_slice.into()).expect("Invalid signature"); + + match verifying_key.verify_prehash(msg_hash_slice, &signature) { + Ok(_) => 1, + Err(_) => 0, + } + } + + fn schnorr_sign( + curve: u32, + mode: u32, + hash_id: u32, + privkey: *const u8, + msg: *const u8, + msg_len: usize, + signature: *mut u8, + ) -> usize { + if curve != CurveKind::Secp256k1 as u32 { + panic!("Unsupported curve"); + } + + if mode != common::ecall_constants::SchnorrSignMode::BIP340 as u32 { + panic!("Invalid or unsupported schnorr signing mode"); + } + + if msg_len > 128 { + panic!("msg_len is too large"); + } + + if hash_id != common::ecall_constants::HashId::Sha256 as u32 { + panic!("Invalid or unsupported hash id"); + } + + let privkey_slice = unsafe { std::slice::from_raw_parts(privkey, 32) }; + let msg_slice = unsafe { std::slice::from_raw_parts(msg, msg_len) }; + + let mut privkey_bytes = [0u8; 32]; + privkey_bytes[..].copy_from_slice(privkey_slice); + let mut signing_key = + schnorr::SigningKey::from_bytes(&privkey_bytes).expect("Invalid private key"); + + let signature_bytes = signing_key.sign(msg_slice).to_bytes(); + + unsafe { + std::ptr::copy_nonoverlapping( + signature_bytes.as_ptr(), + signature, + signature_bytes.len(), + ); + } + + signature_bytes.len() + } + + fn schnorr_verify( + curve: u32, + mode: u32, + hash_id: u32, + pubkey: *const u8, + msg: *const u8, + msg_len: usize, + signature: *const u8, + signature_len: usize, + ) -> u32 { + if curve != CurveKind::Secp256k1 as u32 { + panic!("Unsupported curve"); + } + + if mode != common::ecall_constants::SchnorrSignMode::BIP340 as u32 { + panic!("Invalid or unsupported schnorr signing mode"); + } + + if msg_len > 128 { + panic!("msg_len is too large"); + } + + if hash_id != common::ecall_constants::HashId::Sha256 as u32 { + panic!("Invalid or unsupported hash id"); + } + + if signature_len != 64 { + panic!("Invalid signature length"); + } + + let pubkey_slice = unsafe { std::slice::from_raw_parts(pubkey, 65) }; + let xonly_pubkey_slice = &pubkey_slice[1..33]; + let msg_slice = unsafe { std::slice::from_raw_parts(msg, msg_len) }; + let signature_slice = unsafe { std::slice::from_raw_parts(signature, signature_len) }; + + let verifying_key = + schnorr::VerifyingKey::from_bytes(xonly_pubkey_slice).expect("Invalid public key"); + let signature = schnorr::Signature::try_from(signature_slice).expect("Invalid signature"); + + match verifying_key.verify(msg_slice, &signature) { + Ok(_) => 1, + Err(_) => 0, + } + } +} + +impl Ecall { + // default seed used in Speculos, corrseponding to the mnemonic "glory promote mansion idle axis finger extra february uncover one trip resource lawn turtle enact monster seven myth punch hobby comfort wild raise skin" + const DEFAULT_SEED: [u8; 64] = hex!("b11997faff420a331bb4a4ffdc8bdc8ba7c01732a99a30d83dbbebd469666c84b47d09d3f5f472b3b9384ac634beba2a440ba36ec7661144132f35e206873564"); + + fn get_master_bip32_key() -> XPrv { + XPrv::new(&Self::DEFAULT_SEED).expect("Failed to create master key from seed") + } } diff --git a/app-sdk/src/ecalls_riscv.rs b/app-sdk/src/ecalls_riscv.rs index cd5c381..07d900a 100644 --- a/app-sdk/src/ecalls_riscv.rs +++ b/app-sdk/src/ecalls_riscv.rs @@ -307,6 +307,70 @@ macro_rules! ecall6 { }; } +macro_rules! ecall7 { + // ECALL with 7 arguments and returning a value + ($fn_name:ident, $syscall_number:expr, + ($arg1:ident: $arg1_type:ty), + ($arg2:ident: $arg2_type:ty), + ($arg3:ident: $arg3_type:ty), + ($arg4:ident: $arg4_type:ty), + ($arg5:ident: $arg5_type:ty), + ($arg6:ident: $arg6_type:ty), + ($arg7:ident: $arg7_type:ty), $ret_type:ty) => { + fn $fn_name($arg1: $arg1_type, $arg2: $arg2_type, $arg3: $arg3_type, $arg4: $arg4_type, $arg5: $arg5_type, $arg6: $arg6_type, $arg7: $arg7_type) -> $ret_type { + let ret: $ret_type; + unsafe { + asm!( + "ecall", + in("t0") $syscall_number, // Pass the syscall number in t0 + in("a0") $arg1, // First argument in a0 + in("a1") $arg2, // Second argument in a1 + in("a2") $arg3, // Third argument in a2 + in("a3") $arg4, // Fourth argument in a3 + in("a4") $arg5, // Fifth argument in a4 + in("a5") $arg6, // Sixth argument in a5 + in("a6") $arg7, // Seventh argument in a6 + lateout("a0") ret // Return value in a0 + ); + } + ret + } + }; +} + +macro_rules! ecall8 { + // ECALL with 8 arguments and returning a value + ($fn_name:ident, $syscall_number:expr, + ($arg1:ident: $arg1_type:ty), + ($arg2:ident: $arg2_type:ty), + ($arg3:ident: $arg3_type:ty), + ($arg4:ident: $arg4_type:ty), + ($arg5:ident: $arg5_type:ty), + ($arg6:ident: $arg6_type:ty), + ($arg7:ident: $arg7_type:ty), + ($arg8:ident: $arg8_type:ty), $ret_type:ty) => { + fn $fn_name($arg1: $arg1_type, $arg2: $arg2_type, $arg3: $arg3_type, $arg4: $arg4_type, $arg5: $arg5_type, $arg6: $arg6_type, $arg7: $arg7_type, $arg8: $arg8_type) -> $ret_type { + let ret: $ret_type; + unsafe { + asm!( + "ecall", + in("t0") $syscall_number, // Pass the syscall number in t0 + in("a0") $arg1, // First argument in a0 + in("a1") $arg2, // Second argument in a1 + in("a2") $arg3, // Third argument in a2 + in("a3") $arg4, // Fourth argument in a3 + in("a4") $arg5, // Fifth argument in a4 + in("a5") $arg6, // Sixth argument in a5 + in("a6") $arg7, // Seventh argument in a6 + in("a7") $arg8, // Eighth argument in a7 + lateout("a0") ret // Return value in a0 + ); + } + ret + } + }; +} + pub struct Ecall; impl EcallsInterface for Ecall { @@ -344,6 +408,17 @@ impl EcallsInterface for Ecall { ecall5!(bn_subm, ECALL_SUBM, (r: *mut u8), (a: *const u8), (b: *const u8), (m: *const u8), (len: usize), u32); ecall5!(bn_multm, ECALL_MULTM, (r: *mut u8), (a: *const u8), (b: *const u8), (m: *const u8), (len: usize), u32); ecall6!(bn_powm, ECALL_POWM, (r: *mut u8), (a: *const u8), (e: *const u8), (len_e: usize), (m: *const u8), (len: usize), u32); + + ecall5!(derive_hd_node, ECALL_DERIVE_HD_NODE, (curve: u32), (path: *const u32), (path_len: usize), (privkey: *mut u8), (chain_code: *mut u8), u32); + ecall1!(get_master_fingerprint, ECALL_GET_MASTER_FINGERPRINT, (curve: u32), u32); + + ecall4!(ecfp_add_point, ECALL_ECFP_ADD_POINT, (curve: u32), (r: *mut u8), (p: *const u8), (q: *const u8), u32); + ecall5!(ecfp_scalar_mult, ECALL_ECFP_SCALAR_MULT, (curve: u32), (r: *mut u8), (p: *const u8), (k: *const u8), (k_len: usize), u32); + + ecall6!(ecdsa_sign, ECALL_ECDSA_SIGN, (curve: u32), (mode: u32), (hash_id: u32), (privkey: *const u8), (msg_hash: *const u8), (signature: *mut u8), usize); + ecall5!(ecdsa_verify, ECALL_ECDSA_VERIFY, (curve: u32), (pubkey: *const u8), (msg_hash: *const u8), (signature: *const u8), (signature_len: usize), u32); + ecall7!(schnorr_sign, ECALL_SCHNORR_SIGN, (curve: u32), (mode: u32), (hash_id: u32), (privkey: *const u8), (msg: *const u8), (msg_len: usize), (signature: *mut u8), usize); + ecall8!(schnorr_verify, ECALL_SCHNORR_VERIFY, (curve: u32), (mode: u32), (hash_id: u32), (pubkey: *const u8), (msg: *const u8), (msg_len: usize), (signature: *const u8), (signature_len: usize), u32); } // The following ecalls are specific to this target diff --git a/app-sdk/src/hash.rs b/app-sdk/src/hash.rs index 62825d2..4fda69c 100644 --- a/app-sdk/src/hash.rs +++ b/app-sdk/src/hash.rs @@ -52,15 +52,7 @@ mod hashers { mod hashers { use super::*; use crate::Ecall; - - // TODO: IDs for now are matching the ones in the ledger SDK - #[derive(Clone, Copy, PartialEq, Eq, Debug)] - #[repr(C)] - enum HashId { - Ripemd160 = 1, - Sha256 = 3, - Sha512 = 5, - } + use common::ecall_constants::HashId; #[derive(Clone, PartialEq, Eq, Debug)] #[repr(C)] diff --git a/app-sdk/src/lib.rs b/app-sdk/src/lib.rs index 8dfc0fc..d7e23b3 100644 --- a/app-sdk/src/lib.rs +++ b/app-sdk/src/lib.rs @@ -7,6 +7,7 @@ use alloc::vec::Vec; pub mod bignum; pub mod comm; +pub mod curve; pub mod hash; pub mod ux; diff --git a/apps/sadik/app/src/main.rs b/apps/sadik/app/src/main.rs index 9e41b83..89c26fb 100644 --- a/apps/sadik/app/src/main.rs +++ b/apps/sadik/app/src/main.rs @@ -6,14 +6,14 @@ use sdk::fatal; use sdk::{ bignum::{BigNum, BigNumMod, Modulus}, + curve::{Curve as _, EcfpPrivateKey, EcfpPublicKey, Secp256k1Point}, hash::Hasher, }; extern crate alloc; -use alloc::string::ToString; -use alloc::vec::Vec; -use common::{Command, HashId}; +use alloc::{string::ToString, vec, vec::Vec}; +use common::{Command, Curve, ECPointOperation, HashId}; // Temporary to force the creation of a data section #[used] @@ -42,6 +42,20 @@ pub fn _start(_argc: isize, _argv: *const *const u8) -> isize { main(_argc, _argv) } +// parses a 65-byte uncompressed pubkey into an EcfpPublicKey +fn parse_pubkey(pubkey: &[u8]) -> EcfpPublicKey { + let pubkey_raw: [u8; 65] = pubkey + .try_into() + .expect("invalid pubkey: it must be 65 bytes in uncompressed form"); + if pubkey_raw[0] != 0x04 { + panic!("invalid pubkey: it must start with 0x04"); + } + EcfpPublicKey::new( + pubkey_raw[1..33].try_into().unwrap(), + pubkey_raw[33..65].try_into().unwrap(), + ) +} + #[start] pub fn main(_: isize, _: *const *const u8) -> isize { sdk::rust_init_heap(); @@ -178,6 +192,98 @@ pub fn main(_: isize, _: *const *const u8) -> isize { } } } + Command::GetMasterFingerprint { curve } => match curve { + Curve::Secp256k1 => sdk::curve::Secp256k1::get_master_fingerprint() + .to_be_bytes() + .to_vec(), + }, + Command::DeriveHdNode { curve, path } => match curve { + // returns the concatenation of the chaincode and private key + Curve::Secp256k1 => { + let node = sdk::curve::Secp256k1::derive_hd_node(&path).unwrap(); + let mut result = node.chaincode.to_vec(); + result.extend_from_slice(&node.privkey[..]); + result + } + }, + Command::ECPointOperation { curve, operation } => match curve { + Curve::Secp256k1 => match operation { + ECPointOperation::Add(p, q) => { + let p = Secp256k1Point::from_bytes(p.as_slice().try_into().unwrap()); + let q = Secp256k1Point::from_bytes(q.as_slice().try_into().unwrap()); + (p + q).to_bytes().to_vec() + } + ECPointOperation::ScalarMult(p, k) => { + let p = Secp256k1Point::from_bytes(p.as_slice().try_into().unwrap()); + let k: [u8; 32] = k.as_slice().try_into().unwrap(); + (p * &k).to_bytes().to_vec() + } + }, + }, + Command::EcdsaSign { + curve, + privkey, + msg_hash, + } => match curve { + Curve::Secp256k1 => { + let msg_hash: [u8; 32] = msg_hash + .as_slice() + .try_into() + .expect("hash must be 32 bytes"); + let privkey: EcfpPrivateKey = EcfpPrivateKey::new( + privkey.as_slice().try_into().expect("invalid privkey"), + ); + + privkey.ecdsa_sign_hash(&msg_hash).unwrap() + } + }, + Command::EcdsaVerify { + curve, + msg_hash, + pubkey, + signature, + } => match curve { + Curve::Secp256k1 => { + let pubkey = parse_pubkey(&pubkey); + let msg_hash: [u8; 32] = msg_hash + .as_slice() + .try_into() + .expect("hash must be 32 bytes"); + + if pubkey.ecdsa_verify_hash(&msg_hash, &signature).is_ok() { + vec![1] + } else { + vec![0] + } + } + }, + Command::SchnorrSign { + curve, + privkey, + msg, + } => match curve { + Curve::Secp256k1 => { + let privkey: EcfpPrivateKey = EcfpPrivateKey::new( + privkey.as_slice().try_into().expect("invalid privkey"), + ); + privkey.schnorr_sign(&msg).unwrap() + } + }, + Command::SchnorrVerify { + curve, + pubkey, + msg, + signature, + } => match curve { + Curve::Secp256k1 => { + let pubkey: EcfpPublicKey = parse_pubkey(&pubkey); + if pubkey.schnorr_verify(&msg, &signature).is_ok() { + vec![1] + } else { + vec![0] + } + } + }, }; sdk::comm::send_message(&response); diff --git a/apps/sadik/client/Cargo.toml b/apps/sadik/client/Cargo.toml index d59128e..6409407 100644 --- a/apps/sadik/client/Cargo.toml +++ b/apps/sadik/client/Cargo.toml @@ -18,7 +18,9 @@ serde = "1.0.215" tokio = { version = "1.38.1", features = ["io-util", "macros", "net", "rt", "rt-multi-thread", "sync"] } [dev-dependencies] +k256 = { version = "0.13.4", features = ["schnorr"] } hex-literal = "0.4.1" +sha2 = "0.10.8" [lib] name = "vnd_sadik_client" diff --git a/apps/sadik/client/src/client.rs b/apps/sadik/client/src/client.rs index b66b0e7..9294291 100644 --- a/apps/sadik/client/src/client.rs +++ b/apps/sadik/client/src/client.rs @@ -1,4 +1,4 @@ -use common::{BigIntOperator, Command, HashId}; +use common::{BigIntOperator, Command, Curve, HashId}; use sdk::{ comm::{send_message, SendMessageError}, vanadium_client::{VAppClient, VAppExecutionError}, @@ -85,6 +85,141 @@ impl SadikClient { .expect("Error sending message")) } + pub async fn derive_hd_node( + &mut self, + curve: Curve, + path: Vec, + ) -> Result, SadikClientError> { + let cmd = Command::DeriveHdNode { curve, path }; + + let msg = postcard::to_allocvec(&cmd).expect("Serialization failed"); + Ok(send_message(&mut self.app_client, &msg) + .await + .expect("Error sending message")) + } + + pub async fn get_master_fingerprint( + &mut self, + curve: Curve, + ) -> Result, SadikClientError> { + let cmd = Command::GetMasterFingerprint { curve }; + + let msg = postcard::to_allocvec(&cmd).expect("Serialization failed"); + Ok(send_message(&mut self.app_client, &msg) + .await + .expect("Error sending message")) + } + + pub async fn ecpoint_add( + &mut self, + curve: Curve, + p: &[u8], + q: &[u8], + ) -> Result, SadikClientError> { + let cmd = Command::ECPointOperation { + curve, + operation: common::ECPointOperation::Add(p.to_vec(), q.to_vec()), + }; + + let msg = postcard::to_allocvec(&cmd).expect("Serialization failed"); + Ok(send_message(&mut self.app_client, &msg) + .await + .expect("Error sending message")) + } + + pub async fn ecpoint_scalarmult( + &mut self, + curve: Curve, + p: &[u8], + k: &[u8], + ) -> Result, SadikClientError> { + let cmd = Command::ECPointOperation { + curve, + operation: common::ECPointOperation::ScalarMult(p.to_vec(), k.to_vec()), + }; + + let msg = postcard::to_allocvec(&cmd).expect("Serialization failed"); + Ok(send_message(&mut self.app_client, &msg) + .await + .expect("Error sending message")) + } + + pub async fn ecdsa_sign( + &mut self, + curve: Curve, + privkey: &[u8], + msg_hash: &[u8], + ) -> Result, SadikClientError> { + let cmd = Command::EcdsaSign { + curve, + privkey: privkey.to_vec(), + msg_hash: msg_hash.to_vec(), + }; + + let msg = postcard::to_allocvec(&cmd).expect("Serialization failed"); + Ok(send_message(&mut self.app_client, &msg) + .await + .expect("Error sending message")) + } + + pub async fn ecdsa_verify( + &mut self, + curve: Curve, + pubkey: &[u8], + msg_hash: &[u8], + signature: &[u8], + ) -> Result, SadikClientError> { + let cmd = Command::EcdsaVerify { + curve, + pubkey: pubkey.to_vec(), + msg_hash: msg_hash.to_vec(), + signature: signature.to_vec(), + }; + + let msg = postcard::to_allocvec(&cmd).expect("Serialization failed"); + Ok(send_message(&mut self.app_client, &msg) + .await + .expect("Error sending message")) + } + + pub async fn schnorr_sign( + &mut self, + curve: Curve, + privkey: &[u8], + msg: &[u8], + ) -> Result, SadikClientError> { + let cmd = Command::SchnorrSign { + curve, + privkey: privkey.to_vec(), + msg: msg.to_vec(), + }; + + let msg = postcard::to_allocvec(&cmd).expect("Serialization failed"); + Ok(send_message(&mut self.app_client, &msg) + .await + .expect("Error sending message")) + } + + pub async fn schnorr_verify( + &mut self, + curve: Curve, + pubkey: &[u8], + msg: &[u8], + signature: &[u8], + ) -> Result, SadikClientError> { + let cmd = Command::SchnorrVerify { + curve, + pubkey: pubkey.to_vec(), + msg: msg.to_vec(), + signature: signature.to_vec(), + }; + + let msg = postcard::to_allocvec(&cmd).expect("Serialization failed"); + Ok(send_message(&mut self.app_client, &msg) + .await + .expect("Error sending message")) + } + pub async fn exit(&mut self) -> Result { match send_message(&mut self.app_client, &[]).await { Ok(_) => Err("Exit message shouldn't return!"), diff --git a/apps/sadik/client/tests/integration_test.rs b/apps/sadik/client/tests/integration_test.rs index c700b5d..bae8b52 100644 --- a/apps/sadik/client/tests/integration_test.rs +++ b/apps/sadik/client/tests/integration_test.rs @@ -4,6 +4,7 @@ mod test_common; use common::{BigIntOperator, HashId}; use hex_literal::hex; +use sha2::Digest; #[tokio::test] #[rustfmt::skip] @@ -193,3 +194,223 @@ async fn test_sha512() { ); } } + +#[tokio::test] +async fn test_secp256k1_get_master_fingerprint() { + let mut setup = test_common::setup().await; + + assert_eq!( + setup + .client + .get_master_fingerprint(common::Curve::Secp256k1) + .await + .unwrap(), + hex!("f5acc2fd").to_vec() + ); +} + +#[tokio::test] +async fn test_secp256k1_derive_hd_node() { + let mut setup = test_common::setup().await; + + let test_cases: Vec<(Vec, ([u8; 32], [u8; 32]))> = vec![ + ( + vec![], + ( + hex!("eb473a0fa0af5031f14db9fe7c37bb8416a4ff01bb69dae9966dc83b5e5bf921"), + hex!("34ac5d784ebb4df4727bcddf6a6743f5d5d46d83dd74aa825866390c694f2938"), + ), + ), + ( + vec![0x8000002c, 0x80000000, 0x80000001, 0, 3], + ( + hex!("6da5f32f47232b3b9b2d6b59b802e2b313afa7cbda242f73da607139d8e04989"), + hex!("239841e64103fd024b01283e752a213fee1a8969f6825204ee3617a45c5e4a91"), + ), + ), + ]; + + for (path, (exp_chaincode, exp_privkey)) in test_cases { + let res = setup + .client + .derive_hd_node(common::Curve::Secp256k1, path) + .await + .unwrap(); + + assert_eq!(res.len(), exp_chaincode.len() + exp_privkey.len()); + let chaincode = &res[0..exp_chaincode.len()]; + let privkey = &res[exp_chaincode.len()..]; + + assert_eq!(exp_chaincode, chaincode); + assert_eq!(exp_privkey, privkey); + } +} + +#[tokio::test] +async fn test_secp256k1_point_add() { + let mut setup = test_common::setup().await; + + let p = hex!("04c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee51ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a"); + let q = hex!("04f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9388f7b0f632de8140fe337e62a37f3566500a99934c2231b6cb9fd7584b8e672"); + + let res = setup + .client + .ecpoint_add(common::Curve::Secp256k1, &p, &q) + .await + .unwrap(); + + assert_eq!( + res, + hex!("042f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4d8ac222636e5e3d6d4dba9dda6c9c426f788271bab0d6840dca87d3aa6ac62d6") + ); +} + +#[tokio::test] +async fn test_secp256k1_point_scalarmul() { + let mut setup = test_common::setup().await; + + let p = hex!("0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8"); + let k = hex!("22445566778899aabbccddeeff0011223344556677889900aabbccddeeff0011"); + + let res = setup + .client + .ecpoint_scalarmult(common::Curve::Secp256k1, &p, &k) + .await + .unwrap(); + + assert_eq!( + res, + hex!("042748bce8ffc3f815e69e594ae974be5e9a3be69a233d5557ea9c92b71d69367b747206115143153c85f3e8bb94d392bd955d36f1f0204921e6dd7684e81bdaab") + ); +} + +#[tokio::test] +async fn test_secp256k1_ecdsa_sign() { + let mut setup = test_common::setup().await; + let msg = + "If you don't believe me or don't get it, I don't have time to try to convince you, sorry."; + + // compute the sha256 hash using the sha2 crate + let msg_hash = sha2::Sha256::digest(msg.as_bytes()).to_vec(); + + let privkey = hex!("4242424242424242424242424242424242424242424242424242424242424242"); + + let result = setup + .client + .ecdsa_sign(common::Curve::Secp256k1, &privkey, &msg_hash) + .await + .unwrap(); + + let expected_signature = hex!("304402201bbd5947e4a9cdf85d6efb0aeecdfa8c179480a1b972a3dd8b277a78a409dcdf022064c812320ad4f0ae5a3fa1ef5d66ef70c78922bd9d9e30b224d1b38671a3291b"); + + // signature is deterministic per RFC6979 + assert_eq!(result, expected_signature); +} + +#[tokio::test] +async fn test_secp256k1_ecdsa_verify() { + let mut setup = test_common::setup().await; + let msg = + "If you don't believe me or don't get it, I don't have time to try to convince you, sorry."; + + // compute the sha256 hash using the sha2 crate + let msg_hash = sha2::Sha256::digest(msg.as_bytes()).to_vec(); + + let pubkey = hex!("0424653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c119fc5009a032aa9fe47f5e149bb8442f71f884ccb516590686d8ff6ab91c613"); + let signature = hex!("304402201bbd5947e4a9cdf85d6efb0aeecdfa8c179480a1b972a3dd8b277a78a409dcdf022064c812320ad4f0ae5a3fa1ef5d66ef70c78922bd9d9e30b224d1b38671a3291b"); + + let result = setup + .client + .ecdsa_verify(common::Curve::Secp256k1, &pubkey, &msg_hash, &signature) + .await + .unwrap(); + + assert_eq!(result, vec![1]); + + let sig_wrong = { + let mut sig = signature.clone(); + sig[16] ^= 0x01; + sig + }; + + let result = setup + .client + .ecdsa_verify(common::Curve::Secp256k1, &pubkey, &msg_hash, &sig_wrong) + .await + .unwrap(); + + assert_eq!(result, vec![0]); +} + +#[tokio::test] +async fn test_secp256k1_schnorr_sign() { + let mut setup = test_common::setup().await; + let msg = + "If you don't believe me or don't get it, I don't have time to try to convince you, sorry."; + + let privkey = hex!("4242424242424242424242424242424242424242424242424242424242424242"); + + let result = setup + .client + .schnorr_sign(common::Curve::Secp256k1, &privkey, &msg.as_bytes()) + .await + .unwrap(); + + // Schnorr signature using BIP340 is not deterministic. Check that the returned signature is valid instead + assert_eq!(result.len(), 64); + + println!("Signature: {:?}", result); + + let signature = k256::schnorr::Signature::try_from(result.as_slice()).unwrap(); + + // verify that the signature is valid using the k256 crate + let pubkey = k256::schnorr::VerifyingKey::from_bytes(&hex!( + "24653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c" + )) + .unwrap(); + + // verify_raw uses unhashed messages; normally one wouldn't reaaly use this + pubkey.verify_raw(msg.as_bytes(), &signature).unwrap(); +} + +#[tokio::test] +async fn test_secp256k1_schnorr_verify() { + let mut setup = test_common::setup().await; + let msg = + "If you don't believe me or don't get it, I don't have time to try to convince you, sorry."; + + let pubkey = hex!("0424653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1cee603aff65fcd55601b80a1eb6447bbd08e077b334ae9a6f97927008546e361c"); + let signature = hex!("54a2a499ce77edc2599c3fdb99b66d461230165776abf6efebe3a86cb6b3a88e8bf4a388ff3fe1e424a907974826a991b2bb497691d055da66b1b5ba12bb67cc"); + + let result = setup + .client + .schnorr_verify( + common::Curve::Secp256k1, + &pubkey, + &msg.as_bytes(), + &signature, + ) + .await + .unwrap(); + + assert_eq!(result, vec![1]); + + let sig_wrong = { + let mut sig = signature.clone(); + sig[16] ^= 0x01; + sig + }; + + let result = setup + .client + .schnorr_verify( + common::Curve::Secp256k1, + &pubkey, + &msg.as_bytes(), + &sig_wrong, + ) + .await + .unwrap(); + + assert_eq!(result, vec![0]); +} diff --git a/apps/sadik/common/src/lib.rs b/apps/sadik/common/src/lib.rs index bb19fd9..737f4d7 100644 --- a/apps/sadik/common/src/lib.rs +++ b/apps/sadik/common/src/lib.rs @@ -14,6 +14,17 @@ pub enum BigIntOperator { Pow, } +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub enum ECPointOperation { + Add(Vec, Vec), + ScalarMult(Vec, Vec), +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub enum Curve { + Secp256k1, +} + #[derive(Serialize, Deserialize, Debug, PartialEq)] pub enum Command { BigIntOperation { @@ -26,6 +37,39 @@ pub enum Command { hash_id: u32, msg: Vec, }, + GetMasterFingerprint { + curve: Curve, + }, + DeriveHdNode { + curve: Curve, + path: Vec, + }, + ECPointOperation { + curve: Curve, + operation: ECPointOperation, + }, + EcdsaSign { + curve: Curve, + privkey: Vec, + msg_hash: Vec, + }, + EcdsaVerify { + curve: Curve, + msg_hash: Vec, + pubkey: Vec, + signature: Vec, + }, + SchnorrSign { + curve: Curve, + privkey: Vec, + msg: Vec, + }, + SchnorrVerify { + curve: Curve, + pubkey: Vec, + msg: Vec, + signature: Vec, + }, } #[derive(Clone, Copy, PartialEq, Eq, Debug)] diff --git a/common/src/ecall_constants.rs b/common/src/ecall_constants.rs index edd023b..6082a93 100644 --- a/common/src/ecall_constants.rs +++ b/common/src/ecall_constants.rs @@ -13,7 +13,47 @@ pub const ECALL_POWM: u32 = 114; pub const MAX_BIGNUMBER_SIZE: usize = 64; +// HD derivations +pub enum CurveKind { + Secp256k1 = 0x21, +} + +// TODO: IDs for now are matching the ones in the ledger SDK +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[repr(C)] +pub enum HashId { + Ripemd160 = 1, + Sha256 = 3, + Sha512 = 5, +} + +// TODO: signing modes for now are matching the ones in the ledger SDK +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[repr(C)] +pub enum EcdsaSignMode { + RFC6979 = (3 << 9), +} + +// TODO: signing modes for now are matching the ones in the ledger SDK +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[repr(C)] +pub enum SchnorrSignMode { + BIP340 = 0, +} + +pub const ECALL_DERIVE_HD_NODE: u32 = 130; +pub const ECALL_GET_MASTER_FINGERPRINT: u32 = 131; + // Hash functions pub const ECALL_HASH_INIT: u32 = 150; pub const ECALL_HASH_UPDATE: u32 = 151; pub const ECALL_HASH_DIGEST: u32 = 152; + +// Operations for public keys over elliptic curves +pub const ECALL_ECFP_ADD_POINT: u32 = 160; +pub const ECALL_ECFP_SCALAR_MULT: u32 = 161; + +pub const ECALL_ECDSA_SIGN: u32 = 180; +pub const ECALL_ECDSA_VERIFY: u32 = 181; +pub const ECALL_SCHNORR_SIGN: u32 = 182; +pub const ECALL_SCHNORR_VERIFY: u32 = 183; diff --git a/vm/Cargo.toml b/vm/Cargo.toml index 8cd277b..21769eb 100644 --- a/vm/Cargo.toml +++ b/vm/Cargo.toml @@ -14,6 +14,7 @@ hex = { version = "0.4.3", default-features = false, features = ["serde", "alloc numtoa = "0.2.4" postcard = { version = "1.0.8", features = ["alloc"] } ledger_secure_sdk_sys = "1.5.3" +zeroize = "1.8.1" [profile.release] opt-level = 3 diff --git a/vm/src/handlers/lib/ecall.rs b/vm/src/handlers/lib/ecall.rs index b500f6b..d7d88dc 100644 --- a/vm/src/handlers/lib/ecall.rs +++ b/vm/src/handlers/lib/ecall.rs @@ -6,10 +6,11 @@ use common::{ Message, ReceiveBufferMessage, ReceiveBufferResponse, SendBufferMessage, SendPanicBufferMessage, }, - ecall_constants::*, + ecall_constants::{self, *}, manifest::Manifest, vm::{Cpu, EcallHandler}, }; +use ledger_device_sdk::hash::HashInit; use ledger_secure_sdk_sys::{ cx_ripemd160_t, cx_sha256_t, cx_sha512_t, CX_OK, CX_RIPEMD160, CX_SHA256, CX_SHA512, }; @@ -18,6 +19,11 @@ use crate::{AppSW, Instruction}; use super::outsourced_mem::OutsourcedMemory; +use zeroize::Zeroizing; + +// BIP32 supports up to 255, but we don't want that many, and it would be very slow anyway +const MAX_BIP32_PATH: usize = 16; + #[allow(dead_code)] #[derive(Debug, Clone, Copy)] enum Register { @@ -669,6 +675,448 @@ impl<'a> CommEcallHandler<'a> { Ok(()) } + + fn handle_derive_hd_node( + &self, + cpu: &mut Cpu>, + curve: u32, + path: GuestPointer, + path_len: usize, + private_key: GuestPointer, + chain_code: GuestPointer, + ) -> Result<(), &'static str> { + if curve != CurveKind::Secp256k1 as u32 { + return Err("Unsupported curve"); + } + if path_len > MAX_BIP32_PATH { + return Err("path_len is too large"); + } + + // copy path to local memory (if path_len == 0, the pointer is invalid, + // so we don't want to read from the segment) + let mut path_local_raw: [u8; MAX_BIP32_PATH * 4] = [0; MAX_BIP32_PATH * 4]; + if path_len > 0 { + cpu.get_segment(path.0)? + .read_buffer(path.0, &mut path_local_raw[0..(path_len * 4)])?; + } + + // convert to a slice of u32, by taking 4 bytes at the time as big-endian integers + let path_local = unsafe { + core::slice::from_raw_parts(path_local_raw.as_ptr() as *const u32, path_len as usize) + }; + + // derive the key + let mut private_key_local = Zeroizing::new([0u8; 32]); + let mut chain_code_local: [u8; 32] = [0; 32]; + unsafe { + ledger_secure_sdk_sys::os_perso_derive_node_bip32( + curve as u8, + path_local.as_ptr(), + path_len as u32, + private_key_local.as_mut_ptr(), + chain_code_local.as_mut_ptr(), + ); + } + + // copy private_key and chain_code to V-App memory + cpu.get_segment(private_key.0)? + .write_buffer(private_key.0, &private_key_local[..])?; + cpu.get_segment(chain_code.0)? + .write_buffer(chain_code.0, &chain_code_local)?; + + Ok(()) + } + + fn handle_get_master_fingerprint( + &self, + _cpu: &mut Cpu>, + curve: u32, + ) -> Result { + if curve != CurveKind::Secp256k1 as u32 { + return Err("Unsupported curve"); + } + + // derive the key + let mut private_key_local = Zeroizing::new([0u8; 32]); + let mut chain_code_local: [u8; 32] = [0; 32]; + + let mut pubkey: ledger_secure_sdk_sys::cx_ecfp_public_key_t = Default::default(); + unsafe { + ledger_secure_sdk_sys::os_perso_derive_node_bip32( + CurveKind::Secp256k1 as u8, + [].as_ptr(), + 0, + private_key_local.as_mut_ptr(), + chain_code_local.as_mut_ptr(), + ); + + // generate the corresponding public key + let mut privkey: ledger_secure_sdk_sys::cx_ecfp_private_key_t = Default::default(); + + let ret1 = ledger_secure_sdk_sys::cx_ecfp_init_private_key_no_throw( + curve as u8, + private_key_local.as_ptr(), + private_key_local.len(), + &mut privkey, + ); + + let ret2 = ledger_secure_sdk_sys::cx_ecfp_generate_pair_no_throw( + curve as u8, + &mut pubkey, + &mut privkey, + true, + ); + + if ret1 != CX_OK || ret2 != CX_OK { + return Err("Failed to generate key pair"); + } + } + + let mut sha_hasher = ledger_device_sdk::hash::sha2::Sha2_256::new(); + sha_hasher.update(&[02u8 + (pubkey.W[64] % 2)]).unwrap(); + sha_hasher.update(&pubkey.W[1..33]).unwrap(); + let mut sha256hash = [0u8; 32]; + sha_hasher.finalize(&mut sha256hash).unwrap(); + let mut ripemd160_hasher = ledger_device_sdk::hash::ripemd::Ripemd160::new(); + ripemd160_hasher.update(&sha256hash).unwrap(); + let mut rip = [0u8; 20]; + ripemd160_hasher.finalize(&mut rip).unwrap(); + Ok(u32::from_be_bytes([rip[0], rip[1], rip[2], rip[3]])) + } + + fn handle_ecfp_add_point( + &self, + cpu: &mut Cpu>, + curve: u32, + r: GuestPointer, + p: GuestPointer, + q: GuestPointer, + ) -> Result { + if curve != CurveKind::Secp256k1 as u32 { + return Err("Unsupported curve"); + } + + // copy inputs to local memory + let mut p_local: ledger_secure_sdk_sys::cx_ecfp_public_key_t = Default::default(); + p_local.curve = curve as u8; + p_local.W_len = 65; + cpu.get_segment(p.0)?.read_buffer(p.0, &mut p_local.W)?; + + let mut q_local: ledger_secure_sdk_sys::cx_ecfp_public_key_t = Default::default(); + q_local.curve = curve as u8; + q_local.W_len = 65; + cpu.get_segment(q.0)?.read_buffer(q.0, &mut q_local.W)?; + + let mut r_local: ledger_secure_sdk_sys::cx_ecfp_public_key_t = Default::default(); + unsafe { + let res = ledger_secure_sdk_sys::cx_ecfp_add_point_no_throw( + curve as u8, + r_local.W.as_mut_ptr(), + p_local.W.as_ptr(), + q_local.W.as_ptr(), + ); + if res != CX_OK { + return Err("add_point failed"); + } + } + + // copy r_local to r + let segment = cpu.get_segment(r.0)?; + segment.write_buffer(r.0, &r_local.W)?; + + Ok(1) + } + + fn handle_ecfp_scalar_mult( + &self, + cpu: &mut Cpu>, + curve: u32, + r: GuestPointer, + p: GuestPointer, + k: GuestPointer, + k_len: usize, + ) -> Result { + if curve != CurveKind::Secp256k1 as u32 { + return Err("Unsupported curve"); + } + + if k_len > 32 { + // TODO: do we need to support any larger? + return Err("k_len is too large"); + } + + // copy inputs to local memory + // we use r_local also for the final result + let mut r_local: ledger_secure_sdk_sys::cx_ecfp_public_key_t = Default::default(); + r_local.curve = curve as u8; + r_local.W_len = 65; + cpu.get_segment(p.0)?.read_buffer(p.0, &mut r_local.W)?; + + let mut k_local: [u8; 32] = [0; 32]; + cpu.get_segment(k.0)? + .read_buffer(k.0, &mut k_local[0..k_len])?; + + unsafe { + let res = ledger_secure_sdk_sys::cx_ecfp_scalar_mult_no_throw( + curve as u8, + r_local.W.as_mut_ptr(), + k_local.as_ptr(), + k_len, + ); + if res != CX_OK { + return Err("scalar_mult failed"); + } + } + + // copy r_local to r + let segment = cpu.get_segment(r.0)?; + segment.write_buffer(r.0, &r_local.W)?; + + Ok(1) + } + + fn handle_ecdsa_sign( + &self, + cpu: &mut Cpu>, + curve: u32, + mode: u32, + hash_id: u32, + privkey: GuestPointer, + msg_hash: GuestPointer, + signature: GuestPointer, + ) -> Result { + if curve != CurveKind::Secp256k1 as u32 { + return Err("Unsupported curve"); + } + + if mode != ecall_constants::EcdsaSignMode::RFC6979 as u32 { + return Err("Invalid or unsupported ecdsa signing mode"); + } + + if hash_id != ecall_constants::HashId::Sha256 as u32 { + return Err("Invalid or unsupported hash id"); + } + + // copy inputs to local memory + // TODO: we should zeroize the private key after use + let mut privkey_local: ledger_secure_sdk_sys::cx_ecfp_private_key_t = Default::default(); + privkey_local.curve = curve as u8; + privkey_local.d_len = 32; + cpu.get_segment(privkey.0)? + .read_buffer(privkey.0, &mut privkey_local.d)?; + + let mut msg_hash_local: [u8; 32] = [0; 32]; + cpu.get_segment(msg_hash.0)? + .read_buffer(msg_hash.0, &mut msg_hash_local)?; + + // ECDSA signatures are at most 72 bytes long. + let mut signature_local: [u8; 72] = [0; 72]; + let mut signature_len: usize = signature_local.len(); + let mut info: u32 = 0; // will get the parity bit + + unsafe { + let res = ledger_secure_sdk_sys::cx_ecdsa_sign_no_throw( + &mut privkey_local, + ecall_constants::EcdsaSignMode::RFC6979 as u32, + ecall_constants::HashId::Sha256 as u8, + msg_hash_local.as_ptr(), + msg_hash_local.len(), + signature_local.as_mut_ptr(), + &mut signature_len, + &mut info, + ); + if res != CX_OK { + return Err("cx_ecdsa_sign_no_throw failed"); + } + } + + // copy signature to V-App memory + cpu.get_segment(signature.0)? + .write_buffer(signature.0, &signature_local[0..signature_len as usize])?; + + Ok(signature_len) + } + + fn handle_ecdsa_verify( + &self, + cpu: &mut Cpu>, + curve: u32, + pubkey: GuestPointer, + msg_hash: GuestPointer, + signature: GuestPointer, + signature_len: usize, + ) -> Result { + if curve != CurveKind::Secp256k1 as u32 { + return Err("Unsupported curve"); + } + + if signature_len > 72 { + return Err("signature_len is too large"); + } + + // copy inputs to local memory + let mut pubkey_local: ledger_secure_sdk_sys::cx_ecfp_public_key_t = Default::default(); + pubkey_local.curve = curve as u8; + pubkey_local.W_len = 65; + cpu.get_segment(pubkey.0)? + .read_buffer(pubkey.0, &mut pubkey_local.W)?; + + let mut msg_hash_local: [u8; 32] = [0; 32]; + cpu.get_segment(msg_hash.0)? + .read_buffer(msg_hash.0, &mut msg_hash_local)?; + + let mut signature_local: [u8; 72] = [0; 72]; + cpu.get_segment(signature.0)? + .read_buffer(signature.0, &mut signature_local[0..signature_len])?; + + // verify the signature + let res = unsafe { + ledger_secure_sdk_sys::cx_ecdsa_verify_no_throw( + &pubkey_local, + msg_hash_local.as_ptr(), + msg_hash_local.len(), + signature_local.as_ptr(), + signature_len, + ) + }; + + Ok(res as u32) + } + + fn handle_schnorr_sign( + &self, + cpu: &mut Cpu>, + curve: u32, + mode: u32, + hash_id: u32, + privkey: GuestPointer, + msg: GuestPointer, + msg_len: usize, + signature: GuestPointer, + ) -> Result { + if curve != CurveKind::Secp256k1 as u32 { + return Err("Unsupported curve"); + } + + if mode != ecall_constants::SchnorrSignMode::BIP340 as u32 { + return Err("Invalid or unsupported schnorr signing mode"); + } + + if msg_len > 128 { + return Err("msg_len is too large"); + } + + if hash_id != ecall_constants::HashId::Sha256 as u32 { + return Err("Invalid or unsupported hash id"); + } + + // copy inputs to local memory + // TODO: we should zeroize the private key after use + let mut privkey_local: ledger_secure_sdk_sys::cx_ecfp_private_key_t = Default::default(); + privkey_local.curve = curve as u8; + privkey_local.d_len = 32; + cpu.get_segment(privkey.0)? + .read_buffer(privkey.0, &mut privkey_local.d)?; + + let mut msg_local = vec![0; 128]; + cpu.get_segment(msg.0)?.read_buffer(msg.0, &mut msg_local)?; + + // Schnorr signatures are at most 64 bytes long. + let mut signature_local: [u8; 64] = [0; 64]; + let mut signature_len: usize = signature_local.len(); + + unsafe { + // We don't expose this, but cx_ecschnorr_sign_no_throw requires one of + // CX_RND_TRNG or CX_RND_PROVIDED to be provided. We just use CX_RND_TRNG for now. + const CX_RND_TRNG: u32 = 2 << 9; + + let res = ledger_secure_sdk_sys::cx_ecschnorr_sign_no_throw( + &mut privkey_local, + mode | CX_RND_TRNG, + ecall_constants::HashId::Sha256 as u8, + msg_local.as_ptr(), + msg_len, + signature_local.as_mut_ptr(), + &mut signature_len, + ); + if res != CX_OK { + return Err("cx_schnorr_sign_no_throw failed"); + } + } + + // signatures returned per BIP340 are always exactly 64 bytes + if signature_len != 64 { + return Err("cx_schnorr_sign_no_throw returned a signature of unexpected length"); + } + + // copy signature to V-App memory + cpu.get_segment(signature.0)? + .write_buffer(signature.0, &signature_local[0..signature_len as usize])?; + + Ok(signature_len) + } + + fn handle_schnorr_verify( + &self, + cpu: &mut Cpu>, + curve: u32, + mode: u32, + hash_id: u32, + pubkey: GuestPointer, + msg: GuestPointer, + msg_len: usize, + signature: GuestPointer, + signature_len: usize, + ) -> Result { + if curve != CurveKind::Secp256k1 as u32 { + return Err("Unsupported curve"); + } + + if mode != ecall_constants::SchnorrSignMode::BIP340 as u32 { + return Err("Invalid or unsupported schnorr signing mode"); + } + + if msg_len > 128 { + return Err("msg_len is too large"); + } + + if hash_id != ecall_constants::HashId::Sha256 as u32 { + return Err("Invalid or unsupported hash id"); + } + + if signature_len != 64 { + return Err("Invalid signature length"); + } + + // copy inputs to local memory + let mut pubkey_local: ledger_secure_sdk_sys::cx_ecfp_public_key_t = Default::default(); + pubkey_local.curve = curve as u8; + pubkey_local.W_len = 65; + cpu.get_segment(pubkey.0)? + .read_buffer(pubkey.0, &mut pubkey_local.W)?; + + let mut msg_local = vec![0; 128]; + cpu.get_segment(msg.0)?.read_buffer(msg.0, &mut msg_local)?; + + let mut signature_local: [u8; 64] = [0; 64]; + cpu.get_segment(signature.0)? + .read_buffer(signature.0, &mut signature_local)?; + + // verify the signature + let res = unsafe { + ledger_secure_sdk_sys::cx_ecschnorr_verify( + &pubkey_local, + mode, + ecall_constants::HashId::Sha256 as u8, + msg_local.as_ptr(), + msg_len, + signature_local.as_ptr(), + signature_len, + ) + }; + + Ok(res as u32) + } } // make an error type for the CommEcallHandler<'a> @@ -819,6 +1267,100 @@ impl<'a> EcallHandler for CommEcallHandler<'a> { .handle_hash_digest(cpu, reg!(A0), GPreg!(A1), GPreg!(A2)) .map_err(|_| CommEcallError::GenericError("hash_digest failed"))?, + ECALL_DERIVE_HD_NODE => { + self.handle_derive_hd_node( + cpu, + reg!(A0), + GPreg!(A1), + reg!(A2) as usize, + GPreg!(A3), + GPreg!(A4), + ) + .map_err(|_| CommEcallError::GenericError("HD node derivation failed"))?; + + reg!(A0) = 1; + } + ECALL_GET_MASTER_FINGERPRINT => { + reg!(A0) = self + .handle_get_master_fingerprint(cpu, reg!(A0)) + .map_err(|_| CommEcallError::GenericError("get_master_fingerprint"))?; + } + + ECALL_ECFP_ADD_POINT => { + reg!(A0) = self + .handle_ecfp_add_point(cpu, reg!(A0), GPreg!(A1), GPreg!(A2), GPreg!(A3)) + .map_err(|_| CommEcallError::GenericError("ecfp_add_point failed"))?; + } + ECALL_ECFP_SCALAR_MULT => { + reg!(A0) = self + .handle_ecfp_scalar_mult( + cpu, + reg!(A0), + GPreg!(A1), + GPreg!(A2), + GPreg!(A3), + reg!(A4) as usize, + ) + .map_err(|_| CommEcallError::GenericError("ecfp_scalar_mult failed"))?; + } + + ECALL_ECDSA_SIGN => { + reg!(A0) = self + .handle_ecdsa_sign( + cpu, + reg!(A0), + reg!(A1), + reg!(A2), + GPreg!(A3), + GPreg!(A4), + GPreg!(A5), + ) + .map_err(|_| CommEcallError::GenericError("ecdsa_sign failed"))? + as u32; + } + ECALL_ECDSA_VERIFY => { + reg!(A0) = self + .handle_ecdsa_verify( + cpu, + reg!(A0), + GPreg!(A1), + GPreg!(A2), + GPreg!(A3), + reg!(A4) as usize, + ) + .map_err(|_| CommEcallError::GenericError("ecdsa_verify failed"))?; + } + ECALL_SCHNORR_SIGN => { + reg!(A0) = self + .handle_schnorr_sign( + cpu, + reg!(A0), + reg!(A1), + reg!(A2), + GPreg!(A3), + GPreg!(A4), + reg!(A5) as usize, + GPreg!(A6), + ) + .map_err(|_| CommEcallError::GenericError("schnorr_sign failed"))? + as u32; + } + ECALL_SCHNORR_VERIFY => { + reg!(A0) = self + .handle_schnorr_verify( + cpu, + reg!(A0), + reg!(A1), + reg!(A2), + GPreg!(A3), + GPreg!(A4), + reg!(A5) as usize, + GPreg!(A6), + reg!(A7) as usize, + ) + .map_err(|_| CommEcallError::GenericError("schnorr_verify failed"))?; + } + // Any other ecall is unhandled and will case the CPU to abort _ => { return Err(CommEcallError::UnhandledEcall);