Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions src/lib/src/pqc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,25 @@ pub struct HybridSignature {
pub message_hash: Vec<u8>,
}

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")]
Expand Down Expand Up @@ -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());
}
}
75 changes: 75 additions & 0 deletions src/lib/src/wasm_module/component.rs
Original file line number Diff line number Diff line change
@@ -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<Self> {
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());
}
}
97 changes: 97 additions & 0 deletions src/lib/src/wasm_module/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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));
}
}
Loading