refactor(core/rust/crypto): prepare crypto crate for API bindings
What changed, and why it matters
This is a large internal refactor of how Trezor's Rust crypto code handles sensitive memory. It replaces an older pinning-based mechanism with a new 'copy hazard' framework designed to prevent Rust from accidentally duplicating secret data in memory. The change itself is defensive and does not appear to introduce a new vulnerability; rather, it is a hardening measure. However, because it touches low-level cryptographic state handling across many files, any mistake in the new abstraction could theoretically affect how secrets are cleared from memory.
Treat as a hardening refactor rather than a security fix. Review the new SecretContextLock and HazardGuard abstractions for soundness, ensure zeroize-on-drop is not optimized out, and verify that all call sites using HazardGuard::hazard_new (e.g., TrezorCryptoSha256 in thp/crypto.rs) are appropriately justified and audited. No urgent patch is indicated.
Security signals we found
Refactor of sensitive-memory handling in cryptographic code
Introduction of zeroizing wrapper types (SecretContext, SecretContextLock)
Removal of Pin-based Memory<T> in favor of explicit 'copy hazard' annotations
Expansion of FFI allowlists for ECDSA and SHA3/Keccak
Explicit 'COPY HAZARD' safety comments added around FFI context mutations
Evidence from the diff
The commit refactors core/embed/crypto to replace the Memory
Changed components
core/embed/crypto/src/secret.rscore/embed/crypto/src/sha256.rscore/embed/crypto/src/sha512.rscore/embed/crypto/src/hmac.rscore/embed/crypto/src/aesgcm.rscore/embed/crypto/src/memory.rscore/embed/crypto/src/merkle.rscore/embed/crypto/src/cosi.rscore/embed/crypto/src/crc32.rscore/embed/crypto/src/lib.rscore/embed/crypto/src/ffi.rscore/embed/crypto/build.rscore/embed/rust/src/thp/crypto.rscore/embed/rust/src/translations/blob.rsInspect captured patch +637 / −322
### core/embed/crypto/build.rs
@@ -148,13 +148,28 @@ fn add_crypto_base(lib: &mut CLibrary, common_attrs: &CompileAttrs) -> Result<()
lib.add_rust_bindings(|builder| {
Ok(builder
+ .header(format!("{CRYPTO_PATH}/ecdsa.h"))
.header(format!("{CRYPTO_PATH}/ed25519-donna/ed25519.h"))
.header(format!("{CRYPTO_PATH}/elligator2.h"))
.header(format!("{CRYPTO_PATH}/hmac.h"))
+ .header(format!("{CRYPTO_PATH}/nist256p1.h"))
+ .header(format!("{CRYPTO_PATH}/secp256k1.h"))
.header(format!("{CRYPTO_PATH}/sha2.h"))
+ .header(format!("{CRYPTO_PATH}/sha3.h"))
// curve25519
.allowlist_function("curve25519_scalarmult")
.allowlist_function("curve25519_scalarmult_basepoint")
+ // ecdsa
+ .allowlist_var("ECDSA_PUBLIC_KEY_SIZE")
+ .allowlist_var("ECDSA_PUBLIC_KEY_COMPRESSED_SIZE")
+ .allowlist_var("ECDSA_SCALAR_SIZE")
+ .allowlist_var("ECDSA_RAW_SIGNATURE_SIZE")
+ .allowlist_type("ecdsa_curve")
+ .no_copy("ecdsa_curve")
+ .allowlist_var("secp256k1")
+ .allowlist_var("nist256p1")
+ .allowlist_function("ecdsa_verify_digest")
+ .allowlist_function("ecdsa_recover_pub_from_sig")
// ed25519
.allowlist_type("ed25519_signature")
.allowlist_type("ed25519_public_key")
@@ -183,7 +198,22 @@ fn add_crypto_base(lib: &mut CLibrary, common_attrs: &CompileAttrs) -> Result<()
.no_copy("SHA512_CTX")
.allowlist_function("sha512_Init")
.allowlist_function("sha512_Update")
- .allowlist_function("sha512_Final"))
+ .allowlist_function("sha512_Final")
+ // sha3
+ .allowlist_var("SHA3_224_BLOCK_LENGTH")
+ .allowlist_var("SHA3_224_DIGEST_LENGTH")
+ .allowlist_var("SHA3_256_BLOCK_LENGTH")
+ .allowlist_var("SHA3_256_DIGEST_LENGTH")
+ .allowlist_var("SHA3_384_BLOCK_LENGTH")
+ .allowlist_var("SHA3_384_DIGEST_LENGTH")
+ .allowlist_var("SHA3_512_BLOCK_LENGTH")
+ .allowlist_var("SHA3_512_DIGEST_LENGTH")
+ .allowlist_type("SHA3_CTX")
+ .no_copy("SHA3_CTX")
+ .allowlist_function("sha3_Init")
+ .allowlist_function("sha3_Update")
+ .allowlist_function("sha3_Final")
+ .allowlist_function("keccak_Final"))
})?;
Ok(())
### core/embed/crypto/src/aesgcm.rs
@@ -1,9 +1,8 @@
-use core::pin::Pin;
-
+use rtl::CSlice;
use rtl::error::ensure;
use zeroize::Zeroize;
-use super::memory::Memory;
+use super::secret::{SecretContext, SecretContextLock, ZeroableMemory};
use super::{Error, consteq, ffi};
// Tag size is a parameter but we fix it to 16 here for simplicity.
@@ -23,40 +22,57 @@ enum State {
Failed,
}
+// SAFETY: gcm_ctx is valid when zeroed
+unsafe impl ZeroableMemory for ffi::gcm_ctx {}
+
+pub type AesGcmContext = SecretContext<ffi::gcm_ctx>;
+
struct AesGcmInner<'a> {
- ctx: Pin<&'a mut Memory<ffi::gcm_ctx>>,
+ ctx: SecretContextLock<&'a mut AesGcmContext>,
state: State,
}
pub struct AesGcmEncrypt<'a>(AesGcmInner<'a>);
pub struct AesGcmDecrypt<'a>(AesGcmInner<'a>);
impl<'a> AesGcmInner<'a> {
- fn new(
- mut ctx: Pin<&'a mut Memory<ffi::gcm_ctx>>,
- key: &[u8],
- iv: &[u8],
- ) -> Result<Self, Error> {
+ /// Construct a new AES-GCM context.
+ fn new(ctx: &'a mut AesGcmContext, key: &[u8], iv: &[u8]) -> Result<Self, Error> {
if !KEY_SIZES.contains(&key.len()) {
return Err(Error::InvalidParams);
}
+
+ let key_ptr = CSlice::from(key);
+
// initialize the context
// SAFETY: ffi
- let res =
- unsafe { ffi::gcm_init_and_key(key.as_ptr(), key.len() as cty::c_ulong, ctx.inner()) };
+ // COPY HAZARD: this call operates on ctx in-place
+ let res = unsafe {
+ ffi::gcm_init_and_key(
+ key_ptr.ptr(),
+ key_ptr.len() as cty::c_ulong,
+ ctx.hazard_mut(),
+ )
+ };
ensure!(res == RETURN_GOOD, "gcm_init_and_key");
let mut aesgcm = Self {
- ctx,
+ ctx: SecretContextLock::new(ctx),
state: State::Init,
};
aesgcm.reset(iv);
Ok(aesgcm)
}
fn reset(&mut self, iv: &[u8]) {
+ let iv_ptr = CSlice::from(iv);
// SAFETY: ffi
+ // COPY HAZARD: this call operates on ctx in-place
let res = unsafe {
- ffi::gcm_init_message(iv.as_ptr(), iv.len() as cty::c_ulong, self.ctx.inner())
+ ffi::gcm_init_message(
+ iv_ptr.ptr(),
+ iv_ptr.len() as cty::c_ulong,
+ self.ctx.hazard_mut(),
+ )
};
ensure!(res == RETURN_GOOD, "gcm_init_message");
self.state = State::Init;
@@ -65,9 +81,15 @@ impl<'a> AesGcmInner<'a> {
fn auth(&mut self, data: &[u8]) -> Result<(), Error> {
self.check_state(&[State::Init, State::Processing])?;
+ let data_ptr = CSlice::from(data);
// SAFETY: ffi
+ // COPY HAZARD: this call operates on ctx in-place
let res = unsafe {
- ffi::gcm_auth_header(data.as_ptr(), data.len() as cty::c_ulong, self.ctx.inner())
+ ffi::gcm_auth_header(
+ data_ptr.ptr(),
+ data_ptr.len() as cty::c_ulong,
+ self.ctx.hazard_mut(),
+ )
};
ensure!(res == RETURN_GOOD, "gcm_auth_header");
Ok(())
@@ -79,11 +101,12 @@ impl<'a> AesGcmInner<'a> {
let mut tag = [0u8; TAG_SIZE];
// SAFETY: ffi
+ // COPY HAZARD: this call operates on ctx in-place
let res = unsafe {
ffi::gcm_compute_tag(
tag.as_mut_ptr(),
tag.len() as cty::c_ulong,
- self.ctx.inner(),
+ self.ctx.hazard_mut(),
)
};
if res != RETURN_GOOD {
@@ -102,11 +125,7 @@ impl<'a> AesGcmInner<'a> {
}
impl<'a> AesGcmEncrypt<'a> {
- pub fn new(
- ctx: Pin<&'a mut Memory<ffi::gcm_ctx>>,
- key: &[u8],
- iv: &[u8],
- ) -> Result<Self, Error> {
+ pub fn new(ctx: &'a mut AesGcmContext, key: &[u8], iv: &[u8]) -> Result<Self, Error> {
Ok(Self(AesGcmInner::new(ctx, key, iv)?))
}
@@ -140,11 +159,13 @@ impl<'a> AesGcmEncrypt<'a> {
self.0.check_state(&[State::Init, State::Processing])?;
self.0.state = State::Processing;
+ // SAFETY: ffi
+ // COPY HAZARD: this call operates on ctx in-place
let res = unsafe {
ffi::gcm_encrypt(
data.as_mut_ptr(),
data.len() as cty::c_ulong,
- self.0.ctx.inner(),
+ self.0.ctx.hazard_mut(),
)
};
ensure!(res == RETURN_GOOD, "gcm_encrypt");
@@ -154,18 +175,10 @@ impl<'a> AesGcmEncrypt<'a> {
pub fn finish(&mut self) -> Result<Tag, Error> {
self.0.finish()
}
-
- pub fn memory() -> Memory<ffi::gcm_ctx> {
- Memory::default()
- }
}
impl<'a> AesGcmDecrypt<'a> {
- pub fn new(
- ctx: Pin<&'a mut Memory<ffi::gcm_ctx>>,
- key: &[u8],
- iv: &[u8],
- ) -> Result<Self, Error> {
+ pub fn new(ctx: &'a mut AesGcmContext, key: &[u8], iv: &[u8]) -> Result<Self, Error> {
Ok(Self(AesGcmInner::new(ctx, key, iv)?))
}
@@ -195,11 +208,12 @@ impl<'a> AesGcmDecrypt<'a> {
self.0.state = State::Processing;
// SAFETY: ffi
+ // COPY HAZARD: this call operates on ctx in-place
let res = unsafe {
ffi::gcm_decrypt(
data.as_mut_ptr(),
data.len() as cty::c_ulong,
- self.0.ctx.inner(),
+ self.0.ctx.hazard_mut(),
)
};
ensure!(res == RETURN_GOOD, "gcm_decrypt");
@@ -213,21 +227,10 @@ impl<'a> AesGcmDecrypt<'a> {
}
Ok(())
}
-
- pub fn memory() -> Memory<ffi::gcm_ctx> {
- Memory::default()
- }
-}
-
-impl Drop for AesGcmInner<'_> {
- fn drop(&mut self) {
- self.ctx.zeroize();
- }
}
#[cfg(test)]
mod test {
- use super::super::memory::init_ctx;
use super::*;
struct Vector {
@@ -373,10 +376,10 @@ mod test {
for v in AES_GCM_VECTORS {
let (key, iv, aad, plaintext, ciphertext, tag) = v.decoded();
- init_ctx!(AesGcmEncrypt, ctx_enc, &key, &iv);
- let mut ctx_enc = ctx_enc.unwrap();
- init_ctx!(AesGcmDecrypt, ctx_dec, &key, &iv);
- let mut ctx_dec = ctx_dec.unwrap();
+ let mut ctx_enc = AesGcmContext::default();
+ let mut ctx_enc = AesGcmEncrypt::new(&mut ctx_enc, &key, &iv).unwrap();
+ let mut ctx_dec = AesGcmContext::default();
+ let mut ctx_dec = AesGcmDecrypt::new(&mut ctx_dec, &key, &iv).unwrap();
if !plaintext.is_empty() {
let mut buffer = vec![0; plaintext.len()];
@@ -401,13 +404,13 @@ mod test {
#[test]
fn test_state() {
// ok: empty string tag - encryption
- init_ctx!(AesGcmEncrypt, ctx_enc, &[0u8; 16], b"1");
- let mut ctx_enc = ctx_enc.unwrap();
+ let mut ctx_enc = AesGcmContext::default();
+ let mut ctx_enc = AesGcmEncrypt::new(&mut ctx_enc, &[0u8; 16], b"1").unwrap();
let tag_empty = ctx_enc.finish().unwrap();
// ok: empty string tag - decryption
- init_ctx!(AesGcmDecrypt, ctx_dec, &[0u8; 16], b"1");
- let mut ctx_dec = ctx_dec.unwrap();
+ let mut ctx_dec = AesGcmContext::default();
+ let mut ctx_dec = AesGcmDecrypt::new(&mut ctx_dec, &[0u8; 16], b"1").unwrap();
ctx_dec.finish(&tag_empty).unwrap();
// ok: any single operation
@@ -479,8 +482,8 @@ mod test {
let (key, iv, aad, pt, ct, tag) = v.decoded();
// Test encryption.
- init_ctx!(AesGcmEncrypt, ctx, &key, &iv);
- let mut ctx = ctx.unwrap();
+ let mut ctx = AesGcmContext::default();
+ let mut ctx = AesGcmEncrypt::new(&mut ctx, &key, &iv).unwrap();
if !aad.is_empty() {
ctx.auth(&aad).unwrap();
}
@@ -492,8 +495,8 @@ mod test {
assert_eq!(hex::encode(result), v.tag);
// Test decryption.
- init_ctx!(AesGcmDecrypt, ctx, &key, &iv);
- let mut ctx = ctx.unwrap();
+ let mut ctx = AesGcmContext::default();
+ let mut ctx = AesGcmDecrypt::new(&mut ctx, &key, &iv).unwrap();
if !aad.is_empty() {
ctx.auth(&aad).unwrap();
}
@@ -510,8 +513,8 @@ mod test {
let (key, iv, aad, pt, ct, tag) = v.decoded();
// Test encryption.
- init_ctx!(AesGcmEncrypt, ctx, &key, &iv);
- let mut ctx = ctx.unwrap();
+ let mut ctx = AesGcmContext::default();
+ let mut ctx = AesGcmEncrypt::new(&mut ctx, &key, &iv).unwrap();
if !aad.is_empty() {
ctx.auth(&aad).unwrap();
}
@@ -524,8 +527,8 @@ mod test {
assert_eq!(hex::encode(result), v.tag);
// Test decryption.
- init_ctx!(AesGcmDecrypt, ctx, &key, &iv);
- let mut ctx = ctx.unwrap();
+ let mut ctx = AesGcmContext::default();
+ let mut ctx = AesGcmDecrypt::new(&mut ctx, &key, &iv).unwrap();
if !aad.is_empty() {
ctx.auth(&aad).unwrap();
}
@@ -545,8 +548,8 @@ mod test {
let chunk_len = pt.len() / 3;
let mut buffer = vec![0; pt.len()];
- init_ctx!(AesGcmDecrypt, ctx, &key, &iv);
- let mut ctx = ctx.unwrap();
+ let mut ctx = AesGcmContext::default();
+ let mut ctx = AesGcmDecrypt::new(&mut ctx, &key, &iv).unwrap();
ctx.decrypt(&ct[..chunk_len], &mut buffer[..chunk_len])
.unwrap();
ctx.auth(aad.get(..7).unwrap_or(&[])).unwrap();
@@ -557,8 +560,8 @@ mod test {
ctx.finish(&tag).unwrap();
buffer = vec![0; pt.len()];
- init_ctx!(AesGcmEncrypt, ctx, &key, &iv);
- let mut ctx = ctx.unwrap();
+ let mut ctx = AesGcmContext::default();
+ let mut ctx = AesGcmEncrypt::new(&mut ctx, &key, &iv).unwrap();
ctx.auth(aad.get(..7).unwrap_or(&[])).unwrap();
ctx.encrypt(&pt[..chunk_len], &mut buffer[..chunk_len])
.unwrap();
@@ -577,8 +580,8 @@ mod test {
let chunk_len = pt.len() / 3;
let mut buffer = ct;
- init_ctx!(AesGcmDecrypt, ctx, &key, &iv);
- let mut ctx = ctx.unwrap();
+ let mut ctx = AesGcmContext::default();
+ let mut ctx = AesGcmDecrypt::new(&mut ctx, &key, &iv).unwrap();
ctx.decrypt_in_place(&mut buffer[..chunk_len]).unwrap();
ctx.auth(aad.get(..7).unwrap_or(&[])).unwrap();
ctx.decrypt_in_place(&mut buffer[chunk_len..]).unwrap();
@@ -587,8 +590,8 @@ mod test {
ctx.finish(&tag).unwrap();
let mut buffer = pt;
- init_ctx!(AesGcmEncrypt, ctx, &key, &iv);
- let mut ctx = ctx.unwrap();
+ let mut ctx = AesGcmContext::default();
+ let mut ctx = AesGcmEncrypt::new(&mut ctx, &key, &iv).unwrap();
ctx.auth(aad.get(..7).unwrap_or(&[])).unwrap();
ctx.encrypt_in_place(&mut buffer[..chunk_len]).unwrap();
ctx.auth(aad.get(7..).unwrap_or(&[])).unwrap();
### core/embed/crypto/src/cosi.rs
@@ -4,17 +4,23 @@ use super::{Error, ed25519, ffi};
const MAX_PUBKEYS: usize = 3;
+/// Collective Ed25519 signature with a bitmask of participating public keys.
pub struct Signature {
sigmask: u8,
signature: ed25519::Signature,
}
impl Signature {
+ /// `sigmask` is a bitmask over `public_keys` (bit 0 = first key).
pub fn new(sigmask: u8, signature: ed25519::Signature) -> Self {
Self { sigmask, signature }
}
}
+/// Verify a CoSi signature of `message`.
+///
+/// Combines the public keys selected by `signature.sigmask` and checks that at
+/// least `threshold` of them participated.
pub fn verify(
threshold: u8,
message: &[u8],
@@ -39,7 +45,10 @@ fn select_keys(
let mut selected_keys = Vec::new();
for key in keys {
if sigmask & 1 != 0 {
- if selected_keys.push(*key).is_err() {
+ let result = selected_keys.push(*key);
+ if result.is_err() {
+ // selected_keys is sized to MAX_PUBKEYS.
+ // if the push overflows, means there's too many pubkeys selected.
return Err(Error::InvalidSigmask);
}
}
@@ -54,6 +63,7 @@ fn select_keys(
fn combine_publickeys(keys: &[ed25519::PublicKey]) -> Result<ed25519::PublicKey, Error> {
let mut combined_key = ed25519::PublicKey::default();
+ // SAFETY: ffi
let res = unsafe {
ffi::ed25519_cosi_combine_publickeys(&mut combined_key as *mut _, keys.as_ptr(), keys.len())
};
### core/embed/crypto/src/crc32.rs
@@ -27,6 +27,12 @@ impl Crc32 {
}
}
+impl Default for Crc32 {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
pub fn digest(data: &[u8]) -> [u8; 4] {
Crc32::new().update(data).finalize()
}
### core/embed/crypto/src/ffi.rs
@@ -1,4 +1,5 @@
#![allow(non_camel_case_types)]
#![allow(dead_code)]
+#![allow(non_snake_case)]
include!(concat!(env!("OUT_DIR"), "/crypto.rs"));
### core/embed/crypto/src/hmac.rs
@@ -1,51 +1,80 @@
-use core::pin::Pin;
+use core::ops::DerefMut;
-use zeroize::{Zeroize, ZeroizeOnDrop};
+use rtl::CSlice;
use super::ffi;
-use super::memory::{Memory, init_ctx};
+use super::secret::{HazardGuard, SecretContext, SecretContextLock, ZeroableMemory};
pub const DIGEST_SIZE: usize = ffi::SHA256_DIGEST_LENGTH as usize;
pub type Digest = [u8; DIGEST_SIZE];
-#[derive(Zeroize, ZeroizeOnDrop)]
-pub struct HmacSha256<'a> {
- ctx: Pin<&'a mut Memory<ffi::HMAC_SHA256_CTX>>,
-}
+pub type HmacSha256Ctx = SecretContext<ffi::HMAC_SHA256_CTX>;
-impl<'a> HmacSha256<'a> {
- pub fn new(mut ctx: Pin<&'a mut Memory<ffi::HMAC_SHA256_CTX>>, key: &[u8]) -> Self {
- // initialize the context
- // SAFETY: ffi
- unsafe { ffi::hmac_sha256_Init(ctx.inner(), key.as_ptr(), key.len() as u32) };
- Self { ctx }
- }
+// SAFETY: HMAC_SHA256_CTX is valid when zeroed
+unsafe impl ZeroableMemory for ffi::HMAC_SHA256_CTX {}
- pub fn update(&mut self, data: &[u8]) {
+impl HazardGuard<'_, ffi::HMAC_SHA256_CTX> {
+ /// Initialize the HMAC context with the given key.
+ ///
+ /// Called by [`HmacSha256::new`].
+ fn init(&mut self, key: &[u8]) {
+ let ptr = CSlice::from(key);
// SAFETY: ffi
- unsafe { ffi::hmac_sha256_Update(self.ctx.inner(), data.as_ptr(), data.len() as u32) };
+ // COPY HAZARD: operates on the guarded context in place
+ unsafe { ffi::hmac_sha256_Init(self.hazard_mut(), ptr.ptr(), ptr.len() as u32) };
}
- pub fn memory() -> Memory<ffi::HMAC_SHA256_CTX> {
- Memory::default()
+ /// Update the HMAC context with the given data.
+ fn update(&mut self, data: &[u8]) {
+ let ptr = CSlice::from(data);
+ // SAFETY: ffi
+ // COPY HAZARD: operates on the guarded context in place
+ unsafe { ffi::hmac_sha256_Update(self.hazard_mut(), ptr.ptr(), ptr.len() as u32) };
}
- pub fn finalize_into(mut self, out: &mut Digest) {
+ /// Finalize the HMAC context and return the digest.
+ fn finalize(&mut self) -> Digest {
+ let mut digest = [0u8; DIGEST_SIZE];
// SAFETY: ffi
- unsafe { ffi::hmac_sha256_Final(self.ctx.inner(), out.as_mut_ptr()) };
+ // COPY HAZARD: operates on the guarded context in place
+ unsafe { ffi::hmac_sha256_Final(self.hazard_mut(), digest.as_mut_ptr()) };
+ digest
}
}
-pub fn digest_into(key: &[u8], data: &[u8], out: &mut Digest) {
- init_ctx!(HmacSha256, ctx, key);
- ctx.update(data);
- ctx.finalize_into(out);
+/// HMAC-SHA256 hasher.
+///
+/// A wrapper around an HMAC-SHA256 context that provides a safe interface for
+/// authenticating data.
+pub struct HmacSha256<D: DerefMut<Target = HmacSha256Ctx>>(SecretContextLock<D>);
+
+impl<D: DerefMut<Target = HmacSha256Ctx>> HmacSha256<D> {
+ /// Construct a new HMAC-SHA256 hasher keyed by `key`.
+ pub fn new(ctx: D, key: &[u8]) -> Self {
+ let mut locked_ctx = SecretContextLock::new(ctx);
+ locked_ctx.guarded().init(key);
+ Self(locked_ctx)
+ }
+
+ /// Update the HMAC context with the given data.
+ pub fn update(&mut self, data: &[u8]) {
+ self.0.guarded().update(data);
+ }
+
+ /// Finalize the HMAC context and return the digest.
+ pub fn finalize(mut self) -> Digest {
+ self.0.guarded().finalize()
+ }
}
-pub fn digest(key: &[u8], data: &[u8]) -> Digest {
- let mut out = [0u8; DIGEST_SIZE];
- digest_into(key, data, &mut out);
- out
+impl HmacSha256<&'_ mut HmacSha256Ctx> {
+ /// Calculate the HMAC-SHA256 digest of `data` under `key`.
+ pub fn digest(key: &[u8], data: &[u8]) -> Digest {
+ let mut ctx = HmacSha256Ctx::default();
+ let mut hmac = HmacSha256::new(&mut ctx, key);
+ hmac.update(data);
+ hmac.finalize()
+ }
}
#[cfg(test)]
@@ -96,15 +125,14 @@ mod test {
];
fn hexdigest(key: &[u8], data: &[u8]) -> String {
- hex::encode(digest(key, data))
+ hex::encode(HmacSha256::digest(key, data))
}
#[test]
fn test_empty_ctx() {
- let mut out = [0u8; DIGEST_SIZE];
-
- init_ctx!(HmacSha256, ctx, b"");
- ctx.finalize_into(&mut out);
+ let mut ctx = HmacSha256Ctx::default();
+ let hmac = HmacSha256::new(&mut ctx, b"");
+ let out = hmac.finalize();
let out_hex = hex::encode(out);
assert_eq!(out_hex, HMAC_SHA256_EMPTY);
@@ -123,24 +151,25 @@ mod test {
// case 3
let key =
b"\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa";
- init_ctx!(HmacSha256, ctx, key);
+ let mut ctx = HmacSha256Ctx::default();
+ let mut hmac = HmacSha256::new(&mut ctx, key);
for _ in 0..50 {
- ctx.update(b"\xdd");
+ hmac.update(b"\xdd");
}
- let mut out = [0u8; DIGEST_SIZE];
- ctx.finalize_into(&mut out);
+ let out = hmac.finalize();
assert_eq!(
hex::encode(out),
"773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe"
);
// case 4
let key = b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19";
- init_ctx!(HmacSha256, ctx, key);
+ let mut ctx = HmacSha256Ctx::default();
+ let mut hmac = HmacSha256::new(&mut ctx, key);
for _ in 0..50 {
- ctx.update(b"\xcd");
+ hmac.update(b"\xcd");
}
- ctx.finalize_into(&mut out);
+ let out = hmac.finalize();
assert_eq!(
hex::encode(out),
"82558a389a443c0ea4cc819899f2083a85f0faa3e578f8077a2e3ff46729665b"
### core/embed/crypto/src/lib.rs
@@ -10,24 +10,25 @@ pub mod curve25519;
pub mod ed25519;
mod ffi;
pub mod hmac;
-pub mod memory;
pub mod merkle;
+pub mod secret;
pub mod sha256;
pub mod sha512;
+/// Error returned by cryptographic operations in this crate.
#[cfg_attr(feature = "test", derive(core::fmt::Debug))]
pub enum Error {
- // Signature verification failed
+ /// Signature verification failed.
SignatureVerificationFailed,
- // Provided value is not a valid public key / signature / etc.
+ /// Provided value is not a valid public key, signature, or similar.
InvalidEncoding,
- // Provided parameters are not accepted (e.g., signature threshold out of bounds)
+ /// Parameters are not accepted (e.g., signature threshold out of bounds).
InvalidParams,
- // State precondition check failed (possibly raised by C implementation)
+ /// State precondition check failed (can be raised by the C implementation).
InvalidContext,
- // Authentication failed (e.g. AEAD tag mismatch)
+ /// Authentication failed (e.g. AEAD tag mismatch).
AuthenticationFailed,
- // Invalid sigmask
+ /// CoSi sigmask selects more public keys than supported.
InvalidSigmask,
}
### core/embed/crypto/src/memory.rs
@@ -1,81 +0,0 @@
-use core::marker::PhantomPinned;
-use core::mem::MaybeUninit;
-use core::pin::Pin;
-
-use zeroize::{Zeroize, zeroize_flat_type};
-
-/// Wrapper for a memory used as a context by C functions. Its purpose is to be
-/// !Unpin, thus prevent moves when accessed through a Pin. We want to avoid
-/// moves as they can leave cryptographic data in memory.
-///
-/// T needs to be a plain struct that is valid when zeroed.
-pub struct Memory<T> {
- inner: T,
- _phantom: PhantomPinned,
-}
-
-impl<T> Default for Memory<T> {
- fn default() -> Self {
- // SAFETY: a zeroed block of memory is valid for C functions
- let inner = unsafe { MaybeUninit::<T>::zeroed().assume_init() };
- Self {
- inner,
- _phantom: PhantomPinned,
- }
- }
-}
-
-impl<T> Zeroize for Memory<T> {
- fn zeroize(&mut self) {
- // SAFETY:
- // - contains no references
- // - plain struct with not Drop impls
- // - only called in Drop impl
- // - zeroed block of memory is valid
- unsafe { zeroize_flat_type(&mut self.inner as *mut T) };
- }
-}
-
-impl<T> Memory<T> {
- // SAFETY:
- // The caller must ensure that the return value is handled according to the
- // contract of `Pin::map_unchecked_mut` and `Pin::get_unchecked_mut`.
- // Notably passing the pointer to a C function should be fine since the notion
- // of moving doesn't exist there and the entire point of this pinning is not
- // to leak more data than the C implementation.
- pub unsafe fn inner(self: &mut Pin<&mut Self>) -> *mut T {
- unsafe {
- self.as_mut()
- .map_unchecked_mut(|m| &mut m.inner)
- .get_unchecked_mut()
- }
- }
-}
-
-impl<T> Zeroize for Pin<&mut Memory<T>> {
- fn zeroize(&mut self) {
- // SAFETY: `Memory::zeroize` does not do any moving
- unsafe {
- self.as_mut().get_unchecked_mut().zeroize();
- }
- }
-}
-
-/// Initializes backing memory on the stack and passes it to a constructor.
-/// The macro is basically a specialized version of `core::pin::pin!` for use
-/// with Memory<T>.
-#[macro_export]
-macro_rules! init_ctx {
- ($type:ty, $name:ident $(, $arg:expr)*) => {
- // assign the backing memory to $name...
- let mut $name = <$type>::memory();
- // ... then make it inaccessible by overwriting the binding, and pin it
- // SAFETY: The value is pinned: it is the local above which cannot be named outside this macro.
- #[allow(unused_mut)]
- let mut $name = unsafe {
- <$type>::new(core::pin::Pin::new_unchecked(&mut $name), $($arg),*)
- };
- };
-}
-
-pub use init_ctx;
### core/embed/crypto/src/merkle.rs
@@ -1,17 +1,16 @@
-use super::memory::init_ctx;
use super::sha256;
/// Calculate a Merkle root based on a leaf element and a proof of inclusion.
///
/// Expects the Merkle tree format specified in `external-definitions.md`.
pub fn merkle_root(elem: &[u8], proof: &[sha256::Digest]) -> sha256::Digest {
- let mut out = sha256::Digest::default();
+ let mut ctx = sha256::Sha256Ctx::default();
// hash the leaf element
- init_ctx!(sha256::Sha256, ctx);
- ctx.update(&[0x00]);
- ctx.update(elem);
- ctx.finalize_into(&mut out);
+ let mut sha = sha256::Sha256::new(&mut ctx);
+ sha.update(&[0x00]);
+ sha.update(elem);
+ let mut out = sha.finalize();
for proof_elem in proof {
// hash together the current hash and the proof element
@@ -20,11 +19,11 @@ pub fn merkle_root(elem: &[u8], proof: &[sha256::Digest]) -> sha256::Digest {
} else {
(proof_elem, &out)
};
- init_ctx!(sha256::Sha256, ctx);
- ctx.update(&[0x01]);
- ctx.update(min);
- ctx.update(max);
- ctx.finalize_into(&mut out);
+ let mut sha = sha256::Sha256::new(&mut ctx);
+ sha.update(&[0x01]);
+ sha.update(min);
+ sha.update(max);
+ out = sha.finalize();
}
out
### core/embed/crypto/src/secret.rs
@@ -0,0 +1,243 @@
+use core::hint::black_box;
+use core::marker::PhantomPinned;
+use core::mem::MaybeUninit;
+use core::ops::{Deref, DerefMut};
+
+use zeroize::{Zeroize, ZeroizeOnDrop, zeroize_flat_type};
+
+/// Marker trait for C types that can be safely zeroed.
+///
+/// # Safety
+///
+/// Must only be implemented for types that satisfy `zeroize_flat_type`'s safety
+/// guarantees:
+///
+/// * all-zero memory is a valid value
+/// * the type is flat, that is, does not hold Rust references nor dynamically
+/// sized data
+/// * values inside it do not have Drop impls
+///
+/// These are all generally true for FFI C structs.
+pub unsafe trait ZeroableMemory {}
+
+/// Zeroizing wrapper around sensitive memory contexts.
+///
+/// Implements zeroize-on-drop behavior, provides a Default all-zero value, and
+/// it is !Unpin, so that it can be correctly used inside a Pin.
+///
+/// It also gates all access to the sensitive context behind "hazardous" calls.
+///
+/// T needs to be a plain struct that is valid when zeroed.
+///
+/// This wrapper type should be used together with [`SecretContextLock`]:
+/// allocate memory of type `SecretContext<T>`, then pass it on to its users
+/// wrapped in a `SecretContextLock`.
+///
+/// # Copy hazard
+///
+/// `SecretContext` is designed to wrap cryptographic contexts and other
+/// sensitive memory. When operating on such values naively, Rust is allowed by
+/// design to make copies of the sensitive data as it moves around memory.
+///
+/// This wrapper, by itself, does not (and cannot) prevent this behavior. Its
+/// role is to make it visible to callers, via using the only accessor
+/// [`SecretContext::hazard_mut`].
+///
+/// See [`SecretContextLock`] for proper usage in a struct.
+#[repr(transparent)]
+pub struct SecretContext<T: ZeroableMemory> {
+ inner: T,
+ _phantom: PhantomPinned,
+}
+
+impl<T: ZeroableMemory> Default for SecretContext<T> {
+ fn default() -> Self {
+ // SAFETY: T is ZeroableMemory, so a zeroed block of memory is valid
+ let inner = unsafe { MaybeUninit::<T>::zeroed().assume_init() };
+ Self {
+ inner,
+ _phantom: PhantomPinned,
+ }
+ }
+}
+
+impl<T: ZeroableMemory> Drop for SecretContext<T> {
+ fn drop(&mut self) {
+ self.zeroize();
+ }
+}
+
+impl<T: ZeroableMemory> ZeroizeOnDrop for SecretContext<T> {}
+
+impl<T: ZeroableMemory> SecretContext<T> {
+ /// Get a mutable reference to the wrapped value.
+ ///
+ /// # Copy hazard
+ ///
+ /// You are responsible for not copying out the value (either manually or
+ /// via something like `mem::replace`).
+ pub fn hazard_mut(&mut self) -> &mut T {
+ black_box(&mut self.inner)
+ }
+
+ /// Zeroize the wrapped value.
+ pub fn zeroize(&mut self) {
+ // SAFETY:
+ // - zeroed block of memory is valid
+ unsafe { zeroize_flat_type(&mut self.inner as *mut T) };
+ }
+}
+
+impl<T: ZeroableMemory> Zeroize for SecretContext<T> {
+ /// Zeroize the wrapped value.
+ fn zeroize(&mut self) {
+ Self::zeroize(self);
+ }
+}
+
+/// Exclusive lock over a [`SecretContext`].
+///
+/// Holds a `DerefMut` to a [`SecretContext`] (typically `&mut
+/// SecretContext<T>`) so that no other code can move or copy the context while
+/// the lock is alive. On drop, the pointed-to context is zeroized — even when
+/// dropping the lock does not drop the context itself, which is the case for
+/// `&mut`.
+///
+/// Wrap a hasher or similar type around `SecretContextLock` so that:
+/// * sensitive state is not copied by the wrapper
+/// * nobody else can observe the context while it is in use
+/// * the context is zeroized as soon as exclusive access is released
+///
+/// # Copy hazard
+///
+/// Protects from hazardous calls which rely on the caller not copying out the
+/// sensitive context.
+///
+/// **Important**: MUST NOT be instantiated with `D` a container type that
+/// returns an internal `&mut` reference into itself. Doing so would transfer
+/// ownership of the secret bytes into the `SecretContextLock` struct, exposing
+/// it to copy hazard.
+///
+/// (Future development note: a custom marker trait `PointerToSecret` might be
+/// more appropriate than a generic `DerefMut`.)
+///
+/// You are responsible for not copying out the value obtained through
+/// [`SecretContextLock::hazard_mut`]. Prefer [`SecretContextLock::guarded`]
+/// and implementing the operation on [`HazardGuard`], which moves that
+/// responsibility from the call site to the operation itself.
+///
+/// # Example
+///
+/// ```ignore
+/// struct Hasher<D: DerefMut<Target = SecretContext<Ctx>>>(SecretContextLock<D>);
+///
+/// impl<D: DerefMut<Target = SecretContext<Ctx>>> Hasher<D> {
+/// fn new(ctx: D) -> Self {
+/// Self(SecretContextLock::new(ctx))
+/// }
+/// }
+/// ```
+#[repr(transparent)]
+pub struct SecretContextLock<D>(D)
+where
+ D: DerefMut,
+ <D as Deref>::Target: Zeroize;
+
+impl<D, T> SecretContextLock<D>
+where
+ D: DerefMut<Target = SecretContext<T>>,
+ T: ZeroableMemory,
+{
+ /// Lock `ctx` for exclusive use until this value is dropped.
+ pub fn new(ctx: D) -> Self {
+ // using black_box at construction time (only) should ensure that a
+ // pointer is stored, preventing the compiler from doing something ugly
+ // like optimizing out the whole wrapper struct
+ // (UNPROVEN)
+ Self(black_box(ctx))
+ }
+
+ /// Get a mutable reference to the wrapped value.
+ ///
+ /// # Copy hazard
+ ///
+ /// You are responsible for not copying out the value (either manually or
+ /// via something like `mem::replace`).
+ pub fn hazard_mut(&mut self) -> &mut T {
+ self.0.hazard_mut()
+ }
+
+ /// Get a [`HazardGuard`] for the enclosed `SecretContext`.
+ ///
+ /// Operations on the sensitive context are implemented as methods of
+ /// `HazardGuard`, so that they can only ever run on a locked context.
+ pub fn guarded(&mut self) -> HazardGuard<'_, T> {
+ HazardGuard(&mut self.0)
+ }
+}
+
+/// Witness of exclusive in-place access to a [`SecretContext`].
+///
+/// A `HazardGuard` can be constructed in two ways:
+///
+/// * hazard-free, via [`SecretContextLock::guarded`].
+/// * hazardously via [`HazardGuard::hazard_new`].
+///
+/// Operations on a sensitive context -- typically FFI calls taking a pointer to
+/// it -- should be implemented as methods of `HazardGuard`. Such an operation
+/// then cannot be invoked on an unprotected context, and its callers do not
+/// need to uphold anything by hand.
+///
+/// # Copy hazard
+///
+/// A method of `HazardGuard` must operate on the context in place. It is the
+/// responsibility of the implementation not to copy the context out (either
+/// manually or via something like `mem::replace`).
+///
+/// The method [`HazardGuard::hazard_new`] intentionally overrides the copy
+/// hazard protection, exposing the guarded operations to hazard.
+#[repr(transparent)]
+pub struct HazardGuard<'a, T: ZeroableMemory>(&'a mut SecretContext<T>);
+
+impl<'a, T: ZeroableMemory> HazardGuard<'a, T> {
+ /// Construct a new `HazardGuard` from a mutable reference to a
+ /// `SecretContext`.
+ ///
+ /// # Copy hazard
+ ///
+ /// Constructing a `HazardGuard` this way bypasses hazard protection
+ /// guarantees. This method is only provided as an escape hatch for contexts
+ /// where a [`SecretContextLock`] cannot be used.
+ pub fn hazard_new(ctx: &'a mut SecretContext<T>) -> Self {
+ Self(ctx)
+ }
+
+ /// Get a mutable reference to the guarded value, for passing to FFI.
+ ///
+ /// # Copy hazard
+ ///
+ /// You are responsible for not copying out the value (either manually or
+ /// via something like `mem::replace`).
+ pub fn hazard_mut(&mut self) -> &mut T {
+ self.0.hazard_mut()
+ }
+}
+
+impl<D> Drop for SecretContextLock<D>
+where
+ D: DerefMut,
+ <D as Deref>::Target: Zeroize,
+{
+ fn drop(&mut self) {
+ // Zeroize through the DerefMut so that `&mut SecretContext` is cleared
+ // when exclusive access ends, not only when the context itself is dropped.
+ self.0.zeroize();
+ }
+}
+
+impl<D> ZeroizeOnDrop for SecretContextLock<D>
+where
+ D: DerefMut,
+ <D as Deref>::Target: Zeroize,
+{
+}
### core/embed/crypto/src/sha256.rs
@@ -1,87 +1,96 @@
-use core::mem::MaybeUninit;
-use core::pin::Pin;
+use core::ops::DerefMut;
-use zeroize::{Zeroize, ZeroizeOnDrop};
+use rtl::CSlice;
use super::ffi;
-use super::memory::{Memory, init_ctx};
+use super::secret::{HazardGuard, SecretContext, SecretContextLock, ZeroableMemory};
pub const BLOCK_SIZE: usize = ffi::SHA256_BLOCK_LENGTH as usize;
pub const DIGEST_SIZE: usize = ffi::SHA256_DIGEST_LENGTH as usize;
pub type Digest = [u8; DIGEST_SIZE];
-#[derive(Zeroize, ZeroizeOnDrop)]
-pub struct Sha256<'a> {
- ctx: Pin<&'a mut Memory<ffi::SHA256_CTX>>,
-}
-
-impl<'a> Sha256<'a> {
- pub fn new(mut ctx: Pin<&'a mut Memory<ffi::SHA256_CTX>>) -> Self {
- // initialize the context
- // SAFETY: safe with whatever finds itself as memory contents
- unsafe { ffi::sha256_Init(ctx.inner()) };
- Self { ctx }
- }
-
- pub fn update(&mut self, data: &[u8]) {
- // SAFETY: safe
- unsafe { ffi::sha256_Update(self.ctx.inner(), data.as_ptr(), data.len()) };
- }
+pub type Sha256Ctx = SecretContext<ffi::SHA256_CTX>;
- pub fn memory() -> Memory<ffi::SHA256_CTX> {
- Memory::default()
- }
+// SAFETY: SHA256_CTX is valid when zeroed
+unsafe impl ZeroableMemory for ffi::SHA256_CTX {}
- pub fn finalize_into(mut self, out: &mut Digest) {
- // SAFETY: safe
- unsafe { ffi::sha256_Final(self.ctx.inner(), out.as_mut_ptr()) };
+impl ffi::SHA256_CTX {
+ /// Initialize the SHA256 context.
+ ///
+ /// Called by [`Sha256::new`]. Call again when reusing the context after
+ /// [`HazardGuard::finalize`].
+ pub fn init(&mut self) {
+ // SAFETY: ffi
+ unsafe { ffi::sha256_Init(self) };
}
}
-pub fn digest_into(data: &[u8], out: &mut Digest) {
- init_ctx!(Sha256, 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
-}
-
-// Unpinned variant for use with noise-protocol which does not guarantee
-// pinning. If possible please use [`Sha256`] above.
-#[derive(Clone)]
-pub struct NoPinSha256 {
- ctx: ffi::SHA256_CTX,
-}
+impl HazardGuard<'_, ffi::SHA256_CTX> {
+ /// Update the SHA256 context with the given data.
+ pub fn update(&mut self, data: &[u8]) {
+ let ptr = CSlice::from(data);
+ // SAFETY: ffi
+ // COPY HAZARD: operates on the guarded context in place
+ unsafe { ffi::sha256_Update(self.hazard_mut(), ptr.ptr(), ptr.len()) };
+ }
-impl Drop for NoPinSha256 {
- fn drop(&mut self) {
- // C implementation zeroes the state
+ /// Finalize the SHA256 context and return the digest.
+ ///
+ /// After calling this method, the context is in a zeroized state. Before
+ /// reusing it, the caller must call [`ffi::SHA256_CTX::init`] to
+ /// reinitialize it.
+ pub fn finalize(&mut self) -> Digest {
+ let mut digest = [0u8; DIGEST_SIZE];
// SAFETY: ffi
- unsafe { ffi::sha256_Final(&mut self.ctx as *mut _, core::ptr::null_mut()) };
+ // COPY HAZARD: operates on the guarded context in place
+ unsafe { ffi::sha256_Final(self.hazard_mut(), digest.as_mut_ptr()) };
+ digest
}
}
-impl Default for NoPinSha256 {
- fn default() -> Self {
- let mut ctx = unsafe { MaybeUninit::<ffi::SHA256_CTX>::zeroed().assume_init() };
- unsafe { ffi::sha256_Init(&mut ctx) };
- Self { ctx }
+/// SHA256 hasher.
+///
+/// A wrapper around a SHA256 context that provides a safe interface for hashing
+/// data.
+///
+/// # Example
+///
+/// ```rust
+/// use crypto::sha256::{Sha256, Sha256Ctx};
+///
+/// let mut ctx = Sha256Ctx::default();
+/// let mut sha = Sha256::new(&mut ctx);
+/// sha.update(b"hello");
+/// sha.finalize();
+/// ```
+pub struct Sha256<D: DerefMut<Target = Sha256Ctx>>(SecretContextLock<D>);
+
+impl<D: DerefMut<Target = Sha256Ctx>> Sha256<D> {
+ /// Construct a new SHA256 hasher.
+ pub fn new(mut ctx: D) -> Self {
+ // COPY HAZARD: init is a public operation
+ ctx.hazard_mut().init();
+ Self(SecretContextLock::new(ctx))
}
-}
-impl NoPinSha256 {
+ /// Update the SHA256 context with the given data.
pub fn update(&mut self, data: &[u8]) {
- // SAFETY: ffi
- unsafe { ffi::sha256_Update(&mut self.ctx as *mut _, data.as_ptr(), data.len()) };
+ self.0.guarded().update(data);
}
- pub fn finalize_into(mut self, out: &mut Digest) {
- // SAFETY: ffi
- unsafe { ffi::sha256_Final(&mut self.ctx as *mut _, out.as_mut_ptr()) };
+ /// Finalize the SHA256 context and return the digest.
+ pub fn finalize(mut self) -> Digest {
+ self.0.guarded().finalize()
+ }
+}
+
+impl Sha256<&'_ mut Sha256Ctx> {
+ /// Calculate the SHA256 digest of the given data.
+ pub fn digest(data: &[u8]) -> Digest {
+ let mut ctx = SecretContext::default();
+ let mut sha = Sha256::new(&mut ctx);
+ sha.update(data);
+ sha.finalize()
}
}
@@ -99,15 +108,14 @@ mod test {
];
fn hexdigest(data: &[u8]) -> String {
- hex::encode(digest(data))
+ hex::encode(Sha256::digest(data))
}
#[test]
fn test_empty_ctx() {
- let mut out = Digest::default();
-
- init_ctx!(Sha256, ctx);
- ctx.finalize_into(&mut out);
+ let mut ctx = Sha256Ctx::default();
+ let sha = Sha256::new(&mut ctx);
+ let out = sha.finalize();
let out_hex = hex::encode(out);
assert_eq!(out_hex, SHA256_EMPTY.to_string());
### core/embed/crypto/src/sha512.rs
@@ -1,53 +1,88 @@
-use core::pin::Pin;
+use core::ops::DerefMut;
-use zeroize::{Zeroize, ZeroizeOnDrop};
+use rtl::CSlice;
use super::ffi;
-use super::memory::{Memory, init_ctx};
+use super::secret::{HazardGuard, SecretContext, SecretContextLock, ZeroableMemory};
pub const BLOCK_SIZE: usize = ffi::SHA512_BLOCK_LENGTH as usize;
pub const DIGEST_SIZE: usize = ffi::SHA512_DIGEST_LENGTH as usize;
pub type Digest = [u8; DIGEST_SIZE];
-#[derive(Zeroize, ZeroizeOnDrop)]
-pub struct Sha512<'a> {
- ctx: Pin<&'a mut Memory<ffi::SHA512_CTX>>,
-}
+pub type Sha512Ctx = SecretContext<ffi::SHA512_CTX>;
-impl<'a> Sha512<'a> {
- pub fn new(ctx: Pin<&'a mut Memory<ffi::SHA512_CTX>>) -> Self {
- // initialize the context
- let mut res = Self { ctx };
- // SAFETY: safe with whatever finds itself as memory contents
- unsafe { ffi::sha512_Init(res.ctx.inner()) };
- res
- }
+// SAFETY: SHA512_CTX is valid when zeroed
+unsafe impl ZeroableMemory for ffi::SHA512_CTX {}
- pub fn update(&mut self, data: &[u8]) {
+impl ffi::SHA512_CTX {
+ /// Initialize the SHA512 context.
+ ///
+ /// Called by [`Sha512::new`]. Call again when reusing the context after
+ /// [`HazardGuard::finalize`].
+ pub fn init(&mut self) {
// SAFETY: ffi
- unsafe { ffi::sha512_Update(self.ctx.inner(), data.as_ptr(), data.len()) };
+ unsafe { ffi::sha512_Init(self) };
}
+}
- pub fn memory() -> Memory<ffi::SHA512_CTX> {
- Memory::default()
+impl HazardGuard<'_, ffi::SHA512_CTX> {
+ /// Update the SHA512 context with the given data.
+ pub fn update(&mut self, data: &[u8]) {
+ let data_slice = CSlice::from(data);
+ // SAFETY: ffi
+ // COPY HAZARD: operates on the guarded context in place
+ unsafe { ffi::sha512_Update(self.hazard_mut(), data_slice.ptr(), data_slice.len()) };
}
- pub fn finalize_into(mut self, out: &mut Digest) {
+ /// Finalize the SHA512 context and return the digest.
+ ///
+ /// After calling this method, the context is in a zeroized state. Before
+ /// reusing it, the caller must call [`ffi::SHA512_CTX::init`] to
+ /// reinitialize it.
+ pub fn finalize(&mut self) -> Digest {
+ let mut digest = [0u8; DIGEST_SIZE];
// SAFETY: ffi
- unsafe { ffi::sha512_Final(self.ctx.inner(), out.as_mut_ptr()) };
+ // COPY HAZARD: operates on the guarded context in place
+ unsafe { ffi::sha512_Final(self.hazard_mut(), digest.as_mut_ptr()) };
+ digest
}
}
-pub fn digest_into(data: &[u8], out: &mut Digest) {
- init_ctx!(Sha512, ctx);
- ctx.update(data);
- ctx.finalize_into(out);
+/// SHA512 hasher.
+///
+/// A wrapper around a SHA512 context that provides a safe interface for hashing
+/// data.
+pub struct Sha512<D: DerefMut<Target = Sha512Ctx>>(SecretContextLock<D>);
+
+impl<D: DerefMut<Target = Sha512Ctx>> Sha512<D> {
+ /// Construct a new SHA512 hasher.
+ pub fn new(mut ctx: D) -> Self {
+ // COPY HAZARD: init is a public operation
+ ctx.hazard_mut().init();
+ Self(SecretContextLock::new(ctx))
+ }
+
+ /// Update the SHA512 context with the given data.
+ pub fn update(&mut self, data: &[u8]) {
+ // COPY HAZARD: neither hazard call exfiltrates data
+ self.0.guarded().update(data);
+ }
+
+ /// Finalize the SHA512 context and return the digest.
+ pub fn finalize(mut self) -> Digest {
+ // COPY HAZARD: neither hazard call exfiltrates data
+ self.0.guarded().finalize()
+ }
}
-pub fn digest(data: &[u8]) -> Digest {
- let mut out = [0u8; DIGEST_SIZE];
- digest_into(data, &mut out);
- out
+impl Sha512<&'_ mut Sha512Ctx> {
+ /// Calculate the SHA512 digest of the given data.
+ pub fn digest(data: &[u8]) -> Digest {
+ let mut ctx = Sha512Ctx::default();
+ let mut sha = Sha512::new(&mut ctx);
+ sha.update(data);
+ sha.finalize()
+ }
}
#[cfg(test)]
@@ -72,15 +107,14 @@ mod test {
];
fn hexdigest(data: &[u8]) -> String {
- hex::encode(digest(data))
+ hex::encode(Sha512::digest(data))
}
#[test]
fn test_empty_ctx() {
- let mut out = [0u8; DIGEST_SIZE];
-
- init_ctx!(Sha512, ctx);
- ctx.finalize_into(&mut out);
+ let mut ctx = Sha512Ctx::default();
+ let sha = Sha512::new(&mut ctx);
+ let out = sha.finalize();
let out_hex = hex::encode(out);
assert_eq!(out_hex, SHA512_EMPTY);
### core/embed/rust/src/thp/crypto.rs
@@ -1,4 +1,4 @@
-use crypto::memory::init_ctx;
+use crypto::secret::HazardGuard;
use crypto::{aesgcm, curve25519, sha256};
use trezor_thp::channel::{Backend, Cipher, Hash, U8Array, DH};
use zeroize::{Zeroize, Zeroizing};
@@ -97,8 +97,12 @@ impl Cipher for TrezorCryptoAesGcm {
let (in_out, tag_out) = out.split_at_mut(plaintext.len());
in_out.copy_from_slice(plaintext);
- init_ctx!(aesgcm::AesGcmEncrypt, ctx, key.as_slice(), &full_nonce);
- let mut ctx = unwrap!(ctx);
+ let mut ctx = aesgcm::AesGcmContext::default();
+ let mut ctx = unwrap!(aesgcm::AesGcmEncrypt::new(
+ &mut ctx,
+ key.as_slice(),
+ &full_nonce
+ ));
unwrap!(ctx.encrypt_in_place(in_out));
unwrap!(ctx.auth(ad));
let tag = unwrap!(ctx.finish());
@@ -120,8 +124,12 @@ impl Cipher for TrezorCryptoAesGcm {
let (in_out, tag_out) =
in_out[..plaintext_len + aesgcm::TAG_SIZE].split_at_mut(plaintext_len);
- init_ctx!(aesgcm::AesGcmEncrypt, ctx, key.as_slice(), &full_nonce);
- let mut ctx = unwrap!(ctx);
+ let mut ctx = aesgcm::AesGcmContext::default();
+ let mut ctx = unwrap!(aesgcm::AesGcmEncrypt::new(
+ &mut ctx,
+ key.as_slice(),
+ &full_nonce
+ ));
unwrap!(ctx.encrypt_in_place(in_out));
unwrap!(ctx.auth(ad));
let tag = unwrap!(ctx.finish());
@@ -143,8 +151,12 @@ impl Cipher for TrezorCryptoAesGcm {
let (ciphertext, tag) = unwrap!(ciphertext.split_last_chunk::<{ aesgcm::TAG_SIZE }>());
out.copy_from_slice(ciphertext);
- init_ctx!(aesgcm::AesGcmDecrypt, ctx, key.as_slice(), &full_nonce);
- let mut ctx = unwrap!(ctx);
+ let mut ctx = aesgcm::AesGcmContext::default();
+ let mut ctx = unwrap!(aesgcm::AesGcmDecrypt::new(
+ &mut ctx,
+ key.as_slice(),
+ &full_nonce
+ ));
unwrap!(ctx.decrypt_in_place(out));
unwrap!(ctx.auth(ad));
ctx.finish(tag).map_err(|_| out.zeroize())?;
@@ -166,8 +178,12 @@ impl Cipher for TrezorCryptoAesGcm {
let in_out = &mut in_out[..ciphertext_len];
let (in_out, tag) = unwrap!(in_out.split_last_chunk_mut::<{ aesgcm::TAG_SIZE }>());
- init_ctx!(aesgcm::AesGcmDecrypt, ctx, key.as_slice(), &full_nonce);
- let mut ctx = unwrap!(ctx);
+ let mut ctx = aesgcm::AesGcmContext::default();
+ let mut ctx = unwrap!(aesgcm::AesGcmDecrypt::new(
+ &mut ctx,
+ key.as_slice(),
+ &full_nonce
+ ));
unwrap!(ctx.decrypt_in_place(in_out));
unwrap!(ctx.auth(ad));
ctx.finish(tag).map_err(|_| in_out.zeroize())?;
@@ -176,8 +192,7 @@ impl Cipher for TrezorCryptoAesGcm {
}
}
-#[derive(Default)]
-pub struct TrezorCryptoSha256(sha256::NoPinSha256);
+pub struct TrezorCryptoSha256(sha256::Sha256Ctx);
impl Hash for TrezorCryptoSha256 {
fn name() -> &'static str {
@@ -188,16 +203,33 @@ impl Hash for TrezorCryptoSha256 {
type Output = Sensitive<sha256::Digest>;
fn input(&mut self, data: &[u8]) {
- self.0.update(data);
+ // COPY HAZARD: Hazardous!
+ //
+ // This struct breaks the assumption that the hasher's inner state
+ // cannot be copied around. We have to live with that here, because we
+ // can't put the storage on the heap, so it needs to be owned, which
+ // prevents us from creating a safe interface.
+ let mut guard = HazardGuard::hazard_new(&mut self.0);
+ guard.update(data);
}
fn result(&mut self) -> Self::Output {
- let mut digest = sha256::Digest::default();
- self.0.clone().finalize_into(&mut digest);
+ // COPY HAZARD: Hazardous! (see `input()` above for details)
+ let mut guard = HazardGuard::hazard_new(&mut self.0);
+ let digest = guard.finalize();
Self::Output::from_slice(&digest)
}
}
+impl Default for TrezorCryptoSha256 {
+ fn default() -> Self {
+ let mut ctx = sha256::Sha256Ctx::default();
+ // COPY HAZARD: init is a public operation
+ ctx.hazard_mut().init();
+ Self(ctx)
+ }
+}
+
pub struct TrezorCrypto;
impl Backend for TrezorCrypto {
### core/embed/rust/src/translations/blob.rs
@@ -284,7 +284,7 @@ impl<'a> Translations<'a> {
let payload_bytes = payload_reader.rest();
- let payload_digest = sha256::digest(payload_bytes);
+ let payload_digest = sha256::Sha256::digest(payload_bytes);
if payload_digest != header.data_hash {
return Err(INVALID_TRANSLATIONS_BLOB);
}Why this scored 20/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.