diff --git a/src/lib/src/pqc.rs b/src/lib/src/pqc.rs index 9a8bcfb..7088992 100644 --- a/src/lib/src/pqc.rs +++ b/src/lib/src/pqc.rs @@ -141,6 +141,25 @@ pub struct HybridSignature { pub message_hash: Vec, } +impl HybridSignature { + /// Check that both signature components are present and well-formed. + /// This is a structural precondition — actual cryptographic verification + /// must call the underlying Ed25519 and SLH-DSA verify functions. + pub fn is_complete(&self) -> bool { + !self.classical.is_empty() + && !self.post_quantum.is_empty() + && self.classical.len() == 64 // Ed25519 signature size + && self.post_quantum.len() <= self.pqc_algorithm.max_signature_size() + && !self.message_hash.is_empty() + } + + /// Domain separator for hybrid signatures. + /// Applied to the message before signing with each algorithm independently. + pub fn domain_separator() -> &'static [u8] { + b"wsc-hybrid-v1" + } +} + /// Configuration for PQC signing operations. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -299,4 +318,65 @@ mod tests { deduped.dedup(); assert_eq!(ids.len(), deduped.len()); } + + #[test] + fn test_hybrid_signature_is_complete() { + let complete = HybridSignature { + classical: vec![0u8; 64], + post_quantum: vec![0u8; 100], + pqc_algorithm: PqcAlgorithm::SlhDsaSha2_128s, + message_hash: vec![0u8; 32], + }; + assert!(complete.is_complete()); + } + + #[test] + fn test_hybrid_signature_incomplete_missing_pqc() { + let missing_pqc = HybridSignature { + classical: vec![0u8; 64], + post_quantum: vec![], + pqc_algorithm: PqcAlgorithm::SlhDsaSha2_128s, + message_hash: vec![0u8; 32], + }; + assert!(!missing_pqc.is_complete()); + } + + #[test] + fn test_hybrid_signature_incomplete_missing_classical() { + let missing_classical = HybridSignature { + classical: vec![], + post_quantum: vec![0u8; 100], + pqc_algorithm: PqcAlgorithm::SlhDsaSha2_128s, + message_hash: vec![0u8; 32], + }; + assert!(!missing_classical.is_complete()); + } + + #[test] + fn test_hybrid_signature_incomplete_wrong_classical_size() { + let wrong_size = HybridSignature { + classical: vec![0u8; 32], // Wrong: Ed25519 sigs are 64 bytes + post_quantum: vec![0u8; 100], + pqc_algorithm: PqcAlgorithm::SlhDsaSha2_128s, + message_hash: vec![0u8; 32], + }; + assert!(!wrong_size.is_complete()); + } + + #[test] + fn test_hybrid_signature_incomplete_empty_hash() { + let no_hash = HybridSignature { + classical: vec![0u8; 64], + post_quantum: vec![0u8; 100], + pqc_algorithm: PqcAlgorithm::SlhDsaSha2_128s, + message_hash: vec![], + }; + assert!(!no_hash.is_complete()); + } + + #[test] + fn test_hybrid_domain_separator() { + assert_eq!(HybridSignature::domain_separator(), b"wsc-hybrid-v1"); + assert!(!HybridSignature::domain_separator().is_empty()); + } } diff --git a/src/lib/src/wasm_module/component.rs b/src/lib/src/wasm_module/component.rs new file mode 100644 index 0000000..a8b2568 --- /dev/null +++ b/src/lib/src/wasm_module/component.rs @@ -0,0 +1,75 @@ +//! WASM Component Model section types (informational). +//! +//! The component binary format uses section IDs in the same range as +//! core modules but with different semantics. The core parser handles +//! these via `SectionId::Extension(u8)` which is sufficient for signing +//! since the hash covers all bytes regardless of section type. + +/// Component model section IDs per the WASM Component Model binary spec. +/// These are not used in the signing path (`Extension(u8)` handles them) +/// but are documented here for reference and future validation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum ComponentSectionId { + Custom = 0x00, + CoreModule = 0x01, + CoreInstance = 0x02, + CoreType = 0x03, + Component = 0x04, + Instance = 0x05, + Alias = 0x06, + Type = 0x07, + Canon = 0x08, + Start = 0x09, + Import = 0x0A, + Export = 0x0B, +} + +impl ComponentSectionId { + pub fn from_u8(id: u8) -> Option { + match id { + 0x00 => Some(Self::Custom), + 0x01 => Some(Self::CoreModule), + 0x02 => Some(Self::CoreInstance), + 0x03 => Some(Self::CoreType), + 0x04 => Some(Self::Component), + 0x05 => Some(Self::Instance), + 0x06 => Some(Self::Alias), + 0x07 => Some(Self::Type), + 0x08 => Some(Self::Canon), + 0x09 => Some(Self::Start), + 0x0A => Some(Self::Import), + 0x0B => Some(Self::Export), + _ => None, + } + } + + pub fn is_nested(&self) -> bool { + matches!(self, Self::CoreModule | Self::Component) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_component_section_ids() { + assert_eq!( + ComponentSectionId::from_u8(0x01), + Some(ComponentSectionId::CoreModule) + ); + assert_eq!( + ComponentSectionId::from_u8(0x04), + Some(ComponentSectionId::Component) + ); + assert_eq!(ComponentSectionId::from_u8(0xFF), None); + } + + #[test] + fn test_nested_detection() { + assert!(ComponentSectionId::CoreModule.is_nested()); + assert!(ComponentSectionId::Component.is_nested()); + assert!(!ComponentSectionId::Export.is_nested()); + } +} diff --git a/src/lib/src/wasm_module/mod.rs b/src/lib/src/wasm_module/mod.rs index 6e65281..c9e595b 100644 --- a/src/lib/src/wasm_module/mod.rs +++ b/src/lib/src/wasm_module/mod.rs @@ -4,6 +4,13 @@ /// integers in the LEB128 format used by WebAssembly modules. pub mod varint; +/// WASM Component Model section types (informational). +/// +/// Documents the component model section IDs for reference. The core parser +/// handles component sections via `SectionId::Extension(u8)`, which is +/// sufficient for signing since the hash covers all bytes. +pub mod component; + use crate::signature::*; use ct_codecs::{Encoder, Hex}; @@ -874,4 +881,94 @@ mod tests { assert_eq!(custom.name(), ""); assert_eq!(custom.payload(), &[] as &[u8]); } + + #[test] + fn test_component_model_sign_verify() { + use crate::KeyPair; + + // Build a minimal component: header + one custom section + let mut component = Vec::new(); + component.extend_from_slice(&WASM_COMPONENT_HEADER); + // Add a custom section (id=0, name="test", content="hello") + let section_name = b"test"; + let section_content = b"hello"; + let section_payload_len = 1 + section_name.len() + section_content.len(); + component.push(0x00); // custom section id + // varint encode payload length + component.push(section_payload_len as u8); + component.push(section_name.len() as u8); + component.extend_from_slice(section_name); + component.extend_from_slice(section_content); + + let mut reader = io::Cursor::new(&component); + let module = Module::deserialize(&mut reader).expect("should parse component"); + assert_eq!(module.header, WASM_COMPONENT_HEADER); + + let kp = KeyPair::generate(); + let signed = kp.sk.sign(module, None).expect("should sign component"); + + // Serialize and verify + let mut signed_bytes = Vec::new(); + signed + .serialize(&mut signed_bytes) + .expect("should serialize"); + let mut verify_reader = io::Cursor::new(&signed_bytes); + assert!( + kp.pk.verify(&mut verify_reader, None).is_ok(), + "should verify component signature" + ); + } + + #[test] + fn test_component_model_tamper_detection() { + use crate::KeyPair; + + let mut component = Vec::new(); + component.extend_from_slice(&WASM_COMPONENT_HEADER); + let section_name = b"data"; + let section_content = b"important content"; + let section_payload_len = 1 + section_name.len() + section_content.len(); + component.push(0x00); + component.push(section_payload_len as u8); + component.push(section_name.len() as u8); + component.extend_from_slice(section_name); + component.extend_from_slice(section_content); + + let mut reader = io::Cursor::new(&component); + let module = Module::deserialize(&mut reader).expect("should parse component"); + let kp = KeyPair::generate(); + let signed = kp.sk.sign(module, None).expect("should sign"); + + // Serialize, tamper, re-verify + let mut bytes = Vec::new(); + signed.serialize(&mut bytes).expect("should serialize"); + // Find and modify content byte + if let Some(pos) = bytes + .windows(b"important".len()) + .position(|w| w == b"important") + { + bytes[pos] = b'X'; // tamper + } + let mut tampered_reader = io::Cursor::new(&bytes); + assert!( + kp.pk.verify(&mut tampered_reader, None).is_err(), + "tampered component must fail verification" + ); + } +} + +// ============================================================================ +// Kani proof harnesses for WASM module/component headers +// ============================================================================ +#[cfg(kani)] +mod component_proofs { + use super::*; + + #[kani::proof] + fn proof_component_module_header_mutual_exclusivity() { + let b: [u8; 8] = kani::any(); + let is_module = b == WASM_HEADER; + let is_component = b == WASM_COMPONENT_HEADER; + assert!(!(is_module && is_component)); + } }