From 65c49a4108906c4000fe5702fdee3148dd3c641e Mon Sep 17 00:00:00 2001 From: DaughterOfMars Date: Tue, 16 Jan 2024 16:11:28 -0500 Subject: [PATCH] Add ed25519 public key bytes wrapper (#221) * Add ed25519 public key bytes wrapper * modify a few tests * clippy * revert deref * bump crypto version * copyrights * PR suggestions --- .changes/ed25519-pub-key-bytes.md | 5 + src/hashes/ternary/kerl/bigint/i384/mod.rs | 186 +++++++++------------ src/hashes/ternary/kerl/bigint/macros.rs | 22 +-- src/hashes/ternary/kerl/bigint/u384/mod.rs | 24 ++- src/keys/age.rs | 8 +- src/keys/bip39.rs | 5 +- src/keys/slip10.rs | 6 +- src/signatures/ed25519.rs | 88 +++++++++- tests/ed25519.rs | 14 +- 9 files changed, 212 insertions(+), 146 deletions(-) create mode 100644 .changes/ed25519-pub-key-bytes.md diff --git a/.changes/ed25519-pub-key-bytes.md b/.changes/ed25519-pub-key-bytes.md new file mode 100644 index 00000000..b598a139 --- /dev/null +++ b/.changes/ed25519-pub-key-bytes.md @@ -0,0 +1,5 @@ +--- +"iota-crypto": patch +--- + +Added ed25519 PublicKeyBytes wrapper that does not require validation to use. diff --git a/src/hashes/ternary/kerl/bigint/i384/mod.rs b/src/hashes/ternary/kerl/bigint/i384/mod.rs index 779787dc..a3251cbc 100644 --- a/src/hashes/ternary/kerl/bigint/i384/mod.rs +++ b/src/hashes/ternary/kerl/bigint/i384/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2021 IOTA Stiftung +// Copyright 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 //! This module contains signed integers encoded by 384 bits. @@ -168,6 +168,12 @@ impl PartialEq for I384 { impl PartialOrd for I384 { fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for I384 { + fn cmp(&self, other: &Self) -> Ordering { use Ordering::*; let mut zipped_iter = self.inner.iter().zip(other.inner.iter()); @@ -183,25 +189,25 @@ impl PartialOrd for I384 { const UMAX: u8 = core::u8::MAX; let numbers_negative = match zipped_iter.next() { // Case 1: both numbers are negative, s is less. - Some((s @ NEGBIT..=UMAX, o @ NEGBIT..=UMAX)) if s > o => return Some(Greater), + Some((s @ NEGBIT..=UMAX, o @ NEGBIT..=UMAX)) if s > o => return Greater, // Case 2: both numbers are negative, s is greater. - Some((s @ NEGBIT..=UMAX, o @ NEGBIT..=UMAX)) if s < o => return Some(Less), + Some((s @ NEGBIT..=UMAX, o @ NEGBIT..=UMAX)) if s < o => return Less, // Case 3: both numbers are negative, but equal. Some((NEGBIT..=UMAX, NEGBIT..=UMAX)) => true, // Case 4: only s is negative. - Some((NEGBIT..=UMAX, _)) => return Some(Less), + Some((NEGBIT..=UMAX, _)) => return Less, // Case 5: only o is negative. - Some((_, NEGBIT..=UMAX)) => return Some(Greater), + Some((_, NEGBIT..=UMAX)) => return Greater, // Case 6: both are positive, s is greater. - Some((s, o)) if s > o => return Some(Greater), + Some((s, o)) if s > o => return Greater, // Case 7: both are positive, s is less. - Some((s, o)) if s < o => return Some(Less), + Some((s, o)) if s < o => return Less, // Fallthrough case; only happens if s == o. Some(_) => false, @@ -212,24 +218,13 @@ impl PartialOrd for I384 { for (s, o) in zipped_iter { match s.cmp(o) { - Ordering::Greater => return if numbers_negative { Some(Less) } else { Some(Greater) }, - Ordering::Less => return if numbers_negative { Some(Greater) } else { Some(Less) }, + Ordering::Greater => return if numbers_negative { Less } else { Greater }, + Ordering::Less => return if numbers_negative { Greater } else { Less }, Ordering::Equal => continue, } } - Some(Equal) - } -} - -impl Ord for I384 { - fn cmp(&self, other: &Self) -> Ordering { - match self.partial_cmp(other) { - Some(ordering) => ordering, - - // The ordering is total, hence `partial_cmp` will never return `None`. - None => unreachable!(), - } + Equal } } @@ -401,23 +396,6 @@ impl From> for I384 { impl Ord for I384 { fn cmp(&self, other: &Self) -> Ordering { - match self.partial_cmp(other) { - Some(ordering) => ordering, - - // The ordering is total, hence `partial_cmp` will never return `None`. - None => unreachable!(), - } - } -} - -impl PartialEq for I384 { - fn eq(&self, other: &Self) -> bool { - self.inner == other.inner - } -} - -impl PartialOrd for I384 { - fn partial_cmp(&self, other: &Self) -> Option { use Ordering::*; let mut zipped_iter = self.inner.iter().zip(other.inner.iter()); @@ -433,25 +411,25 @@ impl PartialOrd for I384 { const UMAX: u32 = core::u32::MAX; let numbers_negative = match zipped_iter.next() { // Case 1: both numbers are negative, s is less. - Some((s @ NEGBIT..=UMAX, o @ NEGBIT..=UMAX)) if s > o => return Some(Greater), + Some((s @ NEGBIT..=UMAX, o @ NEGBIT..=UMAX)) if s > o => return Greater, // Case 2: both numbers are negative, s is greater. - Some((s @ NEGBIT..=UMAX, o @ NEGBIT..=UMAX)) if s < o => return Some(Less), + Some((s @ NEGBIT..=UMAX, o @ NEGBIT..=UMAX)) if s < o => return Less, // Case 3: both numbers are negative, but equal. Some((NEGBIT..=UMAX, NEGBIT..=UMAX)) => true, // Case 4: only s is negative. - Some((NEGBIT..=UMAX, _)) => return Some(Less), + Some((NEGBIT..=UMAX, _)) => return Less, // Case 5: only o is negative. - Some((_, NEGBIT..=UMAX)) => return Some(Greater), + Some((_, NEGBIT..=UMAX)) => return Greater, // Case 6: both are positive, s is greater. - Some((s, o)) if s > o => return Some(Greater), + Some((s, o)) if s > o => return Greater, // Case 7: both are positive, s is less. - Some((s, o)) if s < o => return Some(Less), + Some((s, o)) if s < o => return Less, // Fallthrough case; only happens if s == o. Some(_) => false, @@ -462,13 +440,25 @@ impl PartialOrd for I384 { for (s, o) in zipped_iter { match s.cmp(o) { - Ordering::Greater => return if numbers_negative { Some(Less) } else { Some(Greater) }, - Ordering::Less => return if numbers_negative { Some(Greater) } else { Some(Less) }, + Ordering::Greater => return if numbers_negative { Less } else { Greater }, + Ordering::Less => return if numbers_negative { Greater } else { Less }, Ordering::Equal => continue, } } - Some(Equal) + Equal + } +} + +impl PartialEq for I384 { + fn eq(&self, other: &Self) -> bool { + self.inner == other.inner + } +} + +impl PartialOrd for I384 { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) } } @@ -502,27 +492,6 @@ impl From> for I384 { impl Ord for I384 { fn cmp(&self, other: &Self) -> Ordering { - match self.partial_cmp(other) { - Some(ordering) => ordering, - - // The ordering is total, hence `partial_cmp` will never return `None`. - None => unreachable!(), - } - } -} - -impl PartialEq for I384 { - fn eq(&self, other: &Self) -> bool { - let mut are_equal = true; - for (a, b) in self.inner.iter().zip(other.inner.iter()) { - are_equal &= a == b - } - are_equal - } -} - -impl PartialOrd for I384 { - fn partial_cmp(&self, other: &Self) -> Option { use Ordering::*; let mut zipped_iter = self.inner.iter().rev().zip(other.inner.iter().rev()); @@ -538,25 +507,25 @@ impl PartialOrd for I384 { const UMAX: u8 = core::u8::MAX; let numbers_negative = match zipped_iter.next() { // Case 1: both numbers are negative, s is less. - Some((s @ NEGBIT..=UMAX, o @ NEGBIT..=UMAX)) if s > o => return Some(Greater), + Some((s @ NEGBIT..=UMAX, o @ NEGBIT..=UMAX)) if s > o => return Greater, // Case 2: both numbers are negative, s is greater. - Some((s @ NEGBIT..=UMAX, o @ NEGBIT..=UMAX)) if s < o => return Some(Less), + Some((s @ NEGBIT..=UMAX, o @ NEGBIT..=UMAX)) if s < o => return Less, // Case 3: both numbers are negative, but equal. Some((NEGBIT..=UMAX, NEGBIT..=UMAX)) => true, // Case 4: only s is negative. - Some((NEGBIT..=UMAX, _)) => return Some(Less), + Some((NEGBIT..=UMAX, _)) => return Less, // Case 5: only o is negative. - Some((_, NEGBIT..=UMAX)) => return Some(Greater), + Some((_, NEGBIT..=UMAX)) => return Greater, // Case 6: both are positive, s is greater. - Some((s, o)) if s > o => return Some(Greater), + Some((s, o)) if s > o => return Greater, // Case 7: both are positive, s is less. - Some((s, o)) if s < o => return Some(Less), + Some((s, o)) if s < o => return Less, // Fallthrough case; only happens if s == o. Some(_) => false, @@ -567,13 +536,29 @@ impl PartialOrd for I384 { for (s, o) in zipped_iter { match s.cmp(o) { - Ordering::Greater => return if numbers_negative { Some(Less) } else { Some(Greater) }, - Ordering::Less => return if numbers_negative { Some(Greater) } else { Some(Less) }, + Ordering::Greater => return if numbers_negative { Less } else { Greater }, + Ordering::Less => return if numbers_negative { Greater } else { Less }, Ordering::Equal => continue, } } - Some(Equal) + Equal + } +} + +impl PartialEq for I384 { + fn eq(&self, other: &Self) -> bool { + let mut are_equal = true; + for (a, b) in self.inner.iter().zip(other.inner.iter()) { + are_equal &= a == b + } + are_equal + } +} + +impl PartialOrd for I384 { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) } } @@ -737,23 +722,6 @@ impl From> for I384 { impl Ord for I384 { fn cmp(&self, other: &Self) -> Ordering { - match self.partial_cmp(other) { - Some(ordering) => ordering, - - // The ordering is total, hence `partial_cmp` will never return `None`. - None => unreachable!(), - } - } -} - -impl PartialEq for I384 { - fn eq(&self, other: &Self) -> bool { - self.inner == other.inner - } -} - -impl PartialOrd for I384 { - fn partial_cmp(&self, other: &Self) -> Option { use Ordering::*; let mut zipped_iter = self.inner.iter().rev().zip(other.inner.iter().rev()); @@ -770,25 +738,25 @@ impl PartialOrd for I384 { let numbers_negative = match zipped_iter.next() { // Case 1: both numbers are negative, s is less. - Some((s @ NEGBIT..=UMAX, o @ NEGBIT..=UMAX)) if s > o => return Some(Greater), + Some((s @ NEGBIT..=UMAX, o @ NEGBIT..=UMAX)) if s > o => return Greater, // Case 2: both numbers are negative, s is greater. - Some((s @ NEGBIT..=UMAX, o @ NEGBIT..=UMAX)) if s < o => return Some(Less), + Some((s @ NEGBIT..=UMAX, o @ NEGBIT..=UMAX)) if s < o => return Less, // Case 3: both numbers are negative, but equal. Some((NEGBIT..=UMAX, NEGBIT..=UMAX)) => true, // Case 4: only s is negative. - Some((NEGBIT..=UMAX, _)) => return Some(Less), + Some((NEGBIT..=UMAX, _)) => return Less, // Case 5: only o is negative. - Some((_, NEGBIT..=UMAX)) => return Some(Greater), + Some((_, NEGBIT..=UMAX)) => return Greater, // Case 6: both are positive, s is greater. - Some((s, o)) if s > o => return Some(Greater), + Some((s, o)) if s > o => return Greater, // Case 7: both are positive, s is less. - Some((s, o)) if s < o => return Some(Less), + Some((s, o)) if s < o => return Less, // Fallthrough case; only happens if s == o and positive. Some(_) => false, @@ -799,13 +767,25 @@ impl PartialOrd for I384 { for (s, o) in zipped_iter { match s.cmp(o) { - Ordering::Greater => return if numbers_negative { Some(Less) } else { Some(Greater) }, - Ordering::Less => return if numbers_negative { Some(Greater) } else { Some(Less) }, + Ordering::Greater => return if numbers_negative { Less } else { Greater }, + Ordering::Less => return if numbers_negative { Greater } else { Less }, Ordering::Equal => continue, } } - Some(Equal) + Equal + } +} + +impl PartialEq for I384 { + fn eq(&self, other: &Self) -> bool { + self.inner == other.inner + } +} + +impl PartialOrd for I384 { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) } } diff --git a/src/hashes/ternary/kerl/bigint/macros.rs b/src/hashes/ternary/kerl/bigint/macros.rs index 042ec737..124fe808 100644 --- a/src/hashes/ternary/kerl/bigint/macros.rs +++ b/src/hashes/ternary/kerl/bigint/macros.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2021 IOTA Stiftung +// Copyright 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 macro_rules! def_and_impl_ternary { @@ -126,24 +126,20 @@ macro_rules! def_and_impl_ternary { impl Ord for $ident { fn cmp(&self, other: &Self) -> Ordering { - match self.partial_cmp(other) { - Some(ordering) => ordering, - // Cannot be reached because the order is total. - None => unreachable!(), - } - } - } - - impl PartialOrd for $ident { - fn partial_cmp(&self, other: &Self) -> Option { use Ordering::Equal; for (a, b) in self.0.iter().zip(other.0.iter()).rev() { match a.cmp(&b) { Equal => continue, - other_ordering => return Some(other_ordering), + other_ordering => return other_ordering, } } - Some(Equal) + Equal + } + } + + impl PartialOrd for $ident { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) } } }; diff --git a/src/hashes/ternary/kerl/bigint/u384/mod.rs b/src/hashes/ternary/kerl/bigint/u384/mod.rs index 9805eb72..e479d6ee 100644 --- a/src/hashes/ternary/kerl/bigint/u384/mod.rs +++ b/src/hashes/ternary/kerl/bigint/u384/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2021 IOTA Stiftung +// Copyright 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 //! This module contains unsigned integers encoded by 384 bits. @@ -431,29 +431,25 @@ impl PartialEq for U384 { impl PartialOrd for U384 { fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for U384 { + fn cmp(&self, other: &Self) -> Ordering { use Ordering::*; let zipped_iter = self.inner.iter().rev().zip(other.inner.iter().rev()); for (s, o) in zipped_iter { match s.cmp(o) { - Ordering::Greater => return Some(Greater), - Ordering::Less => return Some(Less), + Ordering::Greater => return Greater, + Ordering::Less => return Less, Ordering::Equal => continue, } } - Some(Equal) - } -} - -impl Ord for U384 { - fn cmp(&self, other: &Self) -> Ordering { - match self.partial_cmp(other) { - Some(ordering) => ordering, - // The ordering is total, hence `partial_cmp` will never return `None`. - None => unreachable!(), - } + Equal } } diff --git a/src/keys/age.rs b/src/keys/age.rs index 270e9b75..5ea25e50 100644 --- a/src/keys/age.rs +++ b/src/keys/age.rs @@ -1,4 +1,4 @@ -// Copyright 2023 IOTA Stiftung +// Copyright 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 // https://age-encryption.org/v1 @@ -629,8 +629,7 @@ pub fn enc_vec( nonce: &[u8; 16], plaintext: &[u8], ) -> Vec { - let mut age = Vec::new(); - age.resize(enc_len(work_factor, plaintext.len()), 0_u8); + let mut age = vec![0u8; enc_len(work_factor, plaintext.len())]; let h = enc_header(password, salt, file_key, work_factor.0, &mut age[..]); enc_payload(file_key, nonce, plaintext, &mut age[h..]); age @@ -703,8 +702,7 @@ pub fn decrypt_vec(password: &[u8], max_work_factor: u8, age: &[u8]) -> Result &str { - // SAFETY: MnemonicRef is represented exactly as str due to repr(transparent) - unsafe { core::mem::transmute(self) } + &self.0 } } diff --git a/src/keys/slip10.rs b/src/keys/slip10.rs index 9299f684..cacf01f2 100644 --- a/src/keys/slip10.rs +++ b/src/keys/slip10.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 IOTA Stiftung +// Copyright 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 #![allow(clippy::from_over_into)] @@ -119,8 +119,8 @@ pub mod secp256k1 { let sk_bytes: &[u8; 32] = unsafe { &*(key_bytes[1..].as_ptr() as *const [u8; 32]) }; if let Ok(sk_delta) = k256::SecretKey::from_bytes(sk_bytes.into()) { - let sk = k256::SecretKey::from_bytes((&parent_key[1..]).try_into().unwrap()) - .expect("valid Secp256k1 parent secret key"); + let sk = + k256::SecretKey::from_bytes((&parent_key[1..]).into()).expect("valid Secp256k1 parent secret key"); let scalar_delta = sk_delta.to_nonzero_scalar(); let mut scalar = *sk.to_nonzero_scalar().as_ref(); diff --git a/src/signatures/ed25519.rs b/src/signatures/ed25519.rs index 61758d45..c88fb671 100644 --- a/src/signatures/ed25519.rs +++ b/src/signatures/ed25519.rs @@ -1,4 +1,4 @@ -// Copyright 2020 IOTA Stiftung +// Copyright 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 use core::{ @@ -136,6 +136,92 @@ impl Hash for PublicKey { } } +#[derive(Copy, Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct PublicKeyBytes(ed25519_zebra::VerificationKeyBytes); + +impl PublicKeyBytes { + pub const LENGTH: usize = PublicKey::LENGTH; + + pub fn verify(&self, sig: &Signature, msg: &[u8]) -> crate::Result { + Ok(self.into_public_key()?.verify(sig, msg)) + } + + pub fn into_public_key(self) -> crate::Result { + self.0 + .try_into() + .map_err(|_| crate::Error::ConvertError { + from: "Ed25519 public key bytes", + to: "Ed25519 public key", + }) + .map(PublicKey) + } + + pub fn as_slice(&self) -> &[u8] { + self.0.as_ref() + } + + pub fn to_bytes(self) -> [u8; PublicKey::LENGTH] { + self.0.into() + } + + pub fn from_bytes(bytes: [u8; PublicKey::LENGTH]) -> Self { + Self(ed25519_zebra::VerificationKeyBytes::from(bytes)) + } +} + +impl TryFrom for PublicKey { + type Error = crate::Error; + + fn try_from(value: PublicKeyBytes) -> Result { + value.into_public_key() + } +} + +impl AsRef<[u8]> for PublicKeyBytes { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl From<[u8; PublicKey::LENGTH]> for PublicKeyBytes { + fn from(bytes: [u8; PublicKey::LENGTH]) -> Self { + Self::from_bytes(bytes) + } +} + +impl From for [u8; PublicKey::LENGTH] { + fn from(pk: PublicKeyBytes) -> Self { + pk.to_bytes() + } +} + +impl PartialEq for PublicKeyBytes { + fn eq(&self, other: &Self) -> bool { + self.as_slice() == other.as_slice() + } +} + +impl Eq for PublicKeyBytes {} + +impl PartialOrd for PublicKeyBytes { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for PublicKeyBytes { + fn cmp(&self, other: &Self) -> Ordering { + self.as_slice().cmp(other.as_slice()) + } +} + +impl Hash for PublicKeyBytes { + fn hash(&self, state: &mut H) { + (self.as_slice()).hash(state); + } +} + #[derive(Copy, Clone, Debug, Eq, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Signature(ed25519_zebra::Signature); diff --git a/tests/ed25519.rs b/tests/ed25519.rs index 403090fc..3f3ef483 100644 --- a/tests/ed25519.rs +++ b/tests/ed25519.rs @@ -1,4 +1,4 @@ -// Copyright 2021 IOTA Stiftung +// Copyright 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 #![cfg(feature = "ed25519")] @@ -9,7 +9,7 @@ pub const SECRET_KEY_LENGTH: usize = 32; pub const PUBLIC_KEY_LENGTH: usize = 32; pub const SIGNATURE_LENGTH: usize = 64; -use crypto::signatures::ed25519::{PublicKey, SecretKey, Signature}; +use crypto::signatures::ed25519::{PublicKey, PublicKeyBytes, SecretKey, Signature}; #[test] fn test_zip215() -> crypto::Result<()> { @@ -25,12 +25,15 @@ fn test_zip215() -> crypto::Result<()> { let mut pkb = [0; PUBLIC_KEY_LENGTH]; hex::decode_to_slice(tv.public_key, &mut pkb as &mut [u8]).unwrap(); let pk = PublicKey::try_from_bytes(pkb)?; + let pkb = PublicKeyBytes::from_bytes(pkb); let mut sigb = [0; SIGNATURE_LENGTH]; hex::decode_to_slice(tv.signature, &mut sigb as &mut [u8]).unwrap(); let sig = Signature::from_bytes(sigb); - assert!(PublicKey::verify(&pk, &sig, ms)); + assert!(pk.verify(&sig, ms)); + assert!(pkb.verify(&sig, ms)?); + assert_eq!(pkb.into_public_key()?, pk); } Ok(()) @@ -56,8 +59,11 @@ fn test_malleability() -> crypto::Result<()> { 0x42, 0x0f, 0x88, 0x34, 0xb1, 0x08, 0xc3, 0xbd, 0xae, 0x36, 0x9e, 0xf5, 0x49, 0xfa, ]; let pk = PublicKey::try_from_bytes(pkb)?; + let pkb = PublicKeyBytes::from_bytes(pkb); - assert!(!PublicKey::verify(&pk, &sig, &ms)); + assert!(!pk.verify(&sig, &ms)); + assert!(!pkb.verify(&sig, &ms)?); + assert_eq!(pkb.into_public_key()?, pk); Ok(()) }