1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-12-22 14:28:07 +00:00

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
This commit is contained in:
matejcik 2024-01-22 13:44:02 +01:00 committed by Jiří Musil
parent 76296ad417
commit 3b88116bba
12 changed files with 333 additions and 2 deletions

View File

@ -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"

View File

@ -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

View File

@ -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")

2
core/embed/rust/crypto.h Normal file
View File

@ -0,0 +1,2 @@
#include "ed25519-donna/ed25519.h"
#include "sha2.h"

View File

@ -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<Vec<ed25519::PublicKey, MAX_PUBKEYS>, 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<ed25519::PublicKey, Error> {
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)
}
}

View File

@ -0,0 +1,36 @@
use super::{ffi, Error};
pub type Signature = ffi::ed25519_signature;
pub const SIGNATURE_SIZE: usize = core::mem::size_of::<Signature>();
pub type PublicKey = ffi::ed25519_public_key;
pub const PUBLIC_KEY_SIZE: usize = core::mem::size_of::<PublicKey>();
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)
}
}

View File

@ -0,0 +1,3 @@
#![allow(non_camel_case_types)]
include!(concat!(env!("OUT_DIR"), "/crypto.rs"));

View File

@ -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
}

View File

@ -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<Error> 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"),
}
}
}

View File

@ -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::<Memory>::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);
}
}
}

View File

@ -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))
};
}

View File

@ -18,6 +18,8 @@ mod error;
#[macro_use]
mod trezorhal;
#[cfg(feature = "crypto")]
mod crypto;
mod io;
mod maybe_trace;
#[cfg(feature = "micropython")]