From 3b88116bba587f6e7d55cb55e96f1778b552099f Mon Sep 17 00:00:00 2001 From: matejcik Date: Mon, 22 Jan 2024 13:44:02 +0100 Subject: [PATCH] feat(core/rust): add trezorcrypto bindings for now, we use sha256 and a little of ed25519 for CoSi purposes also add the Merkle root algorithm --- core/embed/rust/Cargo.lock | 7 ++ core/embed/rust/Cargo.toml | 5 +- core/embed/rust/build.rs | 33 +++++++ core/embed/rust/crypto.h | 2 + core/embed/rust/src/crypto/cosi.rs | 63 +++++++++++++ core/embed/rust/src/crypto/ed25519.rs | 36 ++++++++ core/embed/rust/src/crypto/ffi.rs | 3 + core/embed/rust/src/crypto/merkle.rs | 30 ++++++ core/embed/rust/src/crypto/mod.rs | 24 +++++ core/embed/rust/src/crypto/sha256.rs | 128 ++++++++++++++++++++++++++ core/embed/rust/src/error.rs | 2 +- core/embed/rust/src/lib.rs | 2 + 12 files changed, 333 insertions(+), 2 deletions(-) create mode 100644 core/embed/rust/crypto.h create mode 100644 core/embed/rust/src/crypto/cosi.rs create mode 100644 core/embed/rust/src/crypto/ed25519.rs create mode 100644 core/embed/rust/src/crypto/ffi.rs create mode 100644 core/embed/rust/src/crypto/merkle.rs create mode 100644 core/embed/rust/src/crypto/mod.rs create mode 100644 core/embed/rust/src/crypto/sha256.rs diff --git a/core/embed/rust/Cargo.lock b/core/embed/rust/Cargo.lock index 4e00b8424c..76905fa429 100644 --- a/core/embed/rust/Cargo.lock +++ b/core/embed/rust/Cargo.lock @@ -327,6 +327,7 @@ dependencies = [ "qrcodegen", "serde_json", "trezor-tjpgdec", + "zeroize", ] [[package]] @@ -356,3 +357,9 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" diff --git a/core/embed/rust/Cargo.toml b/core/embed/rust/Cargo.toml index d23cedbe21..bd9ead0b1e 100644 --- a/core/embed/rust/Cargo.toml +++ b/core/embed/rust/Cargo.toml @@ -8,6 +8,7 @@ build = "build.rs" [features] default = ["model_tt"] bitcoin_only = [] +crypto = ["zeroize"] model_tt = ["jpeg"] model_tr = [] micropython = [] @@ -32,10 +33,11 @@ rgb_led = [] backlight = [] usb = [] optiga = [] -translations = [] +translations = ["crypto"] test = [ "button", "cc", + "crypto", "debug", "glob", "micropython", @@ -74,6 +76,7 @@ debug = 2 [dependencies] qrcodegen = { version = "1.8.0", path = "../../vendor/QR-Code-generator/rust-no-heap" } trezor-tjpgdec = { version = "0.1.0", path = "../../../rust/trezor-tjpgdec" } +zeroize = { version = "1.7.0", default-features = false, optional = true } # Runtime dependencies diff --git a/core/embed/rust/build.rs b/core/embed/rust/build.rs index 47f90b548f..8257d1082c 100644 --- a/core/embed/rust/build.rs +++ b/core/embed/rust/build.rs @@ -8,6 +8,8 @@ fn main() { #[cfg(feature = "micropython")] generate_micropython_bindings(); generate_trezorhal_bindings(); + #[cfg(feature = "crypto")] + generate_crypto_bindings(); #[cfg(feature = "test")] link_core_objects(); } @@ -390,6 +392,37 @@ fn generate_trezorhal_bindings() { .unwrap(); } +fn generate_crypto_bindings() { + let out_path = env::var("OUT_DIR").unwrap(); + + // Tell cargo to invalidate the built crate whenever the header changes. + println!("cargo:rerun-if-changed=crypto.h"); + + let bindings = prepare_bindings() + .header("crypto.h") + // ed25519 + .allowlist_type("ed25519_signature") + .allowlist_type("ed25519_public_key") + .allowlist_function("ed25519_cosi_combine_publickeys") + // incorrect signature from bindgen, see crypto::ed25519:ffi_override + //.allowlist_function("ed25519_sign_open") + // sha256 + .allowlist_var("SHA256_DIGEST_LENGTH") + .allowlist_type("SHA256_CTX") + .no_copy("SHA256_CTX") + .allowlist_function("sha256_Init") + .allowlist_function("sha256_Update") + .allowlist_function("sha256_Final"); + + // Write the bindings to a file in the OUR_DIR. + bindings + .clang_arg("-fgnuc-version=0") // avoid weirdness with ed25519.h CONST definition + .generate() + .expect("Unable to generate bindings") + .write_to_file(PathBuf::from(out_path).join("crypto.rs")) + .unwrap(); +} + fn is_firmware() -> bool { let target = env::var("TARGET").unwrap(); target.starts_with("thumbv7") diff --git a/core/embed/rust/crypto.h b/core/embed/rust/crypto.h new file mode 100644 index 0000000000..ac81987c00 --- /dev/null +++ b/core/embed/rust/crypto.h @@ -0,0 +1,2 @@ +#include "ed25519-donna/ed25519.h" +#include "sha2.h" diff --git a/core/embed/rust/src/crypto/cosi.rs b/core/embed/rust/src/crypto/cosi.rs new file mode 100644 index 0000000000..2bdca576df --- /dev/null +++ b/core/embed/rust/src/crypto/cosi.rs @@ -0,0 +1,63 @@ +use heapless::Vec; + +use super::{ed25519, ffi, Error}; + +const MAX_PUBKEYS: usize = 3; + +pub struct Signature { + sigmask: u8, + signature: ed25519::Signature, +} + +impl Signature { + pub fn new(sigmask: u8, signature: ed25519::Signature) -> Self { + Self { sigmask, signature } + } +} + +pub fn verify( + threshold: u8, + message: &[u8], + public_keys: &[ed25519::PublicKey], + signature: &Signature, +) -> Result<(), Error> { + if threshold < 1 { + return Err(Error::InvalidParams); + } + let selected_keys = select_keys(signature.sigmask, public_keys)?; + if selected_keys.len() < threshold as usize { + return Err(Error::InvalidParams); + } + let combined_key = combine_publickeys(&selected_keys)?; + ed25519::verify(message, &combined_key, &signature.signature) +} + +fn select_keys( + mut sigmask: u8, + keys: &[ed25519::PublicKey], +) -> Result, Error> { + let mut selected_keys = Vec::new(); + for key in keys { + if sigmask & 1 != 0 { + unwrap!(selected_keys.push(*key)); + } + sigmask >>= 1; + } + if sigmask != 0 { + Err(Error::InvalidParams) + } else { + Ok(selected_keys) + } +} + +fn combine_publickeys(keys: &[ed25519::PublicKey]) -> Result { + let mut combined_key = ed25519::PublicKey::default(); + let res = unsafe { + ffi::ed25519_cosi_combine_publickeys(&mut combined_key as *mut _, keys.as_ptr(), keys.len()) + }; + if res == 0 { + Ok(combined_key) + } else { + Err(Error::InvalidEncoding) + } +} diff --git a/core/embed/rust/src/crypto/ed25519.rs b/core/embed/rust/src/crypto/ed25519.rs new file mode 100644 index 0000000000..338051c305 --- /dev/null +++ b/core/embed/rust/src/crypto/ed25519.rs @@ -0,0 +1,36 @@ +use super::{ffi, Error}; + +pub type Signature = ffi::ed25519_signature; +pub const SIGNATURE_SIZE: usize = core::mem::size_of::(); + +pub type PublicKey = ffi::ed25519_public_key; +pub const PUBLIC_KEY_SIZE: usize = core::mem::size_of::(); + +mod ffi_override { + // bindgen incorrectly generates pk and RS as *mut instead of *const + // https://github.com/rust-lang/rust-bindgen/pull/2684 + extern "C" { + pub fn ed25519_sign_open( + m: *const cty::c_uchar, + mlen: usize, + pk: *const cty::c_uchar, + RS: *const cty::c_uchar, + ) -> cty::c_int; + } +} + +pub fn verify(message: &[u8], public_key: &PublicKey, signature: &Signature) -> Result<(), Error> { + let res = unsafe { + ffi_override::ed25519_sign_open( + message.as_ptr(), + message.len(), + public_key.as_ptr(), + signature.as_ptr(), + ) + }; + if res == 0 { + Ok(()) + } else { + Err(Error::SignatureVerificationFailed) + } +} diff --git a/core/embed/rust/src/crypto/ffi.rs b/core/embed/rust/src/crypto/ffi.rs new file mode 100644 index 0000000000..a4c8a4b8d5 --- /dev/null +++ b/core/embed/rust/src/crypto/ffi.rs @@ -0,0 +1,3 @@ +#![allow(non_camel_case_types)] + +include!(concat!(env!("OUT_DIR"), "/crypto.rs")); diff --git a/core/embed/rust/src/crypto/merkle.rs b/core/embed/rust/src/crypto/merkle.rs new file mode 100644 index 0000000000..1bcbfc74cb --- /dev/null +++ b/core/embed/rust/src/crypto/merkle.rs @@ -0,0 +1,30 @@ +use super::sha256; + +/// Calculate a Merkle root based on a leaf element and a proof of inclusion. +/// +/// Expects the Merkle tree format specified in `ethereum-definitions.md`. +pub fn merkle_root(elem: &[u8], proof: &[sha256::Digest]) -> sha256::Digest { + let mut out = sha256::Digest::default(); + + // hash the leaf element + sha256::init_ctx!(ctx); + ctx.update(&[0x00]); + ctx.update(elem); + ctx.finalize_into(&mut out); + + for proof_elem in proof { + // hash together the current hash and the proof element + let (min, max) = if &out < proof_elem { + (&out, proof_elem) + } else { + (proof_elem, &out) + }; + sha256::init_ctx!(ctx); + ctx.update(&[0x01]); + ctx.update(min); + ctx.update(max); + ctx.finalize_into(&mut out); + } + + out +} diff --git a/core/embed/rust/src/crypto/mod.rs b/core/embed/rust/src/crypto/mod.rs new file mode 100644 index 0000000000..ecb5348f67 --- /dev/null +++ b/core/embed/rust/src/crypto/mod.rs @@ -0,0 +1,24 @@ +pub mod cosi; +pub mod ed25519; +mod ffi; +pub mod merkle; +pub mod sha256; + +pub enum Error { + // Signature verification failed + SignatureVerificationFailed, + // Provided value is not a valid public key / signature / etc. + InvalidEncoding, + // Provided parameters are not accepted (e.g., signature threshold out of bounds) + InvalidParams, +} + +impl From for crate::error::Error { + fn from(e: Error) -> Self { + match e { + Error::SignatureVerificationFailed => value_error!("Signature verification failed"), + Error::InvalidEncoding => value_error!("Invalid key or signature encoding"), + Error::InvalidParams => value_error!("Invalid cryptographic parameters"), + } + } +} diff --git a/core/embed/rust/src/crypto/sha256.rs b/core/embed/rust/src/crypto/sha256.rs new file mode 100644 index 0000000000..fc1c4c05ab --- /dev/null +++ b/core/embed/rust/src/crypto/sha256.rs @@ -0,0 +1,128 @@ +use core::{mem::MaybeUninit, pin::Pin}; + +use zeroize::{DefaultIsZeroes, Zeroize as _}; + +use super::ffi; + +type Memory = ffi::SHA256_CTX; + +impl Default for Memory { + fn default() -> Self { + // SAFETY: a zeroed block of memory is a valid SHA256_CTX + unsafe { MaybeUninit::::zeroed().assume_init() } + } +} + +impl DefaultIsZeroes for Memory {} + +pub const DIGEST_SIZE: usize = ffi::SHA256_DIGEST_LENGTH as usize; +pub type Digest = [u8; DIGEST_SIZE]; + +pub struct Sha256<'a> { + ctx: Pin<&'a mut Memory>, +} + +impl<'a> Sha256<'a> { + pub fn new(mut ctx: Pin<&'a mut Memory>) -> Self { + // initialize the context + // SAFETY: safe with whatever finds itself as memory contents + unsafe { ffi::sha256_Init(ctx.as_mut().get_unchecked_mut()) }; + Self { ctx } + } + + pub fn update(&mut self, data: &[u8]) { + // SAFETY: safe + unsafe { + ffi::sha256_Update( + self.ctx.as_mut().get_unchecked_mut(), + data.as_ptr(), + data.len(), + ) + }; + } + + pub fn memory() -> Memory { + Memory::default() + } + + pub fn finalize_into(mut self, out: &mut Digest) { + // SAFETY: safe + unsafe { ffi::sha256_Final(self.ctx.as_mut().get_unchecked_mut(), out.as_mut_ptr()) }; + } +} + +impl Drop for Sha256<'_> { + fn drop(&mut self) { + self.ctx.zeroize(); + } +} + +macro_rules! init_ctx { + ($name:ident) => { + // assign the backing memory to $name... + let mut $name = crate::crypto::sha256::Sha256::memory(); + // ... then make it inaccessible by overwriting the binding, and pin it + #[allow(unused_mut)] + let mut $name = unsafe { + crate::crypto::sha256::Sha256::new(core::pin::Pin::new_unchecked(&mut $name)) + }; + }; +} + +pub(crate) use init_ctx; + +pub fn digest_into(data: &[u8], out: &mut Digest) { + init_ctx!(ctx); + ctx.update(data); + ctx.finalize_into(out); +} + +pub fn digest(data: &[u8]) -> Digest { + let mut out = Digest::default(); + digest_into(data, &mut out); + out +} + +#[cfg(test)] +mod test { + use crate::strutil::hexlify; + + use super::*; + + const SHA256_EMPTY: &[u8] = b"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + const SHA256_VECTORS: &[(&[u8], &[u8])] = &[ + (b"", SHA256_EMPTY), + ( + b"abc", + b"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", + ), + ]; + + fn hexdigest(data: &[u8]) -> [u8; DIGEST_SIZE * 2] { + let mut out_hex = [0u8; DIGEST_SIZE * 2]; + + let digest = digest(data); + hexlify(&digest, &mut out_hex); + out_hex + } + + #[test] + fn test_empty_ctx() { + let mut out = Digest::default(); + let mut out_hex = [0u8; DIGEST_SIZE * 2]; + + init_ctx!(ctx); + ctx.finalize_into(&mut out); + hexlify(&out, &mut out_hex); + + assert_eq!(out_hex, SHA256_EMPTY); + } + + #[test] + fn test_vectors() { + for (data, expected) in SHA256_VECTORS { + let out_hex = hexdigest(data); + assert_eq!(out_hex, *expected); + } + } +} diff --git a/core/embed/rust/src/error.rs b/core/embed/rust/src/error.rs index b949f0539c..4b2db7d02e 100644 --- a/core/embed/rust/src/error.rs +++ b/core/embed/rust/src/error.rs @@ -30,7 +30,7 @@ pub enum Error { #[macro_export] macro_rules! value_error { ($msg:expr) => { - Error::ValueError(cstr_core::cstr!($msg)) + $crate::error::Error::ValueError(cstr_core::cstr!($msg)) }; } diff --git a/core/embed/rust/src/lib.rs b/core/embed/rust/src/lib.rs index 9e45c37ac9..3727d38d32 100644 --- a/core/embed/rust/src/lib.rs +++ b/core/embed/rust/src/lib.rs @@ -18,6 +18,8 @@ mod error; #[macro_use] mod trezorhal; +#[cfg(feature = "crypto")] +mod crypto; mod io; mod maybe_trace; #[cfg(feature = "micropython")]