refactor(core/rust): trezor-crypto backend glue for noise-protocol
What changed, and why it matters
This commit refactors the cryptographic glue code that connects Trezor's embedded firmware to a Rust-based Noise protocol implementation used for secure device communication. The most notable security-relevant change is the addition of a constant-time comparison function for checking AES-GCM authentication tags, which helps prevent timing-based attacks that could leak information about the tag. The commit also splits AES-GCM into separate encrypt and decrypt types, adds a non-pinned SHA-256 wrapper, and introduces wrappers for Curve25519 and AES-GCM to satisfy the Noise protocol backend interface. There is no explicit vendor statement that this fixes a security vulnerability, and no independent researcher is credited.
Review the `consteq` implementation for correctness on all target architectures, ensure `black_box` is sufficient to prevent compiler optimization of the timing-independent comparison, and verify that the new `AesGcmDecrypt::finish` tag comparison is used consistently across all decryption paths. Continue monitoring the THP/Noise protocol integration for further security-relevant commits.
Security signals we found
Added constant-time tag comparison for AES-GCM to mitigate timing side-channels
Split combined AES-GCM encrypt/decrypt type into separate types to enforce correct state machine usage
Added zeroizing wrappers for sensitive key material used by Noise protocol backend
Added non-pinned SHA-256 wrapper compatible with external noise-protocol crate
Exposed SHA-256/SHA-512 block length constants for use by the Noise protocol implementation
Evidence from the diff
The commit refactors the trezor-crypto backend for the trezor-noise-protocol / trezor-thp crates. Key changes: (1) adds a constant-time bytestring comparison consteq using core::hint::black_box and uses it in AesGcmDecrypt::finish to compare computed vs expected GCM tags; (2) splits the previous AesGcm type into AesGcmEncrypt and AesGcmDecrypt with shared AesGcmInner state machine, preventing mixing of encrypt/decrypt operations; (3) adds NoPinSha256 for use with noise-protocol which does not guarantee pinning; (4) exposes SHA256_BLOCK_LENGTH and SHA512_BLOCK_LENGTH in build.rs bindings; (5) adds zeroize dependency under the thp feature; (6) implements Backend, DH, Cipher, and Hash traits in core/embed/rust/src/thp/crypto.rs backed by trezor-crypto AES-GCM, Curve25519, and SHA-256. The commit is tagged [no changelog].
Changed components
core/embed/rust/src/crypto/aesgcm.rscore/embed/rust/src/crypto/sha256.rscore/embed/rust/src/crypto/sha512.rscore/embed/rust/src/crypto/curve25519.rscore/embed/rust/src/crypto/mod.rscore/embed/rust/src/thp/crypto.rscore/embed/rust/Cargo.tomlcore/embed/rust/build.rscore/embed/Cargo.lockInspect captured patch +553 / −108
diff --git a/core/embed/Cargo.lock b/core/embed/Cargo.lock
index b0bc9688..a465a7dc 100644
--- a/core/embed/Cargo.lock
+++ b/core/embed/Cargo.lock
@@ -390,6 +390,7 @@ dependencies = [
"stable_deref_trait",
"ufmt",
"ufmt-write",
+ "zeroize",
]
[[package]]
@@ -929,6 +930,24 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
+[[package]]
+name = "trezor-noise-protocol"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d23e260001c4bb916c2710f5c80af007f4a760bce38302c2176dbb2053f051a5"
+dependencies = [
+ "heapless",
+]
+
+[[package]]
+name = "trezor-thp"
+version = "0.1.0"
+dependencies = [
+ "heapless",
+ "log",
+ "trezor-noise-protocol",
+]
+
[[package]]
name = "trezor-tjpgdec"
version = "0.1.0"
@@ -955,6 +974,7 @@ dependencies = [
"serde_json",
"spin",
"static-alloc",
+ "trezor-thp",
"trezor-tjpgdec",
"ufmt",
"unsize",
diff --git a/core/embed/rust/Cargo.toml b/core/embed/rust/Cargo.toml
index a4011e0e..aa46ad64 100644
--- a/core/embed/rust/Cargo.toml
+++ b/core/embed/rust/Cargo.toml
@@ -63,7 +63,7 @@ translations = ["crypto"]
secmon_layout = []
dbg_console = []
app_loading = []
-thp = ["crypto", "dep:trezor-thp"]
+thp = ["crypto", "dep:trezor-thp", "dep:zeroize"]
test = [
"backlight",
"button",
diff --git a/core/embed/rust/build.rs b/core/embed/rust/build.rs
index 42d35433..72cffd17 100644
--- a/core/embed/rust/build.rs
+++ b/core/embed/rust/build.rs
@@ -654,6 +654,7 @@ fn generate_crypto_bindings() {
.allowlist_function("hmac_sha256_Final")
// sha256
.allowlist_var("SHA256_DIGEST_LENGTH")
+ .allowlist_var("SHA256_BLOCK_LENGTH")
.allowlist_type("SHA256_CTX")
.no_copy("SHA256_CTX")
.allowlist_function("sha256_Init")
@@ -661,6 +662,7 @@ fn generate_crypto_bindings() {
.allowlist_function("sha256_Final")
// sha512
.allowlist_var("SHA512_DIGEST_LENGTH")
+ .allowlist_var("SHA512_BLOCK_LENGTH")
.allowlist_type("SHA512_CTX")
.no_copy("SHA512_CTX")
.allowlist_function("sha512_Init")
diff --git a/core/embed/rust/src/crypto/aesgcm.rs b/core/embed/rust/src/crypto/aesgcm.rs
index 1b4b2863..7e5e4596 100644
--- a/core/embed/rust/src/crypto/aesgcm.rs
+++ b/core/embed/rust/src/crypto/aesgcm.rs
@@ -2,7 +2,7 @@ use core::pin::Pin;
use zeroize::Zeroize;
-use super::{ffi, memory::Memory, Error};
+use super::{consteq, ffi, memory::Memory, Error};
// Tag size is a parameter but we fix it to 16 here for simplicity.
pub const TAG_SIZE: usize = 16;
@@ -16,19 +16,21 @@ const KEY_SIZES: [usize; 3] = [16, 24, 32];
#[derive(PartialEq)]
enum State {
Init,
- Encrypting,
- Decrypting,
+ Processing,
Finished,
Failed,
}
-pub struct AesGcm<'a> {
+struct AesGcmInner<'a> {
ctx: Pin<&'a mut Memory<ffi::gcm_ctx>>,
state: State,
}
-impl<'a> AesGcm<'a> {
- pub fn new(
+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],
@@ -49,7 +51,7 @@ impl<'a> AesGcm<'a> {
Ok(aesgcm)
}
- pub fn reset(&mut self, iv: &[u8]) {
+ fn reset(&mut self, iv: &[u8]) {
// SAFETY: ffi
let res = unsafe {
ffi::gcm_init_message(iv.as_ptr(), iv.len() as cty::c_ulong, self.ctx.inner())
@@ -58,6 +60,62 @@ impl<'a> AesGcm<'a> {
self.state = State::Init;
}
+ fn auth(&mut self, data: &[u8]) -> Result<(), Error> {
+ self.check_state(&[State::Init, State::Processing])?;
+
+ // SAFETY: ffi
+ let res = unsafe {
+ ffi::gcm_auth_header(data.as_ptr(), data.len() as cty::c_ulong, self.ctx.inner())
+ };
+ ensure!(res == RETURN_GOOD, "gcm_auth_header");
+ Ok(())
+ }
+
+ fn finish(&mut self) -> Result<Tag, Error> {
+ self.check_state(&[State::Init, State::Processing])?;
+ self.state = State::Finished;
+
+ let mut tag = [0u8; TAG_SIZE];
+ // SAFETY: ffi
+ let res = unsafe {
+ ffi::gcm_compute_tag(
+ tag.as_mut_ptr(),
+ tag.len() as cty::c_ulong,
+ self.ctx.inner(),
+ )
+ };
+ if res != RETURN_GOOD {
+ self.state = State::Failed;
+ return Err(Error::InvalidContext);
+ }
+ Ok(tag)
+ }
+
+ fn check_state(&self, allowed: &[State]) -> Result<(), Error> {
+ if !allowed.contains(&self.state) {
+ return Err(Error::InvalidContext);
+ }
+ Ok(())
+ }
+}
+
+impl<'a> AesGcmEncrypt<'a> {
+ pub fn new(
+ ctx: Pin<&'a mut Memory<ffi::gcm_ctx>>,
+ key: &[u8],
+ iv: &[u8],
+ ) -> Result<Self, Error> {
+ Ok(Self(AesGcmInner::new(ctx, key, iv)?))
+ }
+
+ pub fn reset(&mut self, iv: &[u8]) {
+ self.0.reset(iv)
+ }
+
+ pub fn auth(&mut self, data: &[u8]) -> Result<(), Error> {
+ self.0.auth(data)
+ }
+
pub fn encrypt<'b>(
&mut self,
plaintext: &[u8],
@@ -77,20 +135,46 @@ impl<'a> AesGcm<'a> {
}
pub fn encrypt_in_place(&mut self, data: &mut [u8]) -> Result<(), Error> {
- self.check_state(&[State::Init, State::Encrypting])?;
- self.state = State::Encrypting;
+ self.0.check_state(&[State::Init, State::Processing])?;
+ self.0.state = State::Processing;
let res = unsafe {
ffi::gcm_encrypt(
data.as_mut_ptr(),
data.len() as cty::c_ulong,
- self.ctx.inner(),
+ self.0.ctx.inner(),
)
};
ensure!(res == RETURN_GOOD, "gcm_encrypt");
Ok(())
}
+ 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> {
+ Ok(Self(AesGcmInner::new(ctx, key, iv)?))
+ }
+
+ pub fn reset(&mut self, iv: &[u8]) {
+ self.0.reset(iv)
+ }
+
+ pub fn auth(&mut self, data: &[u8]) -> Result<(), Error> {
+ self.0.auth(data)
+ }
+
pub fn decrypt<'b>(
&mut self,
ciphertext: &[u8],
@@ -105,55 +189,25 @@ impl<'a> AesGcm<'a> {
}
pub fn decrypt_in_place(&mut self, data: &mut [u8]) -> Result<(), Error> {
- self.check_state(&[State::Init, State::Decrypting])?;
- self.state = State::Decrypting;
+ self.0.check_state(&[State::Init, State::Processing])?;
+ self.0.state = State::Processing;
// SAFETY: ffi
let res = unsafe {
ffi::gcm_decrypt(
data.as_mut_ptr(),
data.len() as cty::c_ulong,
- self.ctx.inner(),
+ self.0.ctx.inner(),
)
};
ensure!(res == RETURN_GOOD, "gcm_decrypt");
Ok(())
}
- pub fn auth(&mut self, data: &[u8]) -> Result<(), Error> {
- self.check_state(&[State::Init, State::Encrypting, State::Decrypting])?;
-
- // SAFETY: ffi
- let res = unsafe {
- ffi::gcm_auth_header(data.as_ptr(), data.len() as cty::c_ulong, self.ctx.inner())
- };
- ensure!(res == RETURN_GOOD, "gcm_auth_header");
- Ok(())
- }
-
- pub fn finish(&mut self) -> Result<Tag, Error> {
- self.check_state(&[State::Init, State::Encrypting, State::Decrypting])?;
- self.state = State::Finished;
-
- let mut tag = [0u8; TAG_SIZE];
- // SAFETY: ffi
- let res = unsafe {
- ffi::gcm_compute_tag(
- tag.as_mut_ptr(),
- tag.len() as cty::c_ulong,
- self.ctx.inner(),
- )
- };
- if res != RETURN_GOOD {
- self.state = State::Failed;
- return Err(Error::InvalidContext);
- }
- Ok(tag)
- }
-
- fn check_state(&self, allowed: &[State]) -> Result<(), Error> {
- if !allowed.contains(&self.state) {
- return Err(Error::InvalidContext);
+ pub fn finish(&mut self, expected_tag: &Tag) -> Result<(), Error> {
+ let computed_tag = self.0.finish()?;
+ if !consteq(&computed_tag, expected_tag) {
+ return Err(Error::AuthenticationFailed);
}
Ok(())
}
@@ -163,7 +217,7 @@ impl<'a> AesGcm<'a> {
}
}
-impl Drop for AesGcm<'_> {
+impl Drop for AesGcmInner<'_> {
fn drop(&mut self) {
self.ctx.zeroize();
}
@@ -183,18 +237,19 @@ mod test {
}
impl Vector {
- fn decoded(&self) -> (Vec<u8>, Vec<u8>, Vec<u8>, Vec<u8>, Vec<u8>) {
+ fn decoded(&self) -> (Vec<u8>, Vec<u8>, Vec<u8>, Vec<u8>, Vec<u8>, Tag) {
let key = hex::decode(self.key).unwrap();
let iv = hex::decode(self.iv).unwrap();
let aad = hex::decode(self.aad).unwrap();
let pt = hex::decode(self.plaintext).unwrap();
let ct = hex::decode(self.ciphertext).unwrap();
- (key, iv, aad, pt, ct)
+ let tag = hex::decode(self.tag).unwrap();
+ (key, iv, aad, pt, ct, Tag::try_from(tag).unwrap())
}
}
- // first 10 vectors from https://github.com/BrianGladman/modes/blob/master/testvals/gcm.1
const AES_GCM_VECTORS: &[Vector] = &[
+ // first 10 vectors from https://github.com/BrianGladman/modes/blob/master/testvals/gcm.1
Vector {
key: "00000000000000000000000000000000",
iv: "000000000000000000000000",
@@ -275,16 +330,49 @@ mod test {
ciphertext: "3980ca0b3c00e841eb06fac4872a2757859e1ceaa6efd984628593b40ca1e19c7d773d00c144c525ac619d18c84a3f4718e2448b2fe324d9ccda2710",
tag: "2519498e80f1478f37ba55bd6d27618c",
},
+ // test vectors from test_trezor.wire.thp.crypto.py
+ Vector {
+ key: "0001020304050607000102030405060700010203040506070001020304050607",
+ iv: "000000000000000000000000",
+ aad: "5564",
+ plaintext: "00010203040506070809",
+ ciphertext: "e2c9dd152fbee5821ea7",
+ tag: "10625812de81b14a46b9f1e5100a6d0c",
+ },
+ Vector {
+ key: "0001020304050607000102030405060700010203040506070001020304050607",
+ iv: "000000000000000000000001",
+ aad: "5564",
+ plaintext: "00010203040506070809",
+ ciphertext: "79811619ddb07c2b99f8",
+ tag: "71c6b872cdc499a7e9a3c7441f053214",
+ },
+ Vector {
+ key: "0001020304050607000102030405060700010203040506070001020304050607",
+ iv: "000000000000000000000171",
+ aad: "5564",
+ plaintext: "000102030405060708090a0b0c0d0e0f",
+ ciphertext: "03bd030390f2dfe815a61c2b157a064f",
+ tag: "c1200f8a7ae9a6d32cef0fff878d55c2",
+ },
+ Vector {
+ key: "0001020304050607000102030405060700010203040506070001020304050607",
+ iv: "000000000000000000000171",
+ aad: "5564738291",
+ plaintext: "000102030405060708090a0b0c0d0e0f",
+ ciphertext: "03bd030390f2dfe815a61c2b157a064f",
+ tag: "693ac160cd93a20f7fc255f049d808d0",
+ },
];
#[test]
fn test_vectors() {
for v in AES_GCM_VECTORS {
- let (key, iv, aad, plaintext, ciphertext) = v.decoded();
+ let (key, iv, aad, plaintext, ciphertext, tag) = v.decoded();
- init_ctx!(AesGcm, ctx_enc, &key, &iv);
+ init_ctx!(AesGcmEncrypt, ctx_enc, &key, &iv);
let mut ctx_enc = ctx_enc.unwrap();
- init_ctx!(AesGcm, ctx_dec, &key, &iv);
+ init_ctx!(AesGcmDecrypt, ctx_dec, &key, &iv);
let mut ctx_dec = ctx_dec.unwrap();
if !plaintext.is_empty() {
@@ -303,47 +391,45 @@ mod test {
let result = ctx_enc.finish().unwrap();
assert_eq!(hex::encode(result), v.tag);
- let result = ctx_dec.finish().unwrap();
- assert_eq!(hex::encode(result), v.tag);
+ ctx_dec.finish(&tag).unwrap();
}
}
#[test]
fn test_state() {
- init_ctx!(AesGcm, ctx, &[0u8; 16], b"1");
- let mut ctx = ctx.unwrap();
+ // ok: empty string tag - encryption
+ init_ctx!(AesGcmEncrypt, ctx_enc, &[0u8; 16], b"1");
+ let mut ctx_enc = ctx_enc.unwrap();
+ let tag_empty = ctx_enc.finish().unwrap();
- // ok: empty string tag
- ctx.finish().unwrap();
+ // ok: empty string tag - decryption
+ init_ctx!(AesGcmDecrypt, ctx_dec, &[0u8; 16], b"1");
+ let mut ctx_dec = ctx_dec.unwrap();
+ ctx_dec.finish(&tag_empty).unwrap();
// ok: any single operation
// not ok: after reset
- let mut dest = [0u8; 16];
- ctx.reset(b"2");
- ctx.encrypt(b"asdf", &mut dest).unwrap();
- ctx.finish().unwrap();
- assert!(ctx.encrypt(b"asdf", &mut dest).is_err());
-
- ctx.reset(b"3");
- ctx.decrypt(b"fdsa", &mut dest).unwrap();
- ctx.finish().unwrap();
- assert!(ctx.decrypt(b"fdsa", &mut dest).is_err());
-
- ctx.reset(b"5");
- ctx.auth(b"foobar").unwrap();
- ctx.finish().unwrap();
- assert!(ctx.auth(b"foobar").is_err());
-
- // not ok: mixing encrypt and decrypt
- ctx.reset(b"6");
- ctx.encrypt(b"asdf", &mut dest).unwrap();
- ctx.encrypt_in_place(&mut dest).unwrap();
- assert!(ctx.decrypt(b"fdsa", &mut dest).is_err());
-
- ctx.reset(b"7");
- ctx.decrypt_in_place(&mut dest).unwrap();
- ctx.auth(b"foobar").unwrap();
- assert!(ctx.encrypt(b"fdsa", &mut dest).is_err());
+ let mut dest = [0u8; 4];
+ let mut dest2 = [0u8; 16];
+ ctx_enc.reset(b"2");
+ ctx_enc.encrypt(b"asdf", &mut dest).unwrap();
+ let tag2 = ctx_enc.finish().unwrap();
+ assert!(ctx_enc.encrypt(b"asdf", &mut dest2).is_err());
+
+ ctx_dec.reset(b"2");
+ ctx_dec.decrypt(&dest, &mut dest2).unwrap();
+ ctx_dec.finish(&tag2).unwrap();
+ assert!(ctx_dec.decrypt(b"fdsa", &mut dest).is_err());
+
+ ctx_enc.reset(b"5");
+ ctx_enc.auth(b"foobar").unwrap();
+ let tag5 = ctx_enc.finish().unwrap();
+ assert!(ctx_enc.auth(b"foobar").is_err());
+
+ ctx_dec.reset(b"5");
+ ctx_dec.auth(b"foobar").unwrap();
+ ctx_dec.finish(&tag5).unwrap();
+ assert!(ctx_dec.auth(b"foobar").is_err());
}
// test vectors from
@@ -387,10 +473,10 @@ mod test {
#[test]
fn test_gcm() {
for v in NIST_VECTORS {
- let (key, iv, aad, pt, ct) = v.decoded();
+ let (key, iv, aad, pt, ct, tag) = v.decoded();
// Test encryption.
- init_ctx!(AesGcm, ctx, &key, &iv);
+ init_ctx!(AesGcmEncrypt, ctx, &key, &iv);
let mut ctx = ctx.unwrap();
if !aad.is_empty() {
ctx.auth(&aad).unwrap();
@@ -403,25 +489,25 @@ mod test {
assert_eq!(hex::encode(result), v.tag);
// Test decryption.
- ctx.reset(&iv);
+ init_ctx!(AesGcmDecrypt, ctx, &key, &iv);
+ let mut ctx = ctx.unwrap();
if !aad.is_empty() {
ctx.auth(&aad).unwrap();
}
let result = ctx.decrypt(&ct, &mut buffer).unwrap();
assert_eq!(hex::encode(result), v.plaintext);
- let result = ctx.finish().unwrap();
- assert_eq!(hex::encode(result), v.tag);
+ ctx.finish(&tag).unwrap();
}
}
#[test]
fn test_gcm_in_place() {
for v in NIST_VECTORS {
- let (key, iv, aad, pt, ct) = v.decoded();
+ let (key, iv, aad, pt, ct, tag) = v.decoded();
// Test encryption.
- init_ctx!(AesGcm, ctx, &key, &iv);
+ init_ctx!(AesGcmEncrypt, ctx, &key, &iv);
let mut ctx = ctx.unwrap();
if !aad.is_empty() {
ctx.auth(&aad).unwrap();
@@ -435,7 +521,8 @@ mod test {
assert_eq!(hex::encode(result), v.tag);
// Test decryption.
- ctx.reset(&iv);
+ init_ctx!(AesGcmDecrypt, ctx, &key, &iv);
+ let mut ctx = ctx.unwrap();
if !aad.is_empty() {
ctx.auth(&aad).unwrap();
}
@@ -444,19 +531,18 @@ mod test {
ctx.decrypt_in_place(&mut buffer).unwrap();
assert_eq!(hex::encode(buffer), v.plaintext);
- let result = ctx.finish().unwrap();
- assert_eq!(hex::encode(result), v.tag);
+ ctx.finish(&tag).unwrap();
}
}
#[test]
fn test_gcm_chunks() {
for v in NIST_VECTORS {
- let (key, iv, aad, pt, ct) = v.decoded();
+ let (key, iv, aad, pt, ct, tag) = v.decoded();
let chunk_len = pt.len() / 3;
let mut buffer = vec![0; pt.len()];
- init_ctx!(AesGcm, ctx, &key, &iv);
+ init_ctx!(AesGcmDecrypt, ctx, &key, &iv);
let mut ctx = ctx.unwrap();
ctx.decrypt(&ct[..chunk_len], &mut buffer[..chunk_len])
.unwrap();
@@ -465,10 +551,11 @@ mod test {
.unwrap();
ctx.auth(aad.get(7..).unwrap_or(&[])).unwrap();
assert_eq!(hex::encode(buffer), v.plaintext);
- assert_eq!(hex::encode(ctx.finish().unwrap()), v.tag);
+ ctx.finish(&tag).unwrap();
buffer = vec![0; pt.len()];
- ctx.reset(&iv);
+ init_ctx!(AesGcmEncrypt, ctx, &key, &iv);
+ let mut ctx = ctx.unwrap();
ctx.auth(aad.get(..7).unwrap_or(&[])).unwrap();
ctx.encrypt(&pt[..chunk_len], &mut buffer[..chunk_len])
.unwrap();
@@ -483,21 +570,22 @@ mod test {
#[test]
fn test_gcm_chunks_in_place() {
for v in NIST_VECTORS {
- let (key, iv, aad, pt, ct) = v.decoded();
+ let (key, iv, aad, pt, ct, tag) = v.decoded();
let chunk_len = pt.len() / 3;
let mut buffer = ct;
- init_ctx!(AesGcm, ctx, &key, &iv);
+ init_ctx!(AesGcmDecrypt, ctx, &key, &iv);
let mut ctx = ctx.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();
ctx.auth(aad.get(7..).unwrap_or(&[])).unwrap();
assert_eq!(hex::encode(buffer), v.plaintext);
- assert_eq!(hex::encode(ctx.finish().unwrap()), v.tag);
+ ctx.finish(&tag).unwrap();
let mut buffer = pt;
- ctx.reset(&iv);
+ init_ctx!(AesGcmEncrypt, ctx, &key, &iv);
+ let mut ctx = ctx.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();
diff --git a/core/embed/rust/src/crypto/curve25519.rs b/core/embed/rust/src/crypto/curve25519.rs
index 27cea648..f09b1674 100644
--- a/core/embed/rust/src/crypto/curve25519.rs
+++ b/core/embed/rust/src/crypto/curve25519.rs
@@ -7,11 +7,69 @@ pub struct Point {
bytes: [u8; 32],
}
+#[cfg(feature = "thp")]
+impl trezor_thp::channel::U8Array for Point {
+ fn new() -> Self {
+ Self { bytes: [0u8; 32] }
+ }
+
+ fn new_with(c: u8) -> Self {
+ Self { bytes: [c; 32] }
+ }
+
+ fn from_slice(src: &[u8]) -> Self {
+ let mut bytes = [0u8; 32];
+ bytes.copy_from_slice(src);
+ Self { bytes }
+ }
+
+ fn len() -> usize {
+ 32
+ }
+
+ fn as_slice(&self) -> &[u8] {
+ &self.bytes
+ }
+
+ fn as_mut(&mut self) -> &mut [u8] {
+ &mut self.bytes
+ }
+}
+
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct Scalar {
bytes: [u8; 32],
}
+#[cfg(feature = "thp")]
+impl trezor_thp::channel::U8Array for Scalar {
+ fn new() -> Self {
+ Self { bytes: [0u8; 32] }
+ }
+
+ fn new_with(c: u8) -> Self {
+ Self { bytes: [c; 32] }
+ }
+
+ fn from_slice(src: &[u8]) -> Self {
+ let mut bytes = [0u8; 32];
+ bytes.copy_from_slice(src);
+ Self { bytes }
+ }
+
+ fn len() -> usize {
+ 32
+ }
+
+ fn as_slice(&self) -> &[u8] {
+ &self.bytes
+ }
+
+ fn as_mut(&mut self) -> &mut [u8] {
+ &mut self.bytes
+ }
+}
+
impl Scalar {
pub fn from_bytes(bytes: [u8; 32]) -> Self {
let mut res = Self { bytes };
@@ -22,7 +80,6 @@ impl Scalar {
res
}
- #[cfg(feature = "test")]
pub fn generate() -> Self {
let mut bytes = [0u8; 32];
crate::trezorhal::random::bytes(&mut bytes);
diff --git a/core/embed/rust/src/crypto/mod.rs b/core/embed/rust/src/crypto/mod.rs
index 282d6fcb..72f9002f 100644
--- a/core/embed/rust/src/crypto/mod.rs
+++ b/core/embed/rust/src/crypto/mod.rs
@@ -1,3 +1,5 @@
+use core::hint::black_box;
+
use crate::error::value_error;
pub mod aesgcm;
@@ -7,7 +9,7 @@ pub mod curve25519;
pub mod ed25519;
mod ffi;
pub mod hmac;
-mod memory;
+pub mod memory;
pub mod merkle;
pub mod sha256;
pub mod sha512;
@@ -22,6 +24,8 @@ pub enum Error {
InvalidParams,
// State precondition check failed (possibly raised by C implementation)
InvalidContext,
+ // Authentication failed (e.g. AEAD tag mismatch)
+ AuthenticationFailed,
}
impl From<Error> for crate::error::Error {
@@ -31,6 +35,34 @@ impl From<Error> for crate::error::Error {
Error::InvalidEncoding => value_error!(c"Invalid key or signature encoding"),
Error::InvalidParams => value_error!(c"Invalid cryptographic parameters"),
Error::InvalidContext => value_error!(c"Invalid cryptographic context"),
+ Error::AuthenticationFailed => value_error!(c"Authentication failed"),
}
}
}
+
+/// Constant time bytestring comparison for two arrays of the same length.
+fn consteq<const N: usize>(a: &[u8; N], b: &[u8; N]) -> bool {
+ let mut diff: u8 = 0;
+ for i in 0..N {
+ diff |= a[i] ^ b[i];
+ }
+ black_box(black_box(diff) == 0)
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn test_consteq() {
+ assert!(consteq(&[], &[]));
+ assert!(consteq(&[0u8; 256], &[0u8; 256]));
+ assert!(consteq(&[0xffu8; 256], &[0xffu8; 256]));
+ assert!(consteq(b"0123456789abcdef", b"0123456789abcdef"));
+
+ assert!(!consteq(&[0u8; 256], &[0xffu8; 256]));
+ assert!(!consteq(&[0xffu8; 256], &[0u8; 256]));
+ assert!(!consteq(b"0123456789abcdef", b"123456789abcdef0"));
+ assert!(!consteq(b"0000000000000000", b"0000000000000001"));
+ }
+}
diff --git a/core/embed/rust/src/crypto/sha256.rs b/core/embed/rust/src/crypto/sha256.rs
index 9fb387d7..a041e54e 100644
--- a/core/embed/rust/src/crypto/sha256.rs
+++ b/core/embed/rust/src/crypto/sha256.rs
@@ -1,4 +1,4 @@
-use core::pin::Pin;
+use core::{mem::MaybeUninit, pin::Pin};
use zeroize::{Zeroize, ZeroizeOnDrop};
@@ -7,6 +7,7 @@ use super::{
memory::{init_ctx, Memory},
};
+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];
@@ -50,6 +51,41 @@ pub fn digest(data: &[u8]) -> Digest {
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 Drop for NoPinSha256 {
+ fn drop(&mut self) {
+ // C implementation zeroes the state
+ // SAFETY: ffi
+ unsafe { ffi::sha256_Final(&mut self.ctx as *mut _, core::ptr::null_mut()) };
+ }
+}
+
+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 }
+ }
+}
+
+impl NoPinSha256 {
+ pub fn update(&mut self, data: &[u8]) {
+ // SAFETY: ffi
+ unsafe { ffi::sha256_Update(&mut self.ctx as *mut _, data.as_ptr(), data.len()) };
+ }
+
+ pub fn finalize_into(mut self, out: &mut Digest) {
+ // SAFETY: ffi
+ unsafe { ffi::sha256_Final(&mut self.ctx as *mut _, out.as_mut_ptr()) };
+ }
+}
+
#[cfg(test)]
mod test {
use crate::strutil::hexlify;
diff --git a/core/embed/rust/src/crypto/sha512.rs b/core/embed/rust/src/crypto/sha512.rs
index b2b5522a..d73f2cb5 100644
--- a/core/embed/rust/src/crypto/sha512.rs
+++ b/core/embed/rust/src/crypto/sha512.rs
@@ -7,6 +7,7 @@ use super::{
use zeroize::{Zeroize, ZeroizeOnDrop};
+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];
diff --git a/core/embed/rust/src/thp/crypto.rs b/core/embed/rust/src/thp/crypto.rs
new file mode 100644
index 00000000..40903686
--- /dev/null
+++ b/core/embed/rust/src/thp/crypto.rs
@@ -0,0 +1,209 @@
+use trezor_thp::channel::{Backend, Cipher, Hash, U8Array, DH};
+
+use zeroize::{Zeroize, Zeroizing};
+
+use crate::crypto::{aesgcm, curve25519, memory::init_ctx, sha256};
+
+/// Array wrapper that zeroizes on `drop()`. Can't use zeroizing directly due to
+/// the orphan rule.
+pub struct Sensitive<A: U8Array + Zeroize>(Zeroizing<A>);
+
+impl<A: U8Array + Zeroize> Sensitive<A> {
+ pub fn from(a: A) -> Self {
+ Sensitive(Zeroizing::new(a))
+ }
+}
+
+impl<A> U8Array for Sensitive<A>
+where
+ A: Zeroize + U8Array,
+{
+ fn new() -> Self {
+ Sensitive::from(A::new())
+ }
+
+ fn new_with(v: u8) -> Self {
+ Sensitive::from(A::new_with(v))
+ }
+
+ fn from_slice(s: &[u8]) -> Self {
+ Sensitive::from(A::from_slice(s))
+ }
+
+ fn len() -> usize {
+ A::len()
+ }
+
+ fn as_slice(&self) -> &[u8] {
+ self.0.as_slice()
+ }
+
+ fn as_mut(&mut self) -> &mut [u8] {
+ self.0.as_mut()
+ }
+}
+
+pub struct TrezorCryptoCurve25519;
+
+impl DH for TrezorCryptoCurve25519 {
+ // Scalar & Point implement ZeroizeOnDrop, no need to wrap them.
+ type Key = curve25519::Scalar;
+ type Pubkey = curve25519::Point;
+ type Output = curve25519::Point;
+
+ fn name() -> &'static str {
+ "25519"
+ }
+
+ fn genkey() -> Self::Key {
+ curve25519::Scalar::generate()
+ }
+
+ fn pubkey(privkey: &Self::Key) -> Self::Pubkey {
+ curve25519::Point::from_secret(privkey)
+ }
+
+ fn dh(privkey: &Self::Key, pubkey: &Self::Pubkey) -> Result<Self::Output, ()> {
+ Ok(pubkey.multiply(privkey))
+ }
+}
+
+pub struct TrezorCryptoAesGcm;
+
+impl TrezorCryptoAesGcm {
+ const KEY_SIZE: usize = 32;
+ const NONCE_SIZE: usize = 12;
+
+ fn full_nonce(nonce_counter: u64) -> [u8; Self::NONCE_SIZE] {
+ let mut full_nonce = [0u8; Self::NONCE_SIZE];
+ full_nonce[4..].copy_from_slice(&nonce_counter.to_be_bytes());
+ assert_eq!(&full_nonce[0..4], &[0u8; 4]);
+ full_nonce
+ }
+}
+
+impl Cipher for TrezorCryptoAesGcm {
+ fn name() -> &'static str {
+ "AESGCM"
+ }
+
+ type Key = Sensitive<[u8; Self::KEY_SIZE]>;
+
+ fn encrypt(key: &Self::Key, nonce: u64, ad: &[u8], plaintext: &[u8], out: &mut [u8]) {
+ assert!(plaintext.len().checked_add(aesgcm::TAG_SIZE) == Some(out.len()));
+
+ let full_nonce = Self::full_nonce(nonce);
+ 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);
+ unwrap!(ctx.encrypt_in_place(in_out));
+ unwrap!(ctx.auth(ad));
+ let tag = unwrap!(ctx.finish());
+ tag_out.copy_from_slice(&tag);
+ }
+
+ fn encrypt_in_place(
+ key: &Self::Key,
+ nonce: u64,
+ ad: &[u8],
+ in_out: &mut [u8],
+ plaintext_len: usize,
+ ) -> usize {
+ assert!(plaintext_len
+ .checked_add(aesgcm::TAG_SIZE)
+ .is_some_and(|l| l <= in_out.len()));
+
+ let full_nonce = Self::full_nonce(nonce);
+ 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);
+ unwrap!(ctx.encrypt_in_place(in_out));
+ unwrap!(ctx.auth(ad));
+ let tag = unwrap!(ctx.finish());
+ tag_out.copy_from_slice(&tag);
+
+ plaintext_len + aesgcm::TAG_SIZE
+ }
+
+ fn decrypt(
+ key: &Self::Key,
+ nonce: u64,
+ ad: &[u8],
+ ciphertext: &[u8],
+ out: &mut [u8],
+ ) -> Result<(), ()> {
+ assert!(ciphertext.len().checked_sub(aesgcm::TAG_SIZE) == Some(out.len()));
+
+ let full_nonce = Self::full_nonce(nonce);
+ 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);
+ unwrap!(ctx.decrypt_in_place(out));
+ unwrap!(ctx.auth(ad));
+ ctx.finish(tag).map_err(|_| out.zeroize())?;
+
+ Ok(())
+ }
+
+ fn decrypt_in_place(
+ key: &Self::Key,
+ nonce: u64,
+ ad: &[u8],
+ in_out: &mut [u8],
+ ciphertext_len: usize,
+ ) -> Result<usize, ()> {
+ assert!(ciphertext_len <= in_out.len());
+ assert!(ciphertext_len >= aesgcm::TAG_SIZE);
+
+ let full_nonce = Self::full_nonce(nonce);
+ 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);
+ unwrap!(ctx.decrypt_in_place(in_out));
+ unwrap!(ctx.auth(ad));
+ ctx.finish(tag).map_err(|_| in_out.zeroize())?;
+
+ Ok(in_out.len())
+ }
+}
+
+pub type TrezorCryptoSha256 = sha256::NoPinSha256;
+
+impl Hash for TrezorCryptoSha256 {
+ fn name() -> &'static str {
+ "SHA256"
+ }
+
+ type Block = Sensitive<[u8; sha256::BLOCK_SIZE]>;
+ type Output = Sensitive<sha256::Digest>;
+
+ fn input(&mut self, data: &[u8]) {
+ self.update(data);
+ }
+
+ fn result(&mut self) -> Self::Output {
+ let mut digest = sha256::Digest::default();
+ self.clone().finalize_into(&mut digest);
+ Self::Output::from_slice(&digest)
+ }
+}
+
+pub struct TrezorCrypto;
+
+impl Backend for TrezorCrypto {
+ type DH = TrezorCryptoCurve25519;
+ type Cipher = TrezorCryptoAesGcm;
+ type Hash = TrezorCryptoSha256;
+
+ fn random_bytes(dest: &mut [u8]) {
+ crate::trezorhal::random::bytes(dest);
+ }
+}
Why this scored 35/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.