iuna

iuna

iuna - experimental mainnet-candidate protocol
git clone https://getiuna.org/git/iuna.git
Log | Files | Refs | README | LICENSE

signature.rs (11455B)


      1 use anyhow::{Context, Result, bail};
      2 use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
      3 use ml_dsa::{
      4     EncodedVerifyingKey, Keypair, MlDsa44, Seed, Signature as MlDsaSignature,
      5     SigningKey as MlDsaSigningKey, VerifyingKey as MlDsaVerifyingKey,
      6 };
      7 use secrecy::zeroize::Zeroize;
      8 
      9 /// Signature schemes understood by the protocol implementation.
     10 ///
     11 /// Only `Ed25519` is consensus-active today. The other identifiers reserve a
     12 /// stable vocabulary for the post-quantum migration; accepting either of them
     13 /// requires a separately activated transaction and address format.
     14 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
     15 #[repr(u8)]
     16 pub enum SignatureScheme {
     17     Ed25519 = 0,
     18     MlDsa44 = 1,
     19     HybridEd25519MlDsa44 = 2,
     20 }
     21 
     22 #[derive(Clone, Debug, Eq, PartialEq)]
     23 pub struct ProtocolPublicKey {
     24     scheme: SignatureScheme,
     25     bytes: Vec<u8>,
     26 }
     27 
     28 #[derive(Clone, Debug, Eq, PartialEq)]
     29 pub struct ProtocolSignature {
     30     scheme: SignatureScheme,
     31     bytes: Vec<u8>,
     32 }
     33 
     34 impl SignatureScheme {
     35     pub const fn wire_id(self) -> u8 {
     36         self as u8
     37     }
     38 
     39     pub const fn public_key_bytes(self) -> usize {
     40         match self {
     41             Self::Ed25519 => 32,
     42             Self::MlDsa44 => 1_312,
     43             Self::HybridEd25519MlDsa44 => 32 + 1_312,
     44         }
     45     }
     46 
     47     pub const fn signature_bytes(self) -> usize {
     48         match self {
     49             Self::Ed25519 => 64,
     50             Self::MlDsa44 => 2_420,
     51             Self::HybridEd25519MlDsa44 => 64 + 2_420,
     52         }
     53     }
     54 
     55     pub const fn from_wire_id(id: u8) -> Option<Self> {
     56         match id {
     57             0 => Some(Self::Ed25519),
     58             1 => Some(Self::MlDsa44),
     59             2 => Some(Self::HybridEd25519MlDsa44),
     60             _ => None,
     61         }
     62     }
     63 
     64     pub const fn is_consensus_active(self) -> bool {
     65         matches!(self, Self::Ed25519)
     66     }
     67 }
     68 
     69 impl ProtocolPublicKey {
     70     pub fn new(scheme: SignatureScheme, bytes: Vec<u8>) -> Result<Self> {
     71         validate_material_length("public key", scheme.public_key_bytes(), bytes.len())?;
     72         Ok(Self { scheme, bytes })
     73     }
     74 
     75     pub fn scheme(&self) -> SignatureScheme {
     76         self.scheme
     77     }
     78 
     79     pub fn as_bytes(&self) -> &[u8] {
     80         &self.bytes
     81     }
     82 
     83     pub fn encode(&self) -> Vec<u8> {
     84         encode_material(self.scheme, &self.bytes)
     85     }
     86 
     87     pub fn decode(encoded: &[u8]) -> Result<Self> {
     88         let (scheme, bytes) = decode_material(encoded)?;
     89         Self::new(scheme, bytes.to_vec())
     90     }
     91 }
     92 
     93 impl ProtocolSignature {
     94     pub fn new(scheme: SignatureScheme, bytes: Vec<u8>) -> Result<Self> {
     95         validate_material_length("signature", scheme.signature_bytes(), bytes.len())?;
     96         Ok(Self { scheme, bytes })
     97     }
     98 
     99     pub fn scheme(&self) -> SignatureScheme {
    100         self.scheme
    101     }
    102 
    103     pub fn as_bytes(&self) -> &[u8] {
    104         &self.bytes
    105     }
    106 
    107     pub fn encode(&self) -> Vec<u8> {
    108         encode_material(self.scheme, &self.bytes)
    109     }
    110 
    111     pub fn decode(encoded: &[u8]) -> Result<Self> {
    112         let (scheme, bytes) = decode_material(encoded)?;
    113         Self::new(scheme, bytes.to_vec())
    114     }
    115 }
    116 
    117 fn encode_material(scheme: SignatureScheme, bytes: &[u8]) -> Vec<u8> {
    118     let mut encoded = Vec::with_capacity(5 + bytes.len());
    119     encoded.push(scheme.wire_id());
    120     encoded.extend_from_slice(&(bytes.len() as u32).to_be_bytes());
    121     encoded.extend_from_slice(bytes);
    122     encoded
    123 }
    124 
    125 fn decode_material(encoded: &[u8]) -> Result<(SignatureScheme, &[u8])> {
    126     let (&scheme, encoded) = encoded
    127         .split_first()
    128         .context("signature material is empty")?;
    129     let scheme = SignatureScheme::from_wire_id(scheme)
    130         .with_context(|| format!("unknown signature scheme {scheme}"))?;
    131     let (length, bytes) = encoded
    132         .split_at_checked(4)
    133         .context("signature material length is missing")?;
    134     let declared = u32::from_be_bytes(length.try_into().expect("four-byte length")) as usize;
    135     if bytes.len() != declared {
    136         bail!(
    137             "signature material declares {declared} bytes but contains {}",
    138             bytes.len()
    139         );
    140     }
    141     Ok((scheme, bytes))
    142 }
    143 
    144 fn validate_material_length(label: &str, expected: usize, actual: usize) -> Result<()> {
    145     if actual != expected {
    146         bail!("{label} must contain {expected} bytes, got {actual}");
    147     }
    148     Ok(())
    149 }
    150 
    151 pub(crate) fn ed25519_public_key(signing_seed: &[u8; 32]) -> [u8; 32] {
    152     SigningKey::from_bytes(signing_seed)
    153         .verifying_key()
    154         .to_bytes()
    155 }
    156 
    157 pub(crate) fn sign_ed25519(signing_seed: &[u8; 32], payload: &[u8]) -> [u8; 64] {
    158     SigningKey::from_bytes(signing_seed)
    159         .sign(payload)
    160         .to_bytes()
    161 }
    162 
    163 pub(crate) fn validate_ed25519_public_key(public_key: &[u8; 32]) -> Result<()> {
    164     let verifying_key = VerifyingKey::from_bytes(public_key)
    165         .context("address payload is not a valid Ed25519 verifying key")?;
    166     if verifying_key.is_weak() {
    167         bail!("address payload contains a weak Ed25519 verifying key");
    168     }
    169     Ok(())
    170 }
    171 
    172 pub(crate) fn verify_ed25519(
    173     public_key: &[u8; 32],
    174     payload: &[u8],
    175     signature: &[u8; 64],
    176     label: &str,
    177 ) -> Result<()> {
    178     let verifying_key = VerifyingKey::from_bytes(public_key)
    179         .with_context(|| format!("invalid {label} public key"))?;
    180     verifying_key
    181         .verify(payload, &Signature::from_bytes(signature))
    182         .with_context(|| format!("{label} signature is invalid"))
    183 }
    184 
    185 pub(crate) fn verify_ml_dsa44(
    186     public_key: &[u8; 1_312],
    187     payload: &[u8],
    188     signature: &[u8; 2_420],
    189     label: &str,
    190 ) -> Result<()> {
    191     verify_ml_dsa44_with_context(public_key, payload, &[], signature, label)
    192 }
    193 
    194 fn verify_ml_dsa44_with_context(
    195     public_key: &[u8; 1_312],
    196     payload: &[u8],
    197     context: &[u8],
    198     signature: &[u8; 2_420],
    199     label: &str,
    200 ) -> Result<()> {
    201     let encoded_public_key = EncodedVerifyingKey::<MlDsa44>::try_from(public_key.as_slice())
    202         .with_context(|| format!("invalid {label} ML-DSA-44 public key length"))?;
    203     let public_key = MlDsaVerifyingKey::<MlDsa44>::decode(&encoded_public_key);
    204     let signature = MlDsaSignature::<MlDsa44>::try_from(signature.as_slice())
    205         .with_context(|| format!("invalid {label} ML-DSA-44 signature encoding"))?;
    206     if !public_key.verify_with_context(payload, context, &signature) {
    207         bail!("{label} ML-DSA-44 signature is invalid");
    208     }
    209     Ok(())
    210 }
    211 
    212 pub(crate) fn ml_dsa44_public_key(signing_seed: &[u8; 32]) -> [u8; 1_312] {
    213     let signing_key = ml_dsa44_signing_key(signing_seed);
    214     signing_key
    215         .verifying_key()
    216         .encode()
    217         .as_slice()
    218         .try_into()
    219         .expect("ML-DSA-44 public key has a fixed 1,312-byte encoding")
    220 }
    221 
    222 pub(crate) fn sign_ml_dsa44(signing_seed: &[u8; 32], payload: &[u8]) -> Result<[u8; 2_420]> {
    223     let signing_key = ml_dsa44_signing_key(signing_seed);
    224     let signature = signing_key
    225         .expanded_key()
    226         .sign_deterministic(payload, &[])
    227         .context("failed to create deterministic ML-DSA-44 signature")?;
    228     Ok(signature
    229         .encode()
    230         .as_slice()
    231         .try_into()
    232         .expect("ML-DSA-44 signature has a fixed 2,420-byte encoding"))
    233 }
    234 
    235 fn ml_dsa44_signing_key(signing_seed: &[u8; 32]) -> MlDsaSigningKey<MlDsa44> {
    236     let mut seed = Seed::from(*signing_seed);
    237     let signing_key = MlDsaSigningKey::<MlDsa44>::from_seed(&seed);
    238     seed.zeroize();
    239     signing_key
    240 }
    241 
    242 #[cfg(test)]
    243 mod tests {
    244     use serde::Deserialize;
    245 
    246     use super::{
    247         ProtocolPublicKey, ProtocolSignature, SignatureScheme, ed25519_public_key, sign_ed25519,
    248         verify_ed25519, verify_ml_dsa44_with_context,
    249     };
    250     use crate::domain::hex::decode_hex;
    251 
    252     #[derive(Deserialize)]
    253     struct MlDsaAuditFile {
    254         vectors: Vec<MlDsaAuditVector>,
    255     }
    256 
    257     #[derive(Deserialize)]
    258     struct MlDsaAuditVector {
    259         source: String,
    260         tc_id: u64,
    261         comment: String,
    262         expected_valid: bool,
    263         public_key: String,
    264         message: String,
    265         context: String,
    266         signature: String,
    267     }
    268 
    269     #[test]
    270     fn signature_scheme_ids_and_sizes_are_stable() {
    271         assert_eq!(SignatureScheme::Ed25519.wire_id(), 0);
    272         assert_eq!(SignatureScheme::MlDsa44.wire_id(), 1);
    273         assert_eq!(SignatureScheme::HybridEd25519MlDsa44.wire_id(), 2);
    274         assert_eq!(SignatureScheme::Ed25519.public_key_bytes(), 32);
    275         assert_eq!(SignatureScheme::Ed25519.signature_bytes(), 64);
    276         assert_eq!(SignatureScheme::MlDsa44.public_key_bytes(), 1_312);
    277         assert_eq!(SignatureScheme::MlDsa44.signature_bytes(), 2_420);
    278         assert_eq!(
    279             SignatureScheme::HybridEd25519MlDsa44.public_key_bytes(),
    280             1_344
    281         );
    282         assert_eq!(
    283             SignatureScheme::HybridEd25519MlDsa44.signature_bytes(),
    284             2_484
    285         );
    286         assert!(SignatureScheme::Ed25519.is_consensus_active());
    287         assert!(!SignatureScheme::MlDsa44.is_consensus_active());
    288         assert_eq!(SignatureScheme::from_wire_id(3), None);
    289     }
    290 
    291     #[test]
    292     fn centralized_ed25519_backend_signs_and_verifies() {
    293         let seed = [7_u8; 32];
    294         let public_key = ed25519_public_key(&seed);
    295         let signature = sign_ed25519(&seed, b"quantum-agility-test");
    296 
    297         verify_ed25519(&public_key, b"quantum-agility-test", &signature, "test").unwrap();
    298         assert!(verify_ed25519(&public_key, b"tampered", &signature, "test").is_err());
    299     }
    300 
    301     #[test]
    302     fn ml_dsa44_matches_pinned_nist_and_wycheproof_vectors() {
    303         let audit: MlDsaAuditFile =
    304             serde_json::from_str(include_str!("../../tests/vectors/ml_dsa44_audit.json")).unwrap();
    305 
    306         for vector in audit.vectors {
    307             let public_key: [u8; 1_312] =
    308                 decode_hex(&vector.public_key).unwrap().try_into().unwrap();
    309             let message = decode_hex(&vector.message).unwrap();
    310             let context = decode_hex(&vector.context).unwrap();
    311             let signature: [u8; 2_420] = decode_hex(&vector.signature).unwrap().try_into().unwrap();
    312             let result = verify_ml_dsa44_with_context(
    313                 &public_key,
    314                 &message,
    315                 &context,
    316                 &signature,
    317                 "audit vector",
    318             );
    319 
    320             assert_eq!(
    321                 result.is_ok(),
    322                 vector.expected_valid,
    323                 "{} tcId {}: {}",
    324                 vector.source,
    325                 vector.tc_id,
    326                 vector.comment
    327             );
    328         }
    329     }
    330 
    331     #[test]
    332     fn algorithm_tagged_material_roundtrips_and_rejects_malformed_lengths() {
    333         let key = ProtocolPublicKey::new(SignatureScheme::MlDsa44, vec![5; 1_312]).unwrap();
    334         let signature =
    335             ProtocolSignature::new(SignatureScheme::HybridEd25519MlDsa44, vec![9; 2_484]).unwrap();
    336 
    337         assert_eq!(ProtocolPublicKey::decode(&key.encode()).unwrap(), key);
    338         assert_eq!(
    339             ProtocolSignature::decode(&signature.encode()).unwrap(),
    340             signature
    341         );
    342 
    343         let mut unknown_scheme = key.encode();
    344         unknown_scheme[0] = 99;
    345         assert!(ProtocolPublicKey::decode(&unknown_scheme).is_err());
    346 
    347         let mut wrong_declared_length = signature.encode();
    348         wrong_declared_length[4] -= 1;
    349         assert!(ProtocolSignature::decode(&wrong_declared_length).is_err());
    350         assert!(
    351             ProtocolSignature::new(SignatureScheme::Ed25519, vec![0; 63]).is_err(),
    352             "scheme-specific lengths must fail closed"
    353         );
    354     }
    355 }