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 }