diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 426e652..54ed9f7 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -45,7 +45,45 @@ "Bash(git grep:*)", "Bash(find:*)", "Bash(UUID=\"108e9186e8c5677a1b77086cce5d81d1fed81432617971b2c6993681aced1a044c89465e8c60fe20\" curl -s \"https://rekor.sigstore.dev/api/v1/log/entries/$UUID\")", - "Bash(git rm:*)" + "Bash(git rm:*)", + "Bash(awk '{print $2}')", + "Bash(git push *)", + "Bash(gh pr *)", + "Bash(git pull *)", + "Bash(git tag -a v0.8.0 -m 'Release v0.8.0 *)", + "WebFetch(domain:red.anthropic.com)", + "WebFetch(domain:blog.vidocsecurity.com)", + "Bash(cargo kani *)", + "Bash(gh search *)", + "Bash(git stash *)", + "Bash(rivet validate *)", + "Bash(git -C /Users/r/git/pulseengine/kiln status)", + "Bash(rivet list *)", + "Bash(rivet --version)", + "Bash(pip3 install *)", + "Bash(rivet show *)", + "Bash(rivet --help)", + "Bash(rivet get *)", + "Bash(rivet sync *)", + "Bash(rivet query *)", + "Bash(rivet docs *)", + "Bash(rivet coverage *)", + "Bash(rivet add *)", + "Bash(rivet link *)", + "WebFetch(domain:medium.com)", + "Bash(zola build *)", + "WebFetch(domain:ferrous-systems.com)", + "Bash(git -C /Users/r/git/pulseengine/sigil log --oneline --since=\"2025-01-23\")", + "Bash(rivet init *)", + "Bash(rivet stats *)", + "Bash(cargo check *)", + "Bash(rivet schema *)", + "Bash(cargo fmt *)", + "Bash(git branch *)", + "Bash(git reset *)", + "WebFetch(domain:arxiv.org)", + "Bash(rivet next-id *)", + "Bash(rivet validate *)" ], "deny": [], "ask": [] diff --git a/src/attestation/src/dsse.rs b/src/attestation/src/dsse.rs index 42a73ae..d1d299b 100644 --- a/src/attestation/src/dsse.rs +++ b/src/attestation/src/dsse.rs @@ -71,7 +71,10 @@ impl DsseEnvelope { } /// Create a DSSE envelope from a JSON-serializable payload - pub fn from_payload(payload: &T, payload_type: impl Into) -> Result { + pub fn from_payload( + payload: &T, + payload_type: impl Into, + ) -> Result { let json = serde_json::to_vec(payload)?; Ok(Self::new(&json, payload_type)) } @@ -134,7 +137,9 @@ impl DsseEnvelope { /// Parse the payload as JSON pub fn payload_json Deserialize<'de>>(&self) -> Result { - let bytes = self.payload_bytes().map_err(|e| DsseError::DecodeError(e.to_string()))?; + let bytes = self + .payload_bytes() + .map_err(|e| DsseError::DecodeError(e.to_string()))?; serde_json::from_slice(&bytes).map_err(|e| DsseError::JsonError(e.to_string())) } @@ -163,7 +168,10 @@ impl DsseEnvelope { /// Verify all signatures in the envelope #[cfg(feature = "signing")] - pub fn verify_ed25519(&self, public_key: &ed25519_compact::PublicKey) -> Result { + pub fn verify_ed25519( + &self, + public_key: &ed25519_compact::PublicKey, + ) -> Result { use base64::Engine; if self.signatures.is_empty() { @@ -180,7 +188,8 @@ impl DsseEnvelope { let signature = ed25519_compact::Signature::from_slice(&sig_bytes) .map_err(|e| DsseError::InvalidSignature(e.to_string()))?; - public_key.verify(&pae, &signature) + public_key + .verify(&pae, &signature) .map_err(|_| DsseError::VerificationFailed)?; } @@ -365,10 +374,7 @@ impl ResourceDescriptor { } /// Create for a git source - pub fn git_source( - repo_url: impl Into, - commit: impl Into, - ) -> Self { + pub fn git_source(repo_url: impl Into, commit: impl Into) -> Self { let commit = commit.into(); let mut digest = std::collections::HashMap::new(); digest.insert("gitCommit".to_string(), commit.clone()); @@ -515,7 +521,9 @@ mod tests { message: String, } - let payload = TestPayload { message: "test".to_string() }; + let payload = TestPayload { + message: "test".to_string(), + }; let envelope = DsseEnvelope::from_payload(&payload, "application/json").unwrap(); assert_eq!(envelope.payload_type, "application/json"); @@ -532,7 +540,9 @@ mod tests { tool: String, } - let predicate = TestPredicate { tool: "test-tool".to_string() }; + let predicate = TestPredicate { + tool: "test-tool".to_string(), + }; let mut statement = InTotoStatement::new(WSC_TRANSFORMATION_PREDICATE, &predicate).unwrap(); statement.add_subject("artifact.wasm", "abc123def456"); @@ -541,7 +551,10 @@ mod tests { assert_eq!(statement.predicate_type, WSC_TRANSFORMATION_PREDICATE); assert_eq!(statement.subject.len(), 1); assert_eq!(statement.subject[0].name, "artifact.wasm"); - assert_eq!(statement.subject[0].digest.get("sha256"), Some(&"abc123def456".to_string())); + assert_eq!( + statement.subject[0].digest.get("sha256"), + Some(&"abc123def456".to_string()) + ); } #[test] @@ -551,7 +564,9 @@ mod tests { version: String, } - let predicate = TestPredicate { version: "1.0".to_string() }; + let predicate = TestPredicate { + version: "1.0".to_string(), + }; let mut statement = InTotoStatement::new(WSC_TRANSFORMATION_PREDICATE, &predicate).unwrap(); statement.add_subject("test.wasm", "sha256hash"); @@ -583,8 +598,14 @@ mod tests { .with_digest("sha1", "sha1hash"); assert_eq!(subject.digest.len(), 3); - assert_eq!(subject.digest.get("sha256"), Some(&"sha256hash".to_string())); - assert_eq!(subject.digest.get("sha512"), Some(&"sha512hash".to_string())); + assert_eq!( + subject.digest.get("sha256"), + Some(&"sha256hash".to_string()) + ); + assert_eq!( + subject.digest.get("sha512"), + Some(&"sha512hash".to_string()) + ); } #[cfg(feature = "signing")] diff --git a/src/attestation/src/lib.rs b/src/attestation/src/lib.rs index c076049..739dca8 100644 --- a/src/attestation/src/lib.rs +++ b/src/attestation/src/lib.rs @@ -683,7 +683,11 @@ impl TransformationAttestationBuilder { /// /// Returns the attestation structure. The attestation_signature field /// will have algorithm="unsigned" and empty signature. - pub fn build(self, output_bytes: &[u8], output_name: impl Into) -> TransformationAttestation { + pub fn build( + self, + output_bytes: &[u8], + output_name: impl Into, + ) -> TransformationAttestation { let output_hash = compute_sha256_hash(output_bytes); let timestamp = chrono::Utc::now().to_rfc3339(); let attestation_id = generate_uuid_v4(); @@ -789,7 +793,7 @@ impl TransformationAttestationBuilder { /// Compute SHA-256 hash of bytes and return as hex string fn compute_sha256_hash(bytes: &[u8]) -> String { - use sha2::{Sha256, Digest}; + use sha2::{Digest, Sha256}; let mut hasher = Sha256::new(); hasher.update(bytes); let result = hasher.finalize(); @@ -811,7 +815,9 @@ fn generate_uuid_v4() -> String { u16::from_be_bytes([bytes[4], bytes[5]]), u16::from_be_bytes([bytes[6], bytes[7]]), u16::from_be_bytes([bytes[8], bytes[9]]), - u64::from_be_bytes([0, 0, bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15]]) + u64::from_be_bytes([ + 0, 0, bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15] + ]) ) } @@ -830,11 +836,17 @@ mod tests { .build(output, "output.wasm"); assert_eq!(attestation.version, "1.0"); - assert_eq!(attestation.transformation_type, TransformationType::Optimization); + assert_eq!( + attestation.transformation_type, + TransformationType::Optimization + ); assert_eq!(attestation.tool.name, "loom"); assert_eq!(attestation.tool.version, "0.1.0"); assert_eq!(attestation.inputs.len(), 1); - assert_eq!(attestation.inputs[0].signature_status, SignatureStatus::Unsigned); + assert_eq!( + attestation.inputs[0].signature_status, + SignatureStatus::Unsigned + ); } #[test] @@ -855,8 +867,14 @@ mod tests { #[test] fn test_section_names() { - assert_eq!(TRANSFORMATION_ATTESTATION_SECTION, "wsc.transformation.attestation"); - assert_eq!(TRANSFORMATION_AUDIT_TRAIL_SECTION, "wsc.transformation.audit_trail"); + assert_eq!( + TRANSFORMATION_ATTESTATION_SECTION, + "wsc.transformation.attestation" + ); + assert_eq!( + TRANSFORMATION_AUDIT_TRAIL_SECTION, + "wsc.transformation.audit_trail" + ); } #[test] @@ -886,7 +904,10 @@ mod tests { assert_eq!(attestation.attestation_signature.algorithm, "ed25519"); assert!(!attestation.attestation_signature.signature.is_empty()); assert!(attestation.attestation_signature.public_key.is_some()); - assert_eq!(attestation.attestation_signature.key_id, Some("test-key-id".to_string())); + assert_eq!( + attestation.attestation_signature.key_id, + Some("test-key-id".to_string()) + ); // Verify it can be serialized and deserialized let json = attestation.to_json().unwrap(); @@ -905,12 +926,7 @@ mod tests { let result = TransformationAttestationBuilder::new_optimization("loom", "0.1.0") .add_input_unsigned(input, "input.wasm") - .build_and_sign_ed25519( - output, - "output.wasm", - invalid_key, - None, - ); + .build_and_sign_ed25519(output, "output.wasm", invalid_key, None); assert!(result.is_err()); assert!(result.unwrap_err().contains("Invalid Ed25519 secret key")); diff --git a/src/attestation/src/reproducibility.rs b/src/attestation/src/reproducibility.rs index 7c659e4..def0533 100644 --- a/src/attestation/src/reproducibility.rs +++ b/src/attestation/src/reproducibility.rs @@ -326,10 +326,7 @@ impl BuilderIdentity { return Some(Self { builder_type: "github-actions".to_string(), - builder_id: format!( - "https://github.com/{}/actions/runs/{}", - repo, run_id - ), + builder_id: format!("https://github.com/{}/actions/runs/{}", repo, run_id), workflow: std::env::var("GITHUB_WORKFLOW").ok(), job: std::env::var("GITHUB_JOB").ok(), run_id: Some(run_id), @@ -635,7 +632,11 @@ pub struct DependencyPin { impl DependencyPin { /// Create a new dependency pin - pub fn new(name: impl Into, version: impl Into, source: impl Into) -> Self { + pub fn new( + name: impl Into, + version: impl Into, + source: impl Into, + ) -> Self { Self { name: name.into(), version: version.into(), @@ -650,8 +651,7 @@ impl DependencyPin { /// Create a crates.io dependency pub fn crates_io(name: impl Into, version: impl Into) -> Self { - Self::new(name, version, "crates.io") - .with_registry("https://crates.io") + Self::new(name, version, "crates.io").with_registry("https://crates.io") } /// Create a git dependency @@ -668,7 +668,11 @@ impl DependencyPin { } /// Create a path dependency - pub fn path(name: impl Into, version: impl Into, path: impl Into) -> Self { + pub fn path( + name: impl Into, + version: impl Into, + path: impl Into, + ) -> Self { let mut dep = Self::new(name, version, "path"); dep.path = Some(path.into()); dep @@ -745,10 +749,7 @@ pub struct ReproducibilityVerification { impl ReproducibilityVerification { /// Create a successful verification - pub fn success( - env: BuildEnvironment, - original_hash: impl Into, - ) -> Self { + pub fn success(env: BuildEnvironment, original_hash: impl Into) -> Self { let hash = original_hash.into(); Self { is_reproducible: true, @@ -802,12 +803,15 @@ mod tests { fn test_builder_identity_github_detection() { // Can't easily test actual detection without setting env vars // but we can test the struct creation - let builder = BuilderIdentity::new("github-actions", "https://github.com/org/repo/actions/runs/123") - .with_workflow("CI") - .with_job("build") - .with_run_id("123") - .with_repository("org/repo") - .with_commit_sha("abc123"); + let builder = BuilderIdentity::new( + "github-actions", + "https://github.com/org/repo/actions/runs/123", + ) + .with_workflow("CI") + .with_job("build") + .with_run_id("123") + .with_repository("org/repo") + .with_commit_sha("abc123"); assert_eq!(builder.builder_type, "github-actions"); assert_eq!(builder.workflow, Some("CI".to_string())); @@ -816,8 +820,7 @@ mod tests { #[test] fn test_dependency_pin() { - let dep = DependencyPin::crates_io("serde", "1.0.195") - .with_hash("sha256:abc123"); + let dep = DependencyPin::crates_io("serde", "1.0.195").with_hash("sha256:abc123"); assert_eq!(dep.name, "serde"); assert_eq!(dep.version, "1.0.195"); @@ -835,7 +838,10 @@ mod tests { .add_dependency(DependencyPin::crates_io("tokio", "1.0")) .build(); - assert_eq!(manifest.cargo_lock_hash, Some("sha256:lockfile123".to_string())); + assert_eq!( + manifest.cargo_lock_hash, + Some("sha256:lockfile123".to_string()) + ); assert_eq!(manifest.git_commit, Some("abc123def456".to_string())); assert_eq!(manifest.dependency_count(), 2); } diff --git a/src/cli/docs.rs b/src/cli/docs.rs index c7303c7..6a0978e 100644 --- a/src/cli/docs.rs +++ b/src/cli/docs.rs @@ -6,12 +6,36 @@ /// Available documentation topics pub const TOPICS: &[(&str, &str, &str)] = &[ - ("security", "Security policy and vulnerability reporting", include_str!("../../SECURITY.md")), - ("threat-model", "STPA-Sec threat model overview", include_str!("../../docs/THREAT_MODEL.md")), - ("integration", "Integration guidance for embedders", include_str!("../../docs/security/INTEGRATION_GUIDANCE.md")), - ("slsa", "SLSA compliance documentation", include_str!("../../docs/slsa-compliance.md")), - ("agents", "Agent context for AI-assisted development", include_str!("../../AGENTS.md")), - ("risk", "Risk assessment summary", include_str!("../../docs/security/RISK_ASSESSMENT.md")), + ( + "security", + "Security policy and vulnerability reporting", + include_str!("../../SECURITY.md"), + ), + ( + "threat-model", + "STPA-Sec threat model overview", + include_str!("../../docs/THREAT_MODEL.md"), + ), + ( + "integration", + "Integration guidance for embedders", + include_str!("../../docs/security/INTEGRATION_GUIDANCE.md"), + ), + ( + "slsa", + "SLSA compliance documentation", + include_str!("../../docs/slsa-compliance.md"), + ), + ( + "agents", + "Agent context for AI-assisted development", + include_str!("../../AGENTS.md"), + ), + ( + "risk", + "Risk assessment summary", + include_str!("../../docs/security/RISK_ASSESSMENT.md"), + ), ]; pub fn list_topics() { diff --git a/src/cli/main.rs b/src/cli/main.rs index 7c74e28..43a0d9b 100644 --- a/src/cli/main.rs +++ b/src/cli/main.rs @@ -1,18 +1,18 @@ -use wsc::{BoxedPredicate, KeyPair, Module, PublicKey, PublicKeySet, SecretKey, Section, WSError}; use wsc::airgapped::{ - CertificateAuthority, SignedTrustBundle, TransparencyLog, TrustBundle, - TRUST_BUNDLE_FORMAT_VERSION, - fetch_sigstore_trusted_root, trusted_root_to_bundle, SIGSTORE_TRUSTED_ROOT_URL, + CertificateAuthority, SIGSTORE_TRUSTED_ROOT_URL, SignedTrustBundle, + TRUST_BUNDLE_FORMAT_VERSION, TransparencyLog, TrustBundle, fetch_sigstore_trusted_root, + trusted_root_to_bundle, }; use wsc::audit::{self, AuditConfig, LogDestination}; use wsc::composition::{ - extract_transformation_attestation, extract_all_transformation_attestations, - extract_transformation_audit_trail, embed_transformation_attestation, - TransformationAttestation, TransformationAttestationBuilder, TransformationType, - ChainVerificationPolicy, ChainVerificationMode, TrustedToolInfo, TrustedPublicKey, + ChainVerificationMode, ChainVerificationPolicy, TransformationAttestation, + TransformationAttestationBuilder, TransformationType, TrustedPublicKey, TrustedToolInfo, + embed_transformation_attestation, extract_all_transformation_attestations, + extract_transformation_attestation, extract_transformation_audit_trail, verify_transformation_chain, }; -use wsc::policy::{Policy, Enforcement, evaluate_policy}; +use wsc::policy::{Enforcement, Policy, evaluate_policy}; +use wsc::{BoxedPredicate, KeyPair, Module, PublicKey, PublicKeySet, SecretKey, Section, WSError}; use wsc::reexports::log; @@ -754,7 +754,8 @@ fn start() -> Result<(), WSError> { return Err(WSError::UsageError( "Keyless signing is currently supported only for WASM format. \ Use key-based signing for ELF and MCUboot artifacts.", - ).into()); + ) + .into()); } let sk_file = matches @@ -796,7 +797,8 @@ fn start() -> Result<(), WSError> { println!(" Signature: {}", sig_path); } wsc::format::FormatType::Mcuboot => { - let mut artifact = wsc::format::mcuboot::McubootArtifact::from_file(input_file)?; + let mut artifact = + wsc::format::mcuboot::McubootArtifact::from_file(input_file)?; let hash = artifact.compute_hash()?; println!("Signing MCUboot firmware image..."); println!(" Hash: sha256:{}", hex_hash(&hash)); @@ -819,9 +821,7 @@ fn start() -> Result<(), WSError> { use wsc::keyless::{KeylessConfig, KeylessSigner}; println!("Using keyless signing..."); - let expected_issuer = matches - .get_one::("expected_issuer") - .cloned(); + let expected_issuer = matches.get_one::("expected_issuer").cloned(); let config = KeylessConfig { expected_issuer, ..Default::default() @@ -879,8 +879,12 @@ fn start() -> Result<(), WSError> { // Keyless verification path use wsc::keyless::KeylessVerifier; - let cert_identity = matches.get_one::("cert_identity").map(|s| s.as_str()); - let cert_oidc_issuer = matches.get_one::("cert_oidc_issuer").map(|s| s.as_str()); + let cert_identity = matches + .get_one::("cert_identity") + .map(|s| s.as_str()); + let cert_oidc_issuer = matches + .get_one::("cert_oidc_issuer") + .map(|s| s.as_str()); println!("Verifying keyless signature..."); let module = Module::deserialize_from_file(input_file)?; @@ -1088,13 +1092,19 @@ fn start() -> Result<(), WSError> { } } } else if let Some(matches) = matches.subcommand_matches("docs") { - if matches.get_flag("list") || (matches.get_one::("topic").is_none() && matches.get_one::("search").is_none()) { + if matches.get_flag("list") + || (matches.get_one::("topic").is_none() + && matches.get_one::("search").is_none()) + { docs::list_topics(); } else if let Some(query) = matches.get_one::("search") { docs::search_topics(query); } else if let Some(topic) = matches.get_one::("topic") { if !docs::show_topic(topic) { - eprintln!("Unknown topic: '{}'. Use 'wsc docs --list' to see available topics.", topic); + eprintln!( + "Unknown topic: '{}'. Use 'wsc docs --list' to see available topics.", + topic + ); return Err(WSError::UsageError("Unknown documentation topic")); } } @@ -1320,7 +1330,10 @@ fn handle_bundle_command(matches: &clap::ArgMatches, verbose: bool) -> Result<() println!(); // Certificate Authorities - println!("Certificate Authorities ({}):", bundle.certificate_authorities.len()); + println!( + "Certificate Authorities ({}):", + bundle.certificate_authorities.len() + ); for (i, ca) in bundle.certificate_authorities.iter().enumerate() { println!(" [{}] {}", i + 1, ca.name); if !ca.uri.is_empty() { @@ -1330,11 +1343,7 @@ fn handle_bundle_command(matches: &clap::ArgMatches, verbose: bool) -> Result<() if verbose { for (j, pem) in ca.certificates_pem.iter().enumerate() { let lines: Vec<&str> = pem.lines().collect(); - println!( - " [{}] {} lines", - j + 1, - lines.len() - ); + println!(" [{}] {} lines", j + 1, lines.len()); } } } @@ -1344,7 +1353,10 @@ fn handle_bundle_command(matches: &clap::ArgMatches, verbose: bool) -> Result<() println!("Transparency Logs ({}):", bundle.transparency_logs.len()); for (i, log) in bundle.transparency_logs.iter().enumerate() { println!(" [{}] {}", i + 1, log.base_url); - println!(" Log ID: {}...", &log.log_id[..16.min(log.log_id.len())]); + println!( + " Log ID: {}...", + &log.log_id[..16.min(log.log_id.len())] + ); println!(" Algorithm: {}", log.hash_algorithm); } println!(); @@ -1383,7 +1395,10 @@ fn handle_bundle_command(matches: &clap::ArgMatches, verbose: bool) -> Result<() // Fetch trusted root let trusted_root = fetch_sigstore_trusted_root()?; - println!(" Found {} certificate authorities", trusted_root.certificate_authorities.len()); + println!( + " Found {} certificate authorities", + trusted_root.certificate_authorities.len() + ); println!(" Found {} transparency logs", trusted_root.tlogs.len()); // Convert to TrustBundle @@ -1458,7 +1473,12 @@ fn handle_show_chain_command(matches: &clap::ArgMatches) -> Result<(), WSError> println!(); println!("Transformations ({}):", audit_trail.transformations.len()); for (i, attestation) in audit_trail.transformations.iter().enumerate() { - println!(" [{}] {} v{}", i + 1, attestation.tool.name, attestation.tool.version); + println!( + " [{}] {} v{}", + i + 1, + attestation.tool.name, + attestation.tool.version + ); println!(" Type: {}", attestation.transformation_type); println!(" Timestamp: {}", attestation.timestamp); println!(" Inputs: {}", attestation.inputs.len()); @@ -1469,9 +1489,11 @@ fn handle_show_chain_command(matches: &clap::ArgMatches) -> Result<(), WSError> for (i, root) in audit_trail.root_components.iter().enumerate() { println!(" [{}] {}", i + 1, root.artifact.name); println!(" Hash: {}", root.artifact.hash); - println!(" Signature: {} ({})", + println!( + " Signature: {} ({})", root.signature_info.key_id.as_deref().unwrap_or("unknown"), - root.signature_info.algorithm); + root.signature_info.algorithm + ); } } return Ok(()); @@ -1494,13 +1516,23 @@ fn handle_show_chain_command(matches: &clap::ArgMatches) -> Result<(), WSError> println!("Transformation Attestations"); println!("==========================="); for (i, attestation) in attestations.iter().enumerate() { - println!("[{}] {} v{}", i + 1, attestation.tool.name, attestation.tool.version); + println!( + "[{}] {} v{}", + i + 1, + attestation.tool.name, + attestation.tool.version + ); println!(" ID: {}", attestation.attestation_id); println!(" Type: {}", attestation.transformation_type); println!(" Timestamp: {}", attestation.timestamp); println!(" Inputs: {}", attestation.inputs.len()); for (j, input) in attestation.inputs.iter().enumerate() { - println!(" [{}] {} ({})", j + 1, input.artifact.hash, input.signature_status); + println!( + " [{}] {} ({})", + j + 1, + input.artifact.hash, + input.signature_status + ); } println!(" Output: {}", attestation.output.hash); println!(); @@ -1518,20 +1550,24 @@ fn handle_verify_chain_command(matches: &clap::ArgMatches) -> Result<(), WSError .ok_or(WSError::UsageError("Missing input file"))?; let policy_file = matches.get_one::("policy").map(|s| s.as_str()); - let trusted_tools_file = matches.get_one::("trusted_tools").map(|s| s.as_str()); + let trusted_tools_file = matches + .get_one::("trusted_tools") + .map(|s| s.as_str()); let require_signatures = matches.get_flag("require_signatures"); let require_attestation_signatures = matches.get_flag("require_attestation_signatures"); let strict_mode = matches.get_flag("strict"); let report_only = matches.get_flag("report_only"); - let max_age_days = matches.get_one::("max_age") + let max_age_days = matches + .get_one::("max_age") .and_then(|s| s.parse::().ok()); // Read the module let module = Module::deserialize_from_file(input_file)?; // Extract attestation from module - let attestation = extract_transformation_attestation(&module)? - .ok_or(WSError::InternalError("No transformation attestation found in module".to_string()))?; + let attestation = extract_transformation_attestation(&module)?.ok_or( + WSError::InternalError("No transformation attestation found in module".to_string()), + )?; // If a policy file is provided, use the new policy engine if let Some(policy_path) = policy_file { @@ -1558,28 +1594,35 @@ fn handle_verify_chain_command(matches: &clap::ArgMatches) -> Result<(), WSError // Load trusted tools if specified if let Some(tools_file) = trusted_tools_file { let tools_data = std::fs::read_to_string(tools_file).map_err(|e| { - WSError::InternalError(format!("Failed to read trusted tools '{}': {}", tools_file, e)) + WSError::InternalError(format!( + "Failed to read trusted tools '{}': {}", + tools_file, e + )) })?; // Parse as a map of tool name -> tool config with optional public keys - let tools_json: serde_json::Value = serde_json::from_str(&tools_data).map_err(|e| { - WSError::InternalError(format!("Failed to parse trusted tools: {}", e)) - })?; + let tools_json: serde_json::Value = serde_json::from_str(&tools_data) + .map_err(|e| WSError::InternalError(format!("Failed to parse trusted tools: {}", e)))?; if let Some(obj) = tools_json.as_object() { for (name, value) in obj { - let mut info = if let Some(min_ver) = value.get("min_version").and_then(|v| v.as_str()) { - TrustedToolInfo::min_version(min_ver) - } else { - TrustedToolInfo::any_version() - }; + let mut info = + if let Some(min_ver) = value.get("min_version").and_then(|v| v.as_str()) { + TrustedToolInfo::min_version(min_ver) + } else { + TrustedToolInfo::any_version() + }; // Parse public keys if present // Format: "public_keys": [{"algorithm": "ed25519", "key": "base64...", "key_id": "optional"}] if let Some(public_keys) = value.get("public_keys").and_then(|v| v.as_array()) { for pk in public_keys { - let algorithm = pk.get("algorithm").and_then(|v| v.as_str()).unwrap_or("ed25519"); + let algorithm = pk + .get("algorithm") + .and_then(|v| v.as_str()) + .unwrap_or("ed25519"); if let Some(key) = pk.get("key").and_then(|v| v.as_str()) { - let key_id = pk.get("key_id").and_then(|v| v.as_str()).map(String::from); + let key_id = + pk.get("key_id").and_then(|v| v.as_str()).map(String::from); info.public_keys.push(TrustedPublicKey { algorithm: algorithm.to_string(), key: key.to_string(), @@ -1704,13 +1747,14 @@ fn handle_attest_command(matches: &clap::ArgMatches) -> Result<(), WSError> { with_attestation.serialize_to_file(output_file)?; // Get output file size for reporting - let output_size = std::fs::metadata(output_file) - .map(|m| m.len()) - .unwrap_or(0); + let output_size = std::fs::metadata(output_file).map(|m| m.len()).unwrap_or(0); println!("Transformation attestation recorded:"); println!(" ID: {}", attestation.attestation_id); - println!(" Tool: {} v{}", attestation.tool.name, attestation.tool.version); + println!( + " Tool: {} v{}", + attestation.tool.name, attestation.tool.version + ); println!(" Type: {}", attestation.transformation_type); println!(" Input: {} ({} bytes)", input_file, input_bytes.len()); println!(" Output: {} ({} bytes)", output_file, output_size); @@ -1777,7 +1821,10 @@ fn handle_policy_verification( println!(" Warnings: {}", result.summary.failed_report); } if !result.summary.tools_verified.is_empty() { - println!(" Tools verified: {}", result.summary.tools_verified.join(", ")); + println!( + " Tools verified: {}", + result.summary.tools_verified.join(", ") + ); } // SLSA improvement suggestions @@ -1806,9 +1853,7 @@ fn chrono_format(timestamp: u64) -> String { use std::time::{Duration, UNIX_EPOCH}; let dt = UNIX_EPOCH + Duration::from_secs(timestamp); // Format as ISO 8601 using stdlib - let duration = dt - .duration_since(UNIX_EPOCH) - .unwrap_or_default(); + let duration = dt.duration_since(UNIX_EPOCH).unwrap_or_default(); let secs = duration.as_secs(); let days = secs / 86400; // Approximate date from days since epoch diff --git a/src/lib/src/airgapped/bundle.rs b/src/lib/src/airgapped/bundle.rs index 82d5374..a083a52 100644 --- a/src/lib/src/airgapped/bundle.rs +++ b/src/lib/src/airgapped/bundle.rs @@ -292,11 +292,8 @@ impl TransparencyLog { .filter(|line| !line.starts_with("-----")) .collect::(); - let der_bytes = base64::Engine::decode( - &base64::engine::general_purpose::STANDARD, - &der, - ) - .map_err(|e| WSError::InternalError(format!("Invalid PEM encoding: {}", e)))?; + let der_bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &der) + .map_err(|e| WSError::InternalError(format!("Invalid PEM encoding: {}", e)))?; let hash = hmac_sha256::Hash::hash(&der_bytes); Ok(hex::encode(hash)) @@ -334,8 +331,9 @@ impl SignedTrustBundle { /// Serialize to JSON pub fn to_json(&self) -> Result, WSError> { - serde_json::to_vec_pretty(self) - .map_err(|e| WSError::InternalError(format!("Failed to serialize signed bundle: {}", e))) + serde_json::to_vec_pretty(self).map_err(|e| { + WSError::InternalError(format!("Failed to serialize signed bundle: {}", e)) + }) } /// Deserialize from JSON @@ -365,12 +363,10 @@ impl BundleSignature { // Create keypair from secret key bytes let seed = if secret_key.len() == 32 { - Seed::from_slice(secret_key) - .map_err(|e| WSError::CryptoError(e))? + Seed::from_slice(secret_key).map_err(|e| WSError::CryptoError(e))? } else if secret_key.len() == 64 { // Full keypair format - extract seed - Seed::from_slice(&secret_key[..32]) - .map_err(|e| WSError::CryptoError(e))? + Seed::from_slice(&secret_key[..32]).map_err(|e| WSError::CryptoError(e))? } else { return Err(WSError::InvalidArgument); }; @@ -384,10 +380,8 @@ impl BundleSignature { // Sign let sig = keypair.sk.sign(data, None); - let signature = base64::Engine::encode( - &base64::engine::general_purpose::STANDARD, - sig.as_ref(), - ); + let signature = + base64::Engine::encode(&base64::engine::general_purpose::STANDARD, sig.as_ref()); Ok(Self { key_id, @@ -402,8 +396,7 @@ impl BundleSignature { match self.algorithm { SignatureAlgorithm::Ed25519 => { - let pk = PublicKey::from_slice(public_key) - .map_err(|e| WSError::CryptoError(e))?; + let pk = PublicKey::from_slice(public_key).map_err(|e| WSError::CryptoError(e))?; let sig_bytes = base64::Engine::decode( &base64::engine::general_purpose::STANDARD, @@ -411,13 +404,14 @@ impl BundleSignature { ) .map_err(|_| WSError::InvalidArgument)?; - let sig = Signature::from_slice(&sig_bytes) - .map_err(|e| WSError::CryptoError(e))?; + let sig = Signature::from_slice(&sig_bytes).map_err(|e| WSError::CryptoError(e))?; - pk.verify(data, &sig) - .map_err(|e| WSError::CryptoError(e)) + pk.verify(data, &sig).map_err(|e| WSError::CryptoError(e)) } - _ => Err(WSError::UnsupportedAlgorithm(format!("{:?}", self.algorithm))), + _ => Err(WSError::UnsupportedAlgorithm(format!( + "{:?}", + self.algorithm + ))), } } } diff --git a/src/lib/src/airgapped/config.rs b/src/lib/src/airgapped/config.rs index ec01149..770d9eb 100644 --- a/src/lib/src/airgapped/config.rs +++ b/src/lib/src/airgapped/config.rs @@ -209,7 +209,10 @@ mod tests { let config = AirGappedConfig::high_security(); assert!(config.max_signature_age.is_some()); assert!(config.enforce_rollback_protection); - assert!(matches!(config.grace_period_behavior, GracePeriodBehavior::Strict)); + assert!(matches!( + config.grace_period_behavior, + GracePeriodBehavior::Strict + )); } #[test] @@ -231,7 +234,9 @@ mod tests { let req = IdentityRequirements::github_actions("myorg"); assert!(req.matches_issuer("https://token.actions.githubusercontent.com")); - assert!(req.matches_subject("https://github.com/myorg/repo/.github/workflows/ci.yml@refs/heads/main")); + assert!(req.matches_subject( + "https://github.com/myorg/repo/.github/workflows/ci.yml@refs/heads/main" + )); assert!(!req.matches_subject("https://github.com/otherorg/repo")); } diff --git a/src/lib/src/airgapped/mod.rs b/src/lib/src/airgapped/mod.rs index 48ea63b..e40a158 100644 --- a/src/lib/src/airgapped/mod.rs +++ b/src/lib/src/airgapped/mod.rs @@ -113,4 +113,7 @@ pub use storage::*; pub use verifier::*; // Re-export key TUF types -pub use tuf::{fetch_sigstore_trusted_root, parse_trusted_root, trusted_root_to_bundle, SigstoreTrustedRoot, SIGSTORE_TRUSTED_ROOT_URL}; +pub use tuf::{ + SIGSTORE_TRUSTED_ROOT_URL, SigstoreTrustedRoot, fetch_sigstore_trusted_root, + parse_trusted_root, trusted_root_to_bundle, +}; diff --git a/src/lib/src/airgapped/state.rs b/src/lib/src/airgapped/state.rs index 4d0d9ac..3109885 100644 --- a/src/lib/src/airgapped/state.rs +++ b/src/lib/src/airgapped/state.rs @@ -222,10 +222,7 @@ mod tests { assert!(state.check_module_version("my-module", 1000)); // Update with version info - state.update_module_version( - "my-module", - ModuleVersionInfo::new(1000, &[0u8; 32]), - ); + state.update_module_version("my-module", ModuleVersionInfo::new(1000, &[0u8; 32])); // Same or newer is OK assert!(state.check_module_version("my-module", 1000)); diff --git a/src/lib/src/airgapped/storage.rs b/src/lib/src/airgapped/storage.rs index 8f83ba6..ba6a671 100644 --- a/src/lib/src/airgapped/storage.rs +++ b/src/lib/src/airgapped/storage.rs @@ -61,7 +61,9 @@ pub trait TrustStore: Send + Sync { /// Returns `Err` if saving is not supported or fails. fn save_bundle(&self, bundle: &SignedTrustBundle) -> Result<(), WSError> { let _ = bundle; - Err(WSError::InternalError("Bundle saving not supported".to_string())) + Err(WSError::InternalError( + "Bundle saving not supported".to_string(), + )) } /// Check if storage is available and accessible @@ -157,7 +159,9 @@ impl MemoryTrustStore { /// Create with pre-loaded bundle pub fn with_bundle(bundle: SignedTrustBundle) -> Self { - Self { bundle: Some(bundle) } + Self { + bundle: Some(bundle), + } } } @@ -176,7 +180,9 @@ impl TrustStore for MemoryTrustStore { fn save_bundle(&self, _bundle: &SignedTrustBundle) -> Result<(), WSError> { // Can't mutate through shared reference; use interior mutability if needed - Err(WSError::InternalError("MemoryTrustStore is immutable".to_string())) + Err(WSError::InternalError( + "MemoryTrustStore is immutable".to_string(), + )) } fn metadata(&self) -> StorageMetadata { @@ -235,17 +241,15 @@ impl FileTrustStore { #[cfg(not(target_os = "wasi"))] impl TrustStore for FileTrustStore { fn load_bundle(&self) -> Result { - let data = std::fs::read(&self.path).map_err(|e| { - WSError::InternalError(format!("Failed to read bundle file: {}", e)) - })?; + let data = std::fs::read(&self.path) + .map_err(|e| WSError::InternalError(format!("Failed to read bundle file: {}", e)))?; SignedTrustBundle::from_json(&data) } fn save_bundle(&self, bundle: &SignedTrustBundle) -> Result<(), WSError> { let data = bundle.to_json()?; - std::fs::write(&self.path, data).map_err(|e| { - WSError::InternalError(format!("Failed to write bundle file: {}", e)) - }) + std::fs::write(&self.path, data) + .map_err(|e| WSError::InternalError(format!("Failed to write bundle file: {}", e))) } fn metadata(&self) -> StorageMetadata { @@ -276,9 +280,8 @@ impl FileKeyStore { #[cfg(not(target_os = "wasi"))] impl KeyStore for FileKeyStore { fn load_verifier_key(&self) -> Result, WSError> { - let data = std::fs::read(&self.path).map_err(|e| { - WSError::InternalError(format!("Failed to read key file: {}", e)) - })?; + let data = std::fs::read(&self.path) + .map_err(|e| WSError::InternalError(format!("Failed to read key file: {}", e)))?; // Handle wsc key format (1-byte prefix + 32-byte key) if data.len() == 33 { diff --git a/src/lib/src/airgapped/tuf.rs b/src/lib/src/airgapped/tuf.rs index ce3c7ed..3b839ad 100644 --- a/src/lib/src/airgapped/tuf.rs +++ b/src/lib/src/airgapped/tuf.rs @@ -8,8 +8,7 @@ use crate::error::WSError; use serde::Deserialize; /// Default URL for Sigstore's trusted_root.json -pub const SIGSTORE_TRUSTED_ROOT_URL: &str = - "https://raw.githubusercontent.com/sigstore/root-signing/refs/heads/main/targets/trusted_root.json"; +pub const SIGSTORE_TRUSTED_ROOT_URL: &str = "https://raw.githubusercontent.com/sigstore/root-signing/refs/heads/main/targets/trusted_root.json"; /// Sigstore TUF trusted root structure #[derive(Debug, Deserialize)] @@ -199,7 +198,9 @@ pub fn trusted_root_to_bundle( } /// Convert a Sigstore CA entry to wsc CertificateAuthority -fn convert_certificate_authority(entry: &CertificateAuthorityEntry) -> Result { +fn convert_certificate_authority( + entry: &CertificateAuthorityEntry, +) -> Result { let mut pem_certs = Vec::new(); for cert in &entry.cert_chain.certificates { @@ -233,7 +234,10 @@ fn convert_certificate_authority(entry: &CertificateAuthorityEntry) -> Result Result Result { // Validate base64 by decoding - let _ = base64::Engine::decode( - &base64::engine::general_purpose::STANDARD, - base64_der, - ) - .map_err(|e| WSError::InternalError(format!("Invalid base64: {}", e)))?; + let _ = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, base64_der) + .map_err(|e| WSError::InternalError(format!("Invalid base64: {}", e)))?; // Format as PEM (wrap at 64 characters) let mut pem = format!("-----BEGIN {}-----\n", label); @@ -331,14 +332,8 @@ fn parse_rfc3339(s: &str) -> Result { return Err(WSError::InternalError(format!("Invalid RFC 3339: {}", s))); } - let date_parts: Vec = parts[0] - .split('-') - .filter_map(|p| p.parse().ok()) - .collect(); - let time_parts: Vec = parts[1] - .split(':') - .filter_map(|p| p.parse().ok()) - .collect(); + let date_parts: Vec = parts[0].split('-').filter_map(|p| p.parse().ok()).collect(); + let time_parts: Vec = parts[1].split(':').filter_map(|p| p.parse().ok()).collect(); if date_parts.len() != 3 || time_parts.len() != 3 { return Err(WSError::InternalError(format!("Invalid RFC 3339: {}", s))); @@ -436,8 +431,15 @@ mod tests { assert_eq!(bundle.version, 1); assert_eq!(bundle.transparency_logs.len(), 1); assert_eq!(bundle.certificate_authorities.len(), 1); - assert!(bundle.transparency_logs[0].public_key_pem.contains("-----BEGIN PUBLIC KEY-----")); - assert!(bundle.certificate_authorities[0].certificates_pem[0].contains("-----BEGIN CERTIFICATE-----")); + assert!( + bundle.transparency_logs[0] + .public_key_pem + .contains("-----BEGIN PUBLIC KEY-----") + ); + assert!( + bundle.certificate_authorities[0].certificates_pem[0] + .contains("-----BEGIN CERTIFICATE-----") + ); } #[test] diff --git a/src/lib/src/airgapped/verifier.rs b/src/lib/src/airgapped/verifier.rs index ebf0f7a..a811115 100644 --- a/src/lib/src/airgapped/verifier.rs +++ b/src/lib/src/airgapped/verifier.rs @@ -2,7 +2,7 @@ use crate::error::WSError; use crate::signature::keyless::{KeylessSignature, RekorEntry}; -use crate::time::{TimeSource, BUILD_TIMESTAMP}; +use crate::time::{BUILD_TIMESTAMP, TimeSource}; use super::{ AirGappedConfig, DeviceSecurityState, GracePeriodBehavior, KeyStore, SignedTrustBundle, @@ -159,8 +159,7 @@ impl AirGappedVerifier { // Check if bundle is expired if current_time > self.trust_bundle.validity.not_after { - let days_overdue = - (current_time - self.trust_bundle.validity.not_after) / 86400; + let days_overdue = (current_time - self.trust_bundle.validity.not_after) / 86400; if self.trust_bundle.is_in_grace_period(current_time) { warnings.push(VerificationWarning::BundleInGracePeriod { @@ -173,8 +172,7 @@ impl AirGappedVerifier { } } else { // Check if bundle is expiring soon (within 30 days) - let days_remaining = - (self.trust_bundle.validity.not_after - current_time) / 86400; + let days_remaining = (self.trust_bundle.validity.not_after - current_time) / 86400; if days_remaining <= 30 { warnings.push(VerificationWarning::BundleExpiringSoon { days_remaining: days_remaining as u32, @@ -205,15 +203,17 @@ impl AirGappedVerifier { // Get current time for bundle validity check // SECURITY: Fail closed when no time source is available. Without a reliable // clock, all time-based checks (expiry, freshness, grace period) are meaningless. - let current_time = self - .time_source - .as_ref() - .and_then(|ts| ts.now_unix().ok()) - .ok_or_else(|| WSError::VerificationError( + let current_time = + self.time_source + .as_ref() + .and_then(|ts| ts.now_unix().ok()) + .ok_or_else(|| { + WSError::VerificationError( "No time source available: air-gapped verification requires a reliable clock \ to enforce bundle expiry and signature freshness. Configure a time source \ via with_time_source() or use BuildTimeSource for development only.".to_string(), - ))?; + ) + })?; // 1. Check bundle validity if !self.trust_bundle.is_valid(current_time) { @@ -331,8 +331,12 @@ impl AirGappedVerifier { /// Extract identity from signature certificate fn extract_identity(&self, signature: &KeylessSignature) -> Result { - let issuer = signature.get_issuer().unwrap_or_else(|_| "unknown".to_string()); - let subject = signature.get_identity().unwrap_or_else(|_| "unknown".to_string()); + let issuer = signature + .get_issuer() + .unwrap_or_else(|_| "unknown".to_string()); + let subject = signature + .get_identity() + .unwrap_or_else(|_| "unknown".to_string()); Ok(SignerIdentity { issuer, @@ -344,7 +348,9 @@ impl AirGappedVerifier { /// Compute certificate fingerprint for revocation check fn compute_cert_fingerprint(&self, signature: &KeylessSignature) -> Result { if signature.cert_chain.is_empty() { - return Err(WSError::CertificateError("No certificates in chain".to_string())); + return Err(WSError::CertificateError( + "No certificates in chain".to_string(), + )); } // Get leaf certificate (first in chain) @@ -356,18 +362,19 @@ impl AirGappedVerifier { .filter(|line| !line.starts_with("-----")) .collect::(); - let der_bytes = base64::Engine::decode( - &base64::engine::general_purpose::STANDARD, - &der, - ) - .map_err(|e| WSError::CertificateError(format!("Invalid certificate PEM: {}", e)))?; + let der_bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &der) + .map_err(|e| WSError::CertificateError(format!("Invalid certificate PEM: {}", e)))?; let hash = hmac_sha256::Hash::hash(&der_bytes); Ok(hex::encode(hash)) } /// Verify cryptographic signature - fn verify_crypto(&self, signature: &KeylessSignature, module_hash: &[u8; 32]) -> Result<(), WSError> { + fn verify_crypto( + &self, + signature: &KeylessSignature, + module_hash: &[u8; 32], + ) -> Result<(), WSError> { // For now, delegate to the existing verification logic // In a full implementation, we would: // 1. Verify Rekor SET using bundle's Rekor key @@ -376,7 +383,9 @@ impl AirGappedVerifier { // Extract public key from leaf certificate if signature.cert_chain.is_empty() { - return Err(WSError::CertificateError("No certificates in chain".to_string())); + return Err(WSError::CertificateError( + "No certificates in chain".to_string(), + )); } let leaf_pem = &signature.cert_chain[0]; @@ -384,11 +393,10 @@ impl AirGappedVerifier { use ed25519_compact::{PublicKey, Signature}; - let pk = PublicKey::from_slice(&public_key) - .map_err(|e| WSError::CryptoError(e))?; + let pk = PublicKey::from_slice(&public_key).map_err(|e| WSError::CryptoError(e))?; - let sig = Signature::from_slice(&signature.signature) - .map_err(|e| WSError::CryptoError(e))?; + let sig = + Signature::from_slice(&signature.signature).map_err(|e| WSError::CryptoError(e))?; pk.verify(module_hash, &sig) .map_err(|e| WSError::CryptoError(e)) @@ -405,11 +413,8 @@ fn extract_public_key_from_cert(pem: &str) -> Result, WSError> { .filter(|line| !line.starts_with("-----")) .collect::(); - let der_bytes = base64::Engine::decode( - &base64::engine::general_purpose::STANDARD, - &der, - ) - .map_err(|e| WSError::CertificateError(format!("Invalid certificate PEM: {}", e)))?; + let der_bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &der) + .map_err(|e| WSError::CertificateError(format!("Invalid certificate PEM: {}", e)))?; // Parse certificate let (_, cert) = X509Certificate::from_der(&der_bytes) @@ -543,7 +548,11 @@ mod tests { .with_time_source(crate::time::SystemTimeSource); let warnings = verifier.check_bundle_health(); - assert!(warnings.iter().any(|w| matches!(w, VerificationWarning::BundleExpiringSoon { .. }))); + assert!( + warnings + .iter() + .any(|w| matches!(w, VerificationWarning::BundleExpiringSoon { .. })) + ); } #[test] diff --git a/src/lib/src/audit/mod.rs b/src/lib/src/audit/mod.rs index 4e741c5..06d9285 100644 --- a/src/lib/src/audit/mod.rs +++ b/src/lib/src/audit/mod.rs @@ -44,9 +44,9 @@ use std::sync::OnceLock; use tracing_subscriber::{ + EnvFilter, fmt::{self, format::FmtSpan}, prelude::*, - EnvFilter, }; /// Global audit configuration state @@ -352,7 +352,10 @@ fn sanitize_error_message(message: &str) -> String { // Remove anything that looks like a token .split_whitespace() .map(|word| { - if word.len() > 40 && word.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') + if word.len() > 40 + && word + .chars() + .all(|c| c.is_alphanumeric() || c == '-' || c == '_') { "[REDACTED]" } else { @@ -390,7 +393,8 @@ mod tests { ); // Long token-like strings should be redacted - let with_token = "Failed with token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9eyJzdWIiOiIxMjM0NTY3ODkwIn0"; + let with_token = + "Failed with token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9eyJzdWIiOiIxMjM0NTY3ODkwIn0"; assert!(sanitize_error_message(with_token).contains("[REDACTED]")); } diff --git a/src/lib/src/build_env.rs b/src/lib/src/build_env.rs index a78323e..eace10f 100644 --- a/src/lib/src/build_env.rs +++ b/src/lib/src/build_env.rs @@ -92,11 +92,7 @@ fn resolve_command_path(cmd: &str) -> Option { std::env::var_os("PATH").and_then(|paths| { std::env::split_paths(&paths).find_map(|dir| { let full = dir.join(cmd); - if full.is_file() { - Some(full) - } else { - None - } + if full.is_file() { Some(full) } else { None } }) }) } @@ -180,20 +176,23 @@ impl BuildEnvironment { let nix_flake_lock_hash = hash_flake_lock(); - let nix_build = if std::env::var("IN_NIX_SHELL").is_ok() - || std::env::var("NIX_BUILD_TOP").is_ok() - { - Some(true) - } else { - None - }; + let nix_build = + if std::env::var("IN_NIX_SHELL").is_ok() || std::env::var("NIX_BUILD_TOP").is_ok() { + Some(true) + } else { + None + }; let wasm_tools_version = capture_command_output("wasm-tools", &["--version"]); - let host_platform = Some(format!("{}-{}", std::env::consts::ARCH, std::env::consts::OS)); + let host_platform = Some(format!( + "{}-{}", + std::env::consts::ARCH, + std::env::consts::OS + )); - let os_version = capture_command_output("uname", &["-sr"]) - .or_else(|| std::env::var("OS").ok()); + let os_version = + capture_command_output("uname", &["-sr"]).or_else(|| std::env::var("OS").ok()); let captured_at = Some(chrono::Utc::now().to_rfc3339()); @@ -340,10 +339,7 @@ mod tests { env.additional_tools.get("protoc"), Some(&"3.21.0".to_string()) ); - assert_eq!( - env.additional_tools.get("z3"), - Some(&"4.12.0".to_string()) - ); + assert_eq!(env.additional_tools.get("z3"), Some(&"4.12.0".to_string())); } #[test] @@ -385,7 +381,8 @@ mod tests { #[test] fn test_capture_command_output_missing_tool() { - let result = capture_command_output("this-tool-definitely-does-not-exist-xyz", &["--version"]); + let result = + capture_command_output("this-tool-definitely-does-not-exist-xyz", &["--version"]); assert!(result.is_none()); } diff --git a/src/lib/src/composition/mod.rs b/src/lib/src/composition/mod.rs index 310665b..2acd8f0 100644 --- a/src/lib/src/composition/mod.rs +++ b/src/lib/src/composition/mod.rs @@ -3,19 +3,6 @@ use crate::wasm_module::{CustomSection, Module, Section, SectionLike}; use base64::Engine; // Re-export attestation types from the minimal attestation crate -pub use wsc_attestation::{ - // Section constants - TRANSFORMATION_ATTESTATION_SECTION, TRANSFORMATION_AUDIT_TRAIL_SECTION, - // Build provenance - BuildProvenance, ProvenanceBuilder, - // Transformation types - TransformationType, ArtifactDescriptor, SignatureStatus, - InputSignatureInfo, ToolInfo, AttestationSignature, - InputArtifact, TransformationAttestation, - RootComponent, TransformationAuditTrail, - // Builder - TransformationAttestationBuilder, -}; /// Component composition and provenance tracking /// /// This module provides support for WebAssembly component composition with @@ -86,6 +73,27 @@ pub use wsc_attestation::{ /// ``` use serde::{Deserialize, Serialize}; use std::collections::HashMap; +pub use wsc_attestation::{ + ArtifactDescriptor, + AttestationSignature, + // Build provenance + BuildProvenance, + InputArtifact, + InputSignatureInfo, + ProvenanceBuilder, + RootComponent, + SignatureStatus, + // Section constants + TRANSFORMATION_ATTESTATION_SECTION, + TRANSFORMATION_AUDIT_TRAIL_SECTION, + ToolInfo, + TransformationAttestation, + // Builder + TransformationAttestationBuilder, + TransformationAuditTrail, + // Transformation types + TransformationType, +}; use x509_parser::prelude::FromDer; // BuildProvenance and ProvenanceBuilder are re-exported from wsc_attestation @@ -284,10 +292,7 @@ impl DependencyGraph { pub fn add_dependency(&mut self, from: impl Into, to: impl Into) { let from = from.into(); let to = to.into(); - self.dependencies - .entry(from) - .or_default() - .push(to); + self.dependencies.entry(from).or_default().push(to); } /// Set the actual hash for a component (for validation) @@ -317,9 +322,9 @@ impl DependencyGraph { if !visited.contains_key(node) && let Some(cycle) = self.dfs_cycle_detection(node, &mut visited, &mut rec_stack, &mut Vec::new()) - { - return Some(cycle); - } + { + return Some(cycle); + } } None @@ -371,13 +376,14 @@ impl DependencyGraph { for (id, expected_hash) in &self.expected_hashes { if let Some(actual_hash) = self.actual_hashes.get(id) - && expected_hash != actual_hash { - substitutions.push(ComponentSubstitution { - component_id: id.clone(), - expected_hash: expected_hash.clone(), - actual_hash: actual_hash.clone(), - }); - } + && expected_hash != actual_hash + { + substitutions.push(ComponentSubstitution { + component_id: id.clone(), + expected_hash: expected_hash.clone(), + actual_hash: actual_hash.clone(), + }); + } } substitutions @@ -1352,20 +1358,18 @@ pub fn extract_composition_manifest( ) -> Result, WSError> { for section in &module.sections { if let Section::Custom(custom) = section - && custom.name() == COMPOSITION_MANIFEST_SECTION { - let json = std::str::from_utf8(custom.payload()).map_err(|e| { - WSError::InternalError(format!("Invalid UTF-8 in composition manifest: {}", e)) - })?; + && custom.name() == COMPOSITION_MANIFEST_SECTION + { + let json = std::str::from_utf8(custom.payload()).map_err(|e| { + WSError::InternalError(format!("Invalid UTF-8 in composition manifest: {}", e)) + })?; - let manifest = CompositionManifest::from_json(json).map_err(|e| { - WSError::InternalError(format!( - "Failed to deserialize composition manifest: {}", - e - )) - })?; + let manifest = CompositionManifest::from_json(json).map_err(|e| { + WSError::InternalError(format!("Failed to deserialize composition manifest: {}", e)) + })?; - return Ok(Some(manifest)); - } + return Ok(Some(manifest)); + } } Ok(None) } @@ -1392,17 +1396,18 @@ pub fn embed_build_provenance( pub fn extract_build_provenance(module: &Module) -> Result, WSError> { for section in &module.sections { if let Section::Custom(custom) = section - && custom.name() == BUILD_PROVENANCE_SECTION { - let json = std::str::from_utf8(custom.payload()).map_err(|e| { - WSError::InternalError(format!("Invalid UTF-8 in build provenance: {}", e)) - })?; + && custom.name() == BUILD_PROVENANCE_SECTION + { + let json = std::str::from_utf8(custom.payload()).map_err(|e| { + WSError::InternalError(format!("Invalid UTF-8 in build provenance: {}", e)) + })?; - let provenance = serde_json::from_str(json).map_err(|e| { - WSError::InternalError(format!("Failed to deserialize build provenance: {}", e)) - })?; + let provenance = serde_json::from_str(json).map_err(|e| { + WSError::InternalError(format!("Failed to deserialize build provenance: {}", e)) + })?; - return Ok(Some(provenance)); - } + return Ok(Some(provenance)); + } } Ok(None) } @@ -1423,16 +1428,17 @@ pub fn embed_sbom(mut module: Module, sbom: &Sbom) -> Result { pub fn extract_sbom(module: &Module) -> Result, WSError> { for section in &module.sections { if let Section::Custom(custom) = section - && custom.name() == SBOM_SECTION { - let json = std::str::from_utf8(custom.payload()) - .map_err(|e| WSError::InternalError(format!("Invalid UTF-8 in SBOM: {}", e)))?; + && custom.name() == SBOM_SECTION + { + let json = std::str::from_utf8(custom.payload()) + .map_err(|e| WSError::InternalError(format!("Invalid UTF-8 in SBOM: {}", e)))?; - let sbom = Sbom::from_json(json).map_err(|e| { - WSError::InternalError(format!("Failed to deserialize SBOM: {}", e)) - })?; + let sbom = Sbom::from_json(json).map_err(|e| { + WSError::InternalError(format!("Failed to deserialize SBOM: {}", e)) + })?; - return Ok(Some(sbom)); - } + return Ok(Some(sbom)); + } } Ok(None) } @@ -1459,20 +1465,18 @@ pub fn embed_intoto_attestation( pub fn extract_intoto_attestation(module: &Module) -> Result, WSError> { for section in &module.sections { if let Section::Custom(custom) = section - && custom.name() == INTOTO_ATTESTATION_SECTION { - let json = std::str::from_utf8(custom.payload()).map_err(|e| { - WSError::InternalError(format!("Invalid UTF-8 in in-toto attestation: {}", e)) - })?; + && custom.name() == INTOTO_ATTESTATION_SECTION + { + let json = std::str::from_utf8(custom.payload()).map_err(|e| { + WSError::InternalError(format!("Invalid UTF-8 in in-toto attestation: {}", e)) + })?; - let attestation = InTotoAttestation::from_json(json).map_err(|e| { - WSError::InternalError(format!( - "Failed to deserialize in-toto attestation: {}", - e - )) - })?; + let attestation = InTotoAttestation::from_json(json).map_err(|e| { + WSError::InternalError(format!("Failed to deserialize in-toto attestation: {}", e)) + })?; - return Ok(Some(attestation)); - } + return Ok(Some(attestation)); + } } Ok(None) } @@ -1528,7 +1532,10 @@ pub fn embed_transformation_attestation( attestation: &TransformationAttestation, ) -> Result { let json = attestation.to_json().map_err(|e| { - WSError::InternalError(format!("Failed to serialize transformation attestation: {}", e)) + WSError::InternalError(format!( + "Failed to serialize transformation attestation: {}", + e + )) })?; let custom_section = CustomSection::new( @@ -1614,7 +1621,10 @@ pub fn embed_transformation_audit_trail( trail: &TransformationAuditTrail, ) -> Result { let json = trail.to_json().map_err(|e| { - WSError::InternalError(format!("Failed to serialize transformation audit trail: {}", e)) + WSError::InternalError(format!( + "Failed to serialize transformation audit trail: {}", + e + )) })?; let custom_section = CustomSection::new( @@ -1975,10 +1985,7 @@ pub fn verify_attestation_signature( let signature = match Signature::from_slice(&signature_bytes) { Ok(sig) => sig, Err(e) => { - return AttestationSignatureResult::Invalid(format!( - "Invalid signature format: {}", - e - )); + return AttestationSignatureResult::Invalid(format!("Invalid signature format: {}", e)); } }; @@ -2105,7 +2112,10 @@ fn verify_attestation_recursive( // Verify tool is trusted let tool_info = policy.trusted_tools.get(tool_name); if let Some(info) = tool_info { - if !info.satisfies(&attestation.tool.version, attestation.tool.tool_hash.as_deref()) { + if !info.satisfies( + &attestation.tool.version, + attestation.tool.tool_hash.as_deref(), + ) { result.add_error(format!( "Tool '{}' version '{}' does not meet policy requirements", tool_name, attestation.tool.version @@ -2192,10 +2202,7 @@ fn verify_attestation_recursive( if let Some(signer_id) = signer { if !policy.trusted_attestation_signers.contains(signer_id) { - result.add_error(format!( - "Attestation signer '{}' is not trusted", - signer_id - )); + result.add_error(format!("Attestation signer '{}' is not trusted", signer_id)); } } else { result.add_error("Attestation has no signer identity or key ID"); @@ -2346,9 +2353,10 @@ pub fn validate_attestation_timestamps( ) -> Result<(), String> { // Validate metadata timestamps if let Some(finished_on_value) = attestation.predicate.metadata.get("finishedOn") - && let Some(finished_on) = finished_on_value.as_str() { - policy.validate_timestamp(finished_on, "Build completion")?; - } + && let Some(finished_on) = finished_on_value.as_str() + { + policy.validate_timestamp(finished_on, "Build completion")?; + } Ok(()) } @@ -2375,15 +2383,17 @@ pub fn validate_all_timestamps( // Validate build provenance timestamps if let Some(ref p) = provenance - && let Err(e) = validate_provenance_timestamps(p, policy) { - errors.push(e); - } + && let Err(e) = validate_provenance_timestamps(p, policy) + { + errors.push(e); + } // Validate attestation timestamps if let Some(ref a) = attestation - && let Err(e) = validate_attestation_timestamps(a, policy) { - errors.push(e); - } + && let Err(e) = validate_attestation_timestamps(a, policy) + { + errors.push(e); + } Ok(ValidationResult { valid: errors.is_empty(), @@ -2457,13 +2467,14 @@ impl SignatureFreshnessPolicy { // Check minimum timestamp if let Some(min_ts) = &self.minimum_timestamp - && timestamp_utc < *min_ts { - return Err(format!( - "{} signature was created before minimum acceptable time ({})", - context, - min_ts.to_rfc3339() - )); - } + && timestamp_utc < *min_ts + { + return Err(format!( + "{} signature was created before minimum acceptable time ({})", + context, + min_ts.to_rfc3339() + )); + } Ok(()) } @@ -2550,14 +2561,13 @@ impl CertificateValidityPolicy { .with_timezone(&chrono::Utc); // Check if certificate is not yet valid - if now < not_before_time - && !self.allow_not_yet_valid { - return Err(format!( - "{} certificate is not yet valid (valid from: {})", - context, - not_before_time.to_rfc3339() - )); - } + if now < not_before_time && !self.allow_not_yet_valid { + return Err(format!( + "{} certificate is not yet valid (valid from: {})", + context, + not_before_time.to_rfc3339() + )); + } // Check if certificate is expired if now > not_after_time { @@ -2875,20 +2885,18 @@ pub fn embed_device_attestation( pub fn extract_device_attestation(module: &Module) -> Result, WSError> { for section in &module.sections { if let Section::Custom(custom) = section - && custom.name() == DEVICE_ATTESTATION_SECTION { - let json = std::str::from_utf8(custom.payload()).map_err(|e| { - WSError::InternalError(format!("Invalid UTF-8 in device attestation: {}", e)) - })?; + && custom.name() == DEVICE_ATTESTATION_SECTION + { + let json = std::str::from_utf8(custom.payload()).map_err(|e| { + WSError::InternalError(format!("Invalid UTF-8 in device attestation: {}", e)) + })?; - let attestation = DeviceAttestation::from_json(json).map_err(|e| { - WSError::InternalError(format!( - "Failed to deserialize device attestation: {}", - e - )) - })?; + let attestation = DeviceAttestation::from_json(json).map_err(|e| { + WSError::InternalError(format!("Failed to deserialize device attestation: {}", e)) + })?; - return Ok(Some(attestation)); - } + return Ok(Some(attestation)); + } } Ok(None) } @@ -2917,23 +2925,21 @@ pub fn extract_transparency_log_entry( ) -> Result, WSError> { for section in &module.sections { if let Section::Custom(custom) = section - && custom.name() == TRANSPARENCY_LOG_SECTION { - let json = std::str::from_utf8(custom.payload()).map_err(|e| { - WSError::InternalError(format!( - "Invalid UTF-8 in transparency log entry: {}", - e - )) - })?; + && custom.name() == TRANSPARENCY_LOG_SECTION + { + let json = std::str::from_utf8(custom.payload()).map_err(|e| { + WSError::InternalError(format!("Invalid UTF-8 in transparency log entry: {}", e)) + })?; - let entry = TransparencyLogEntry::from_json(json).map_err(|e| { - WSError::InternalError(format!( - "Failed to deserialize transparency log entry: {}", - e - )) - })?; + let entry = TransparencyLogEntry::from_json(json).map_err(|e| { + WSError::InternalError(format!( + "Failed to deserialize transparency log entry: {}", + e + )) + })?; - return Ok(Some(entry)); - } + return Ok(Some(entry)); + } } Ok(None) } @@ -2965,7 +2971,7 @@ pub fn embed_slsa_provenance( signer: &dyn crate::dsse::DsseSigner, ) -> Result { use crate::dsse::DsseEnvelope; - use crate::intoto::{predicate_types, Statement, Subject}; + use crate::intoto::{Statement, Subject, predicate_types}; use sha2::{Digest, Sha256}; // Compute module hash for subject @@ -2988,8 +2994,10 @@ pub fn embed_slsa_provenance( // Serialize and embed let envelope_json = envelope.to_json()?; - let custom_section = - CustomSection::new(DSSE_ATTESTATION_SECTION.to_string(), envelope_json.into_bytes()); + let custom_section = CustomSection::new( + DSSE_ATTESTATION_SECTION.to_string(), + envelope_json.into_bytes(), + ); module.sections.push(Section::Custom(custom_section)); Ok(module) @@ -3010,7 +3018,7 @@ pub fn embed_transformation_dsse( signer: &dyn crate::dsse::DsseSigner, ) -> Result { use crate::dsse::DsseEnvelope; - use crate::intoto::{predicate_types, Statement, Subject}; + use crate::intoto::{Statement, Subject, predicate_types}; use sha2::{Digest, Sha256}; // Compute module hash for subject @@ -3033,8 +3041,10 @@ pub fn embed_transformation_dsse( // Serialize and embed let envelope_json = envelope.to_json()?; - let custom_section = - CustomSection::new(DSSE_ATTESTATION_SECTION.to_string(), envelope_json.into_bytes()); + let custom_section = CustomSection::new( + DSSE_ATTESTATION_SECTION.to_string(), + envelope_json.into_bytes(), + ); module.sections.push(Section::Custom(custom_section)); Ok(module) @@ -3046,7 +3056,9 @@ pub fn embed_transformation_dsse( /// - Verified with any DSSE-compatible tool /// - Saved as a standalone .sigstore bundle /// - Parsed to extract the in-toto statement -pub fn extract_dsse_attestation(module: &Module) -> Result, WSError> { +pub fn extract_dsse_attestation( + module: &Module, +) -> Result, WSError> { for section in &module.sections { if let Section::Custom(custom) = section { if custom.name() == DSSE_ATTESTATION_SECTION { @@ -3134,12 +3146,13 @@ pub fn validate_device_attestation( // Validate device ID matches expected (if provided) if let Some(expected) = _expected_device_id - && attestation.device_id != expected { - return Err(format!( - "Device ID mismatch: expected '{}', got '{}'", - expected, attestation.device_id - )); - } + && attestation.device_id != expected + { + return Err(format!( + "Device ID mismatch: expected '{}', got '{}'", + expected, attestation.device_id + )); + } // Validate timestamp format if chrono::DateTime::parse_from_rfc3339(&attestation.timestamp).is_err() { @@ -4911,10 +4924,9 @@ mod tests { let mut policy = ChainVerificationPolicy::default(); policy.mode = ChainVerificationMode::NoRootSignaturesRequired; policy.verify_attestation_signatures = false; - policy.trusted_tools.insert( - "loom".to_string(), - TrustedToolInfo::min_version("0.1.0"), - ); + policy + .trusted_tools + .insert("loom".to_string(), TrustedToolInfo::min_version("0.1.0")); // Verify should pass because signature verification is disabled let result = verify_transformation_chain(&attestation, &policy); diff --git a/src/lib/src/container/bundle.rs b/src/lib/src/container/bundle.rs index 8a9e4b4..8fbfd38 100644 --- a/src/lib/src/container/bundle.rs +++ b/src/lib/src/container/bundle.rs @@ -206,9 +206,7 @@ impl SigstoreBundle { } /// Build a `TransparencyLogEntry` from a wsc `RekorEntry`. -fn build_tlog_entry( - rekor: &crate::signature::keyless::rekor::RekorEntry, -) -> TransparencyLogEntry { +fn build_tlog_entry(rekor: &crate::signature::keyless::rekor::RekorEntry) -> TransparencyLogEntry { // Parse inclusion proof from the serialized bytes if available. let inclusion_proof = if rekor.inclusion_proof.is_empty() { None @@ -277,7 +275,9 @@ fn pem_to_der(pem_str: &str) -> Vec { .join(""); // Decode base64 - BASE64.decode(&b64).unwrap_or_else(|_| pem_str.as_bytes().to_vec()) + BASE64 + .decode(&b64) + .unwrap_or_else(|_| pem_str.as_bytes().to_vec()) } #[cfg(test)] @@ -330,7 +330,11 @@ mod tests { assert_eq!(bundle.media_type, BUNDLE_MEDIA_TYPE); assert_eq!( - bundle.verification_material.x509_certificate_chain.certificates.len(), + bundle + .verification_material + .x509_certificate_chain + .certificates + .len(), 2 ); assert_eq!(bundle.verification_material.tlog_entries.len(), 1); @@ -340,7 +344,10 @@ mod tests { assert_eq!(bundle.message_signature.signature, expected_sig_b64); // Check digest - assert_eq!(bundle.message_signature.message_digest.algorithm, "SHA2_256"); + assert_eq!( + bundle.message_signature.message_digest.algorithm, + "SHA2_256" + ); assert_eq!(bundle.message_signature.message_digest.digest, "deadbeef"); } @@ -397,8 +404,16 @@ mod tests { assert_eq!(parsed.media_type, bundle.media_type); assert_eq!( - parsed.verification_material.x509_certificate_chain.certificates.len(), - bundle.verification_material.x509_certificate_chain.certificates.len() + parsed + .verification_material + .x509_certificate_chain + .certificates + .len(), + bundle + .verification_material + .x509_certificate_chain + .certificates + .len() ); assert_eq!( parsed.verification_material.tlog_entries.len(), diff --git a/src/lib/src/container/cosign.rs b/src/lib/src/container/cosign.rs index 0eefb28..e1d1ba5 100644 --- a/src/lib/src/container/cosign.rs +++ b/src/lib/src/container/cosign.rs @@ -217,9 +217,9 @@ impl CosignDelegator { } } - let output = cmd.output().map_err(|e| { - WSError::InternalError(format!("Failed to execute cosign: {}", e)) - })?; + let output = cmd + .output() + .map_err(|e| WSError::InternalError(format!("Failed to execute cosign: {}", e)))?; let stdout = String::from_utf8_lossy(&output.stdout).to_string(); let stderr = String::from_utf8_lossy(&output.stderr).to_string(); @@ -255,7 +255,9 @@ impl CosignDelegator { // Use digest reference if available (preferred), otherwise full reference let reference = if image.has_digest() { - image.digest_reference().unwrap_or_else(|_| image.full_reference()) + image + .digest_reference() + .unwrap_or_else(|_| image.full_reference()) } else { image.full_reference() }; diff --git a/src/lib/src/container/digest.rs b/src/lib/src/container/digest.rs index edd57f9..03294f7 100644 --- a/src/lib/src/container/digest.rs +++ b/src/lib/src/container/digest.rs @@ -174,7 +174,12 @@ fn resolve_with_crane(reference: &str) -> Option { fn resolve_with_skopeo(reference: &str) -> Option { Command::new("skopeo") - .args(["inspect", "--format", "{{.Digest}}", &format!("docker://{}", reference)]) + .args([ + "inspect", + "--format", + "{{.Digest}}", + &format!("docker://{}", reference), + ]) .output() .ok() .filter(|o| o.status.success()) diff --git a/src/lib/src/container/referrer.rs b/src/lib/src/container/referrer.rs index 286c70c..2653426 100644 --- a/src/lib/src/container/referrer.rs +++ b/src/lib/src/container/referrer.rs @@ -42,12 +42,8 @@ impl ArtifactType { /// Return the media type string for this artifact type. pub fn as_str(&self) -> &str { match self { - ArtifactType::SigstoreBundleV03 => { - "application/vnd.dev.sigstore.bundle.v0.3+json" - } - ArtifactType::CosignSimpleSigning => { - "application/vnd.dev.cosign.simplesigning.v1+json" - } + ArtifactType::SigstoreBundleV03 => "application/vnd.dev.sigstore.bundle.v0.3+json", + ArtifactType::CosignSimpleSigning => "application/vnd.dev.cosign.simplesigning.v1+json", ArtifactType::Custom(s) => s.as_str(), } } @@ -56,9 +52,7 @@ impl ArtifactType { pub fn from_str(s: &str) -> Self { match s { "application/vnd.dev.sigstore.bundle.v0.3+json" => ArtifactType::SigstoreBundleV03, - "application/vnd.dev.cosign.simplesigning.v1+json" => { - ArtifactType::CosignSimpleSigning - } + "application/vnd.dev.cosign.simplesigning.v1+json" => ArtifactType::CosignSimpleSigning, other => ArtifactType::Custom(other.to_string()), } } @@ -267,7 +261,10 @@ fn probe_with_crane(registry: &str) -> Option { // crane does not have a direct referrers command, but we can try // to list the manifest and check the response headers/errors. let output = Command::new("crane") - .args(["manifest", &format!("{}/oci-conformance/test:latest", registry)]) + .args([ + "manifest", + &format!("{}/oci-conformance/test:latest", registry), + ]) .output() .ok()?; @@ -346,11 +343,7 @@ fn store_with_oras( file_path: &std::path::Path, artifact_type: &str, ) -> Option { - let file_arg = format!( - "{}:{}", - file_path.display(), - artifact_type - ); + let file_arg = format!("{}:{}", file_path.display(), artifact_type); let output = Command::new("oras") .args([ @@ -402,7 +395,10 @@ mod tests { fn test_artifact_type_sigstore_bundle() { let at = ArtifactType::SigstoreBundleV03; assert_eq!(at.as_str(), "application/vnd.dev.sigstore.bundle.v0.3+json"); - assert_eq!(at.to_string(), "application/vnd.dev.sigstore.bundle.v0.3+json"); + assert_eq!( + at.to_string(), + "application/vnd.dev.sigstore.bundle.v0.3+json" + ); } #[test] @@ -585,8 +581,14 @@ mod tests { #[test] fn test_artifact_type_equality() { - assert_eq!(ArtifactType::SigstoreBundleV03, ArtifactType::SigstoreBundleV03); - assert_ne!(ArtifactType::SigstoreBundleV03, ArtifactType::CosignSimpleSigning); + assert_eq!( + ArtifactType::SigstoreBundleV03, + ArtifactType::SigstoreBundleV03 + ); + assert_ne!( + ArtifactType::SigstoreBundleV03, + ArtifactType::CosignSimpleSigning + ); assert_ne!( ArtifactType::SigstoreBundleV03, ArtifactType::Custom("application/vnd.dev.sigstore.bundle.v0.3+json".to_string()) diff --git a/src/lib/src/dsse.rs b/src/lib/src/dsse.rs index ca6fce2..59c2581 100644 --- a/src/lib/src/dsse.rs +++ b/src/lib/src/dsse.rs @@ -24,7 +24,7 @@ //! let verified_payload = envelope.verify(&verifier)?; //! ``` -use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; +use base64::{Engine, engine::general_purpose::STANDARD as BASE64}; use serde::{Deserialize, Serialize}; use crate::error::WSError; @@ -148,9 +148,9 @@ impl DsseEnvelope { } // Decode payload - let payload = BASE64.decode(&self.payload).map_err(|e| { - WSError::InternalError(format!("Invalid base64 payload: {}", e)) - })?; + let payload = BASE64 + .decode(&self.payload) + .map_err(|e| WSError::InternalError(format!("Invalid base64 payload: {}", e)))?; // Compute PAE let pae = compute_pae(&self.payload_type, &payload); @@ -158,9 +158,9 @@ impl DsseEnvelope { // Verify at least one signature let mut verified = false; for sig in &self.signatures { - let sig_bytes = BASE64.decode(&sig.sig).map_err(|e| { - WSError::InternalError(format!("Invalid base64 signature: {}", e)) - })?; + let sig_bytes = BASE64 + .decode(&sig.sig) + .map_err(|e| WSError::InternalError(format!("Invalid base64 signature: {}", e)))?; if verifier.verify(&pae, &sig_bytes).is_ok() { verified = true; @@ -183,16 +183,16 @@ impl DsseEnvelope { return Err(WSError::VerificationFailed); } - let payload = BASE64.decode(&self.payload).map_err(|e| { - WSError::InternalError(format!("Invalid base64 payload: {}", e)) - })?; + let payload = BASE64 + .decode(&self.payload) + .map_err(|e| WSError::InternalError(format!("Invalid base64 payload: {}", e)))?; let pae = compute_pae(&self.payload_type, &payload); for sig in &self.signatures { - let sig_bytes = BASE64.decode(&sig.sig).map_err(|e| { - WSError::InternalError(format!("Invalid base64 signature: {}", e)) - })?; + let sig_bytes = BASE64 + .decode(&sig.sig) + .map_err(|e| WSError::InternalError(format!("Invalid base64 signature: {}", e)))?; verifier.verify(&pae, &sig_bytes)?; } @@ -207,9 +207,9 @@ impl DsseEnvelope { /// This does not verify signatures. Use only when verification /// is done separately or not required. pub fn payload_bytes(&self) -> Result, WSError> { - BASE64.decode(&self.payload).map_err(|e| { - WSError::InternalError(format!("Invalid base64 payload: {}", e)) - }) + BASE64 + .decode(&self.payload) + .map_err(|e| WSError::InternalError(format!("Invalid base64 payload: {}", e))) } /// Serialize to JSON @@ -228,9 +228,8 @@ impl DsseEnvelope { /// Deserialize from JSON pub fn from_json(json: &str) -> Result { - serde_json::from_str(json).map_err(|e| { - WSError::InternalError(format!("Failed to parse DSSE envelope: {}", e)) - }) + serde_json::from_str(json) + .map_err(|e| WSError::InternalError(format!("Failed to parse DSSE envelope: {}", e))) } /// Create an unsigned envelope (for testing or deferred signing) @@ -298,8 +297,8 @@ impl Ed25519DsseSigner { /// Create from raw secret key bytes pub fn from_bytes(bytes: &[u8], key_id: Option) -> Result { - let secret_key = ed25519_compact::SecretKey::from_slice(bytes) - .map_err(|e| WSError::CryptoError(e))?; + let secret_key = + ed25519_compact::SecretKey::from_slice(bytes).map_err(|e| WSError::CryptoError(e))?; Ok(Self { secret_key, key_id }) } } @@ -328,8 +327,8 @@ impl Ed25519DsseVerifier { /// Create from raw public key bytes pub fn from_bytes(bytes: &[u8]) -> Result { - let public_key = ed25519_compact::PublicKey::from_slice(bytes) - .map_err(|e| WSError::CryptoError(e))?; + let public_key = + ed25519_compact::PublicKey::from_slice(bytes).map_err(|e| WSError::CryptoError(e))?; Ok(Self { public_key }) } } @@ -390,11 +389,7 @@ mod tests { let verifier = Ed25519DsseVerifier::new(pk); let payload = b"test payload"; - let envelope = DsseEnvelope::sign( - payload, - payload_types::IN_TOTO, - &signer, - ).unwrap(); + let envelope = DsseEnvelope::sign(payload, payload_types::IN_TOTO, &signer).unwrap(); assert_eq!(envelope.payload_type, payload_types::IN_TOTO); assert_eq!(envelope.signatures.len(), 1); @@ -409,11 +404,7 @@ mod tests { let (sk, _pk) = generate_test_keypair(); let signer = Ed25519DsseSigner::new(sk, None); - let envelope = DsseEnvelope::sign( - b"test data", - "application/json", - &signer, - ).unwrap(); + let envelope = DsseEnvelope::sign(b"test data", "application/json", &signer).unwrap(); let json = envelope.to_json().unwrap(); let parsed = DsseEnvelope::from_json(&json).unwrap(); @@ -437,7 +428,8 @@ mod tests { b"multi-signed payload", "application/json", &[&signer1, &signer2], - ).unwrap(); + ) + .unwrap(); assert_eq!(envelope.signatures.len(), 2); @@ -454,11 +446,7 @@ mod tests { let signer = Ed25519DsseSigner::new(sk, None); let wrong_verifier = Ed25519DsseVerifier::new(other_pk); - let envelope = DsseEnvelope::sign( - b"test", - "application/json", - &signer, - ).unwrap(); + let envelope = DsseEnvelope::sign(b"test", "application/json", &signer).unwrap(); assert!(envelope.verify(&wrong_verifier).is_err()); } @@ -595,6 +583,9 @@ mod proofs { fn proof_pae_length_prefix_prevents_ambiguity() { let pae_a = compute_pae("a", b""); let pae_b = compute_pae("", b"a"); - assert_ne!(pae_a, pae_b, "PAE ambiguity: different type/payload split produced same encoding"); + assert_ne!( + pae_a, pae_b, + "PAE ambiguity: different type/payload split produced same encoding" + ); } } diff --git a/src/lib/src/format/elf.rs b/src/lib/src/format/elf.rs index 84089cc..478d624 100644 --- a/src/lib/src/format/elf.rs +++ b/src/lib/src/format/elf.rs @@ -75,7 +75,7 @@ impl ElfArtifact { _ => { return Err(WSError::InternalError( "Invalid ELF class (expected 32-bit or 64-bit)".into(), - )) + )); } }; @@ -86,7 +86,7 @@ impl ElfArtifact { _ => { return Err(WSError::InternalError( "Invalid ELF data encoding (expected LE or BE)".into(), - )) + )); } }; @@ -219,9 +219,7 @@ impl ElfArtifact { } // Check section is within file bounds - let sh_end = sh_offset - .checked_add(sh_size) - .ok_or(WSError::ParseError)?; + let sh_end = sh_offset.checked_add(sh_size).ok_or(WSError::ParseError)?; if sh_end > data.len() { return Err(WSError::InternalError(format!( "ELF section {} extends beyond file (offset: {}, size: {})", diff --git a/src/lib/src/format/mcuboot.rs b/src/lib/src/format/mcuboot.rs index 3604f7a..e40223f 100644 --- a/src/lib/src/format/mcuboot.rs +++ b/src/lib/src/format/mcuboot.rs @@ -76,14 +76,12 @@ impl McubootArtifact { let is_little_endian = true; // Read ih_img_size from header (offset 12, 4 bytes LE) - let header_img_size = u32::from_le_bytes( - data[12..16].try_into().map_err(|_| WSError::ParseError)?, - ); + let header_img_size = + u32::from_le_bytes(data[12..16].try_into().map_err(|_| WSError::ParseError)?); // Read ih_hdr_size from header (offset 8, 2 bytes LE) - let hdr_size = u16::from_le_bytes( - data[8..10].try_into().map_err(|_| WSError::ParseError)?, - ) as u32; + let hdr_size = + u16::from_le_bytes(data[8..10].try_into().map_err(|_| WSError::ParseError)?) as u32; // The total image content = header + payload // ih_img_size is the payload size (after header) @@ -116,9 +114,7 @@ impl McubootArtifact { Maximum expected TLV trailer is {} bytes. This may indicate a \ partial-image attack where ih_img_size was reduced to exclude \ payload from signing (SC-36 / H-38)", - trailing_bytes, - declared_total, - MAX_TLV_OVERHEAD, + trailing_bytes, declared_total, MAX_TLV_OVERHEAD, ))); } @@ -141,9 +137,7 @@ impl McubootArtifact { /// Get the image payload (header + image content, excluding TLV). pub fn payload(&self) -> &[u8] { - let hdr_size = u16::from_le_bytes( - self.data[8..10].try_into().unwrap_or([0; 2]), - ) as usize; + let hdr_size = u16::from_le_bytes(self.data[8..10].try_into().unwrap_or([0; 2])) as usize; let end = hdr_size + self.verified_img_size as usize; &self.data[..end.min(self.data.len())] } diff --git a/src/lib/src/format/mod.rs b/src/lib/src/format/mod.rs index 775acb8..9875bc5 100644 --- a/src/lib/src/format/mod.rs +++ b/src/lib/src/format/mod.rs @@ -116,10 +116,7 @@ pub trait SignableArtifact: Sized { /// /// Used when both --format flag and file content are available (SC-15). /// Returns error if they disagree, preventing polyglot attacks (AS-17). -pub fn validate_format_consistency( - declared: FormatType, - data: &[u8], -) -> Result<(), WSError> { +pub fn validate_format_consistency(declared: FormatType, data: &[u8]) -> Result<(), WSError> { if let Some(detected) = FormatType::detect(data) { if detected != declared { return Err(WSError::InternalError(format!( @@ -151,7 +148,10 @@ mod tests { #[test] fn test_format_detection_mcuboot() { let mcuboot_magic = [0x3d, 0xb8, 0xf3, 0x96]; - assert_eq!(FormatType::detect(&mcuboot_magic), Some(FormatType::Mcuboot)); + assert_eq!( + FormatType::detect(&mcuboot_magic), + Some(FormatType::Mcuboot) + ); } #[test] @@ -171,7 +171,10 @@ mod tests { assert_eq!(FormatType::from_str("wasm").unwrap(), FormatType::Wasm); assert_eq!(FormatType::from_str("elf").unwrap(), FormatType::Elf); assert_eq!(FormatType::from_str("ELF").unwrap(), FormatType::Elf); - assert_eq!(FormatType::from_str("mcuboot").unwrap(), FormatType::Mcuboot); + assert_eq!( + FormatType::from_str("mcuboot").unwrap(), + FormatType::Mcuboot + ); assert!(FormatType::from_str("unknown").is_err()); } diff --git a/src/lib/src/http/mod.rs b/src/lib/src/http/mod.rs index 98436bd..ec478ac 100644 --- a/src/lib/src/http/mod.rs +++ b/src/lib/src/http/mod.rs @@ -147,9 +147,9 @@ mod sync_impl { request = request.header(key, value); } - let response = request.call().map_err(|e| { - WSError::InternalError(format!("HTTP GET failed: {}", e)) - })?; + let response = request + .call() + .map_err(|e| WSError::InternalError(format!("HTTP GET failed: {}", e)))?; let status = response.status().as_u16(); let mut response_headers = HashMap::new(); @@ -159,10 +159,9 @@ mod sync_impl { } } - let body = response - .into_body() - .read_to_vec() - .map_err(|e| WSError::InternalError(format!("Failed to read response body: {}", e)))?; + let body = response.into_body().read_to_vec().map_err(|e| { + WSError::InternalError(format!("Failed to read response body: {}", e)) + })?; Ok(HttpResponse { status, @@ -192,9 +191,9 @@ mod sync_impl { request = request.header(key, value); } - let response = request.send(body).map_err(|e| { - WSError::InternalError(format!("HTTP POST failed: {}", e)) - })?; + let response = request + .send(body) + .map_err(|e| WSError::InternalError(format!("HTTP POST failed: {}", e)))?; let status = response.status().as_u16(); let mut response_headers = HashMap::new(); @@ -204,10 +203,9 @@ mod sync_impl { } } - let body = response - .into_body() - .read_to_vec() - .map_err(|e| WSError::InternalError(format!("Failed to read response body: {}", e)))?; + let body = response.into_body().read_to_vec().map_err(|e| { + WSError::InternalError(format!("Failed to read response body: {}", e)) + })?; Ok(HttpResponse { status, @@ -234,7 +232,9 @@ mod async_impl { .user_agent(&self.user_agent) .timeout(std::time::Duration::from_secs(self.timeout_secs)) .build() - .map_err(|e| WSError::InternalError(format!("Failed to create HTTP client: {}", e)))?; + .map_err(|e| { + WSError::InternalError(format!("Failed to create HTTP client: {}", e)) + })?; let mut request = client.get(url); @@ -242,9 +242,10 @@ mod async_impl { request = request.header(key, value); } - let response = request.send().await.map_err(|e| { - WSError::InternalError(format!("HTTP GET failed: {}", e)) - })?; + let response = request + .send() + .await + .map_err(|e| WSError::InternalError(format!("HTTP GET failed: {}", e)))?; let status = response.status().as_u16(); let response_headers: HashMap = response @@ -275,7 +276,9 @@ mod async_impl { .user_agent(&self.user_agent) .timeout(std::time::Duration::from_secs(self.timeout_secs)) .build() - .map_err(|e| WSError::InternalError(format!("Failed to create HTTP client: {}", e)))?; + .map_err(|e| { + WSError::InternalError(format!("Failed to create HTTP client: {}", e)) + })?; let mut request = client .post(url) @@ -286,9 +289,10 @@ mod async_impl { request = request.header(key, value); } - let response = request.send().await.map_err(|e| { - WSError::InternalError(format!("HTTP POST failed: {}", e)) - })?; + let response = request + .send() + .await + .map_err(|e| WSError::InternalError(format!("HTTP POST failed: {}", e)))?; let status = response.status().as_u16(); let response_headers: HashMap = response @@ -429,20 +433,24 @@ mod pinned_sync_impl { // Create connector chain with our pinned TLS config let pinned_connector = PinnedRustlsConnectorFromConfig::new(self.tls_config.clone()); - let connector = () - .chain(TcpConnector::default()) - .chain(pinned_connector); + let connector = ().chain(TcpConnector::default()).chain(pinned_connector); let config = ureq::config::Config::builder() .http_status_as_error(false) .timeout_global(Some(std::time::Duration::from_secs(self.timeout_secs))) .build(); - Ok(ureq::Agent::with_parts(config, connector, DefaultResolver::default())) + Ok(ureq::Agent::with_parts( + config, + connector, + DefaultResolver::default(), + )) } } - fn convert_ureq_response(response: http::Response) -> Result { + fn convert_ureq_response( + response: http::Response, + ) -> Result { let status = response.status().as_u16(); let mut response_headers = HashMap::new(); for (name, value) in response.headers() { @@ -482,8 +490,8 @@ mod pinned_sync_impl { } } - impl - ureq::unversioned::transport::Connector for PinnedRustlsConnectorFromConfig + impl ureq::unversioned::transport::Connector + for PinnedRustlsConnectorFromConfig { type Out = ureq::unversioned::transport::Either< In, @@ -609,10 +617,9 @@ mod pinned_async_impl { .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())) .collect(); - let body = response - .bytes() - .await - .map_err(|e| WSError::InternalError(format!("Failed to read response body: {}", e)))?; + let body = response.bytes().await.map_err(|e| { + WSError::InternalError(format!("Failed to read response body: {}", e)) + })?; Ok(HttpResponse { status, diff --git a/src/lib/src/intoto.rs b/src/lib/src/intoto.rs index 2ea7286..46a86f2 100644 --- a/src/lib/src/intoto.rs +++ b/src/lib/src/intoto.rs @@ -62,39 +62,34 @@ impl Statement

{ /// Serialize to JSON bytes (for DSSE payload) pub fn to_json_bytes(&self) -> Result, WSError> { - serde_json::to_vec(self).map_err(|e| { - WSError::InternalError(format!("Failed to serialize statement: {}", e)) - }) + serde_json::to_vec(self) + .map_err(|e| WSError::InternalError(format!("Failed to serialize statement: {}", e))) } /// Serialize to JSON string pub fn to_json(&self) -> Result { - serde_json::to_string(self).map_err(|e| { - WSError::InternalError(format!("Failed to serialize statement: {}", e)) - }) + serde_json::to_string(self) + .map_err(|e| WSError::InternalError(format!("Failed to serialize statement: {}", e))) } /// Serialize to pretty JSON string pub fn to_json_pretty(&self) -> Result { - serde_json::to_string_pretty(self).map_err(|e| { - WSError::InternalError(format!("Failed to serialize statement: {}", e)) - }) + serde_json::to_string_pretty(self) + .map_err(|e| WSError::InternalError(format!("Failed to serialize statement: {}", e))) } } impl Deserialize<'de>> Statement

{ /// Deserialize from JSON bytes pub fn from_json_bytes(bytes: &[u8]) -> Result { - serde_json::from_slice(bytes).map_err(|e| { - WSError::InternalError(format!("Failed to parse statement: {}", e)) - }) + serde_json::from_slice(bytes) + .map_err(|e| WSError::InternalError(format!("Failed to parse statement: {}", e))) } /// Deserialize from JSON string pub fn from_json(json: &str) -> Result { - serde_json::from_str(json).map_err(|e| { - WSError::InternalError(format!("Failed to parse statement: {}", e)) - }) + serde_json::from_str(json) + .map_err(|e| WSError::InternalError(format!("Failed to parse statement: {}", e))) } } @@ -129,7 +124,7 @@ impl Subject { /// Create a subject from raw bytes (computes SHA256) pub fn from_bytes(name: impl Into, bytes: &[u8]) -> Self { - use sha2::{Sha256, Digest}; + use sha2::{Digest, Sha256}; let hash = Sha256::digest(bytes); Self::new(name, hex::encode(hash)) } @@ -263,7 +258,7 @@ impl ResourceDescriptor { /// Create from raw bytes (computes SHA256) pub fn from_bytes(name: impl Into, bytes: &[u8]) -> Self { - use sha2::{Sha256, Digest}; + use sha2::{Digest, Sha256}; let hash = Sha256::digest(bytes); Self::from_name(name, hex::encode(hash)) } @@ -376,7 +371,9 @@ mod tests { let original = Statement::new( vec![Subject::new("test.bin", "123456")], "https://example.com/predicate/v1", - TestPredicate { value: "test".to_string() }, + TestPredicate { + value: "test".to_string(), + }, ); let json = original.to_json().unwrap(); diff --git a/src/lib/src/metrics/mod.rs b/src/lib/src/metrics/mod.rs index f383585..8fcd2ba 100644 --- a/src/lib/src/metrics/mod.rs +++ b/src/lib/src/metrics/mod.rs @@ -127,9 +127,7 @@ pub struct Histogram { impl Histogram { /// Create a histogram with the given bucket boundaries pub fn new(boundaries: Vec) -> Self { - let buckets = (0..=boundaries.len()) - .map(|_| AtomicU64::new(0)) - .collect(); + let buckets = (0..=boundaries.len()).map(|_| AtomicU64::new(0)).collect(); Self { boundaries, @@ -434,8 +432,14 @@ mod tests { metrics.record_signing_failure(SigningFailure::TokenInvalid); metrics.record_signing_failure(SigningFailure::NetworkError); - assert_eq!(metrics.signing_failures.get(&SigningFailure::TokenInvalid), 2); - assert_eq!(metrics.signing_failures.get(&SigningFailure::NetworkError), 1); + assert_eq!( + metrics.signing_failures.get(&SigningFailure::TokenInvalid), + 2 + ); + assert_eq!( + metrics.signing_failures.get(&SigningFailure::NetworkError), + 1 + ); assert_eq!(metrics.signing_failures.total(), 3); } @@ -443,19 +447,19 @@ mod tests { fn test_histogram() { let hist = Histogram::new(vec![10, 50, 100]); - hist.record(5); // bucket 0 (<=10) - hist.record(25); // bucket 1 (<=50) - hist.record(75); // bucket 2 (<=100) + hist.record(5); // bucket 0 (<=10) + hist.record(25); // bucket 1 (<=50) + hist.record(75); // bucket 2 (<=100) hist.record(200); // bucket 3 (+Inf) assert_eq!(hist.count(), 4); assert_eq!(hist.sum(), 305); let snapshot = hist.snapshot(); - assert_eq!(snapshot[0], (10, 1)); // <=10: 1 - assert_eq!(snapshot[1], (50, 1)); // <=50: 1 + assert_eq!(snapshot[0], (10, 1)); // <=10: 1 + assert_eq!(snapshot[1], (50, 1)); // <=50: 1 assert_eq!(snapshot[2], (100, 1)); // <=100: 1 - assert_eq!(snapshot[3].1, 1); // +Inf: 1 + assert_eq!(snapshot[3].1, 1); // +Inf: 1 } #[test] diff --git a/src/lib/src/platform/keyring_storage.rs b/src/lib/src/platform/keyring_storage.rs index 7de789e..6c870e4 100644 --- a/src/lib/src/platform/keyring_storage.rs +++ b/src/lib/src/platform/keyring_storage.rs @@ -51,7 +51,6 @@ /// // Key persists across restarts /// let handle2 = provider.load_key("key-123")?; /// ``` - use super::{Attestation, KeyHandle, SecureKeyProvider, SecurityLevel}; use crate::error::WSError; use crate::signature::{KeyPair, PublicKey}; @@ -96,7 +95,9 @@ impl KeyCache { } fn get_by_id(&self, key_id: &str) -> Option { - self.id_to_handle.get(key_id).map(|&h| KeyHandle::from_raw(h)) + self.id_to_handle + .get(key_id) + .map(|&h| KeyHandle::from_raw(h)) } fn remove(&mut self, handle: KeyHandle) -> Option<(String, KeyPair)> { @@ -166,20 +167,22 @@ impl KeyringProvider { /// Store a secret key in the keyring fn store_secret_key(&self, key_id: &str, secret_bytes: &[u8]) -> Result<(), WSError> { - let entry = keyring::Entry::new(&self.service_name(), key_id) - .map_err(|e| WSError::InternalError(format!("Failed to create keyring entry: {}", e)))?; + let entry = keyring::Entry::new(&self.service_name(), key_id).map_err(|e| { + WSError::InternalError(format!("Failed to create keyring entry: {}", e)) + })?; - entry - .set_secret(secret_bytes) - .map_err(|e| WSError::InternalError(format!("Failed to store key in keyring: {}", e)))?; + entry.set_secret(secret_bytes).map_err(|e| { + WSError::InternalError(format!("Failed to store key in keyring: {}", e)) + })?; Ok(()) } /// Load a secret key from the keyring fn load_secret_key(&self, key_id: &str) -> Result>, WSError> { - let entry = keyring::Entry::new(&self.service_name(), key_id) - .map_err(|e| WSError::InternalError(format!("Failed to create keyring entry: {}", e)))?; + let entry = keyring::Entry::new(&self.service_name(), key_id).map_err(|e| { + WSError::InternalError(format!("Failed to create keyring entry: {}", e)) + })?; let secret = entry.get_secret().map_err(|e| match e { keyring::Error::NoEntry => { @@ -193,8 +196,9 @@ impl KeyringProvider { /// Delete a secret key from the keyring fn delete_secret_key(&self, key_id: &str) -> Result<(), WSError> { - let entry = keyring::Entry::new(&self.service_name(), key_id) - .map_err(|e| WSError::InternalError(format!("Failed to create keyring entry: {}", e)))?; + let entry = keyring::Entry::new(&self.service_name(), key_id).map_err(|e| { + WSError::InternalError(format!("Failed to create keyring entry: {}", e)) + })?; entry.delete_credential().map_err(|e| match e { keyring::Error::NoEntry => { diff --git a/src/lib/src/platform/mod.rs b/src/lib/src/platform/mod.rs index 385c344..fbb2fa0 100644 --- a/src/lib/src/platform/mod.rs +++ b/src/lib/src/platform/mod.rs @@ -165,7 +165,8 @@ pub trait HardwareVerifier: Send + Sync { /// /// - `Ok(())` if signature is valid /// - `Err(HardwareError::VerificationFailed)` if invalid - fn verify(&self, data: &[u8], signature: &[u8], public_key: &[u8]) -> Result<(), HardwareError>; + fn verify(&self, data: &[u8], signature: &[u8], public_key: &[u8]) + -> Result<(), HardwareError>; /// Get supported algorithms. fn supported_algorithms(&self) -> Vec; @@ -278,7 +279,12 @@ impl Default for SoftwareEd25519Verifier { } impl HardwareVerifier for SoftwareEd25519Verifier { - fn verify(&self, data: &[u8], signature: &[u8], public_key: &[u8]) -> Result<(), HardwareError> { + fn verify( + &self, + data: &[u8], + signature: &[u8], + public_key: &[u8], + ) -> Result<(), HardwareError> { if signature.len() != 64 { return Err(HardwareError::VerificationFailed( "Invalid signature length (expected 64 bytes)".to_string(), @@ -298,8 +304,9 @@ impl HardwareVerifier for SoftwareEd25519Verifier { HardwareError::VerificationFailed(format!("Invalid public key format: {:?}", e)) })?; - pk.verify(data, &sig) - .map_err(|_| HardwareError::VerificationFailed("Signature verification failed".to_string())) + pk.verify(data, &sig).map_err(|_| { + HardwareError::VerificationFailed("Signature verification failed".to_string()) + }) } fn supported_algorithms(&self) -> Vec { @@ -729,7 +736,11 @@ mod tests { // Test public key extraction let public_key = signer.public_key().expect("public key should be available"); - assert_eq!(public_key.len(), 32, "Ed25519 public key should be 32 bytes"); + assert_eq!( + public_key.len(), + 32, + "Ed25519 public key should be 32 bytes" + ); // Test key ID assert_eq!(signer.key_id(), Some("test-key".to_string())); @@ -755,7 +766,11 @@ mod tests { assert!(verifier.verify(data, &signature, &public_key).is_ok()); // Test failed verification with wrong data - assert!(verifier.verify(b"wrong data", &signature, &public_key).is_err()); + assert!( + verifier + .verify(b"wrong data", &signature, &public_key) + .is_err() + ); // Test failed verification with corrupted signature let mut bad_sig = signature.clone(); diff --git a/src/lib/src/platform/tpm2.rs b/src/lib/src/platform/tpm2.rs index 4d1860c..c315e32 100644 --- a/src/lib/src/platform/tpm2.rs +++ b/src/lib/src/platform/tpm2.rs @@ -48,6 +48,7 @@ use std::collections::HashMap; use std::sync::{Arc, Mutex}; use tss_esapi::{ + Context, attributes::ObjectAttributesBuilder, interface_types::{ algorithm::{HashingAlgorithm, PublicAlgorithm}, @@ -55,12 +56,11 @@ use tss_esapi::{ resource_handles::Hierarchy, }, structures::{ - EccPoint, EccScheme, HashScheme, KeyDerivationFunctionScheme, - PublicBuilder, PublicEccParametersBuilder, SignatureScheme, + EccPoint, EccScheme, HashScheme, KeyDerivationFunctionScheme, PublicBuilder, + PublicEccParametersBuilder, SignatureScheme, }, tcti_ldr::{DeviceConfig, NetworkTPMConfig, TctiNameConf}, utils::PublicKey as TssPublicKey, - Context, }; /// Signing algorithm used by a TPM key @@ -132,9 +132,8 @@ impl Tpm2Provider { /// let provider = Tpm2Provider::with_tcti(tcti)?; /// ``` pub fn with_tcti(tcti: TctiNameConf) -> Result { - let context = Context::new(tcti).map_err(|e| { - WSError::HardwareError(format!("Failed to connect to TPM: {}", e)) - })?; + let context = Context::new(tcti) + .map_err(|e| WSError::HardwareError(format!("Failed to connect to TPM: {}", e)))?; let context = Arc::new(Mutex::new(context)); @@ -172,9 +171,8 @@ impl Tpm2Provider { fn detect_tcti() -> Result { // Check environment variable first (allows override) if std::env::var("TPM2_TCTI").is_ok() { - return TctiNameConf::from_environment_variable().map_err(|e| { - WSError::HardwareError(format!("Invalid TPM2_TCTI: {}", e)) - }); + return TctiNameConf::from_environment_variable() + .map_err(|e| WSError::HardwareError(format!("Invalid TPM2_TCTI: {}", e))); } #[cfg(target_os = "linux")] @@ -218,11 +216,7 @@ impl Tpm2Provider { // Query supported ECC curves let (caps, _more) = ctx - .get_capability( - tss_esapi::constants::CapabilityType::EccCurves, - 0, - 100, - ) + .get_capability(tss_esapi::constants::CapabilityType::EccCurves, 0, 100) .map_err(|e| { WSError::HardwareError(format!("Failed to query TPM capabilities: {}", e)) })?; @@ -306,7 +300,9 @@ impl Tpm2Provider { .with_ecc_parameters(ecc_params) .with_ecc_unique_identifier(EccPoint::default()) .build() - .map_err(|e| WSError::InternalError(format!("Failed to build public template: {}", e)))?; + .map_err(|e| { + WSError::InternalError(format!("Failed to build public template: {}", e)) + })?; // Create the primary key under the owner hierarchy // Use execute_with_nullauth_session for proper authorization handling @@ -323,9 +319,7 @@ impl Tpm2Provider { } /// Extract public key bytes from TPM public structure - fn extract_ecc_public_key( - public: &tss_esapi::structures::Public, - ) -> Result, WSError> { + fn extract_ecc_public_key(public: &tss_esapi::structures::Public) -> Result, WSError> { // Convert Public to tss_esapi's PublicKey enum let public_key = TssPublicKey::try_from(public.clone()) .map_err(|e| WSError::InternalError(format!("Failed to extract public key: {}", e)))?; @@ -361,7 +355,11 @@ impl Tpm2Provider { fn encode_integer(val: &[u8]) -> Vec { let mut result = Vec::new(); // Skip leading zeros but keep at least one byte - let val = val.iter().skip_while(|&&b| b == 0).copied().collect::>(); + let val = val + .iter() + .skip_while(|&&b| b == 0) + .copied() + .collect::>(); let val = if val.is_empty() { vec![0] } else { val }; // Add leading zero if high bit is set (to keep positive) @@ -463,7 +461,9 @@ impl SecureKeyProvider for Tpm2Provider { // First get the key data (need to hold keys lock briefly) let tpm_handle = { let keys = self.keys.lock().unwrap(); - let key_data = keys.get(&handle.as_raw()).ok_or(WSError::InvalidKeyHandle)?; + let key_data = keys + .get(&handle.as_raw()) + .ok_or(WSError::InvalidKeyHandle)?; key_data.key_handle }; @@ -497,7 +497,9 @@ impl SecureKeyProvider for Tpm2Provider { fn get_public_key(&self, handle: KeyHandle) -> Result { let keys = self.keys.lock().unwrap(); - let _key_data = keys.get(&handle.as_raw()).ok_or(WSError::InvalidKeyHandle)?; + let _key_data = keys + .get(&handle.as_raw()) + .ok_or(WSError::InvalidKeyHandle)?; // For TPM2 with P-256, we store the uncompressed point (65 bytes) // The PublicKey struct expects ed25519 format, but we're using P-256 @@ -515,7 +517,8 @@ impl SecureKeyProvider for Tpm2Provider { fn delete_key(&self, handle: KeyHandle) -> Result<(), WSError> { let key_data = { let mut keys = self.keys.lock().unwrap(); - keys.remove(&handle.as_raw()).ok_or(WSError::InvalidKeyHandle)? + keys.remove(&handle.as_raw()) + .ok_or(WSError::InvalidKeyHandle)? }; // Flush the key from TPM @@ -544,7 +547,9 @@ impl Tpm2Provider { /// Returns the uncompressed P-256 point (65 bytes: 0x04 || x || y) pub fn get_public_key_bytes(&self, handle: KeyHandle) -> Result, WSError> { let keys = self.keys.lock().unwrap(); - let key_data = keys.get(&handle.as_raw()).ok_or(WSError::InvalidKeyHandle)?; + let key_data = keys + .get(&handle.as_raw()) + .ok_or(WSError::InvalidKeyHandle)?; Ok(key_data.public_key.clone()) } @@ -557,7 +562,7 @@ impl Tpm2Provider { data: &[u8], signature_der: &[u8], ) -> Result { - use p256::ecdsa::{signature::Verifier, Signature, VerifyingKey}; + use p256::ecdsa::{Signature, VerifyingKey, signature::Verifier}; let public_key_bytes = self.get_public_key_bytes(handle)?; diff --git a/src/lib/src/policy/eval.rs b/src/lib/src/policy/eval.rs index 12f8f9d..05c21df 100644 --- a/src/lib/src/policy/eval.rs +++ b/src/lib/src/policy/eval.rs @@ -3,9 +3,9 @@ //! This module provides the core policy evaluation logic that checks //! transformation attestations against supply chain policies. +use super::slsa::{SlsaLevel, SlsaLevelAnalysis, detect_slsa_level_detailed}; use super::{Enforcement, Policy, SlsaPolicy}; -use super::slsa::{SlsaLevel, detect_slsa_level_detailed, SlsaLevelAnalysis}; -use wsc_attestation::{TransformationAttestation, SignatureStatus}; +use wsc_attestation::{SignatureStatus, TransformationAttestation}; // ============================================================================ // Evaluation Result Types @@ -119,12 +119,15 @@ impl PolicyEvaluationResult { /// Get only report failures (warnings that didn't fail the policy). pub fn report_failures(&self) -> impl Iterator { - self.rules.iter().filter(|r| !r.passed && r.enforcement == Enforcement::Report) + self.rules + .iter() + .filter(|r| !r.passed && r.enforcement == Enforcement::Report) } /// Get suggestions for improving SLSA level. pub fn slsa_suggestions(&self) -> Vec { - self.summary.slsa_analysis + self.summary + .slsa_analysis .as_ref() .map(|a| a.suggestions_for_next_level()) .unwrap_or_default() @@ -200,7 +203,10 @@ pub fn evaluate_policy( let total_rules = rules.len(); let passed_count = rules.iter().filter(|r| r.passed).count(); let failed_strict = rules.iter().filter(|r| r.causes_failure()).count(); - let failed_report = rules.iter().filter(|r| !r.passed && r.enforcement == Enforcement::Report).count(); + let failed_report = rules + .iter() + .filter(|r| !r.passed && r.enforcement == Enforcement::Report) + .count(); let summary = PolicySummary { total_rules, @@ -241,21 +247,23 @@ fn check_slsa_level( "slsa.minimum_level", enforcement, format!("Detected {} meets requirement of {}", detected, required), - ).with_details(analysis.reasons.join("; ")) + ) + .with_details(analysis.reasons.join("; ")) } else { RuleResult::fail( "slsa.minimum_level", enforcement, - format!("Detected {} does not meet requirement of {}", detected, required), - ).with_details(analysis.reasons.join("; ")) + format!( + "Detected {} does not meet requirement of {}", + detected, required + ), + ) + .with_details(analysis.reasons.join("; ")) } } /// Check signature requirements. -fn check_signatures( - attestation: &TransformationAttestation, - policy: &Policy, -) -> Vec { +fn check_signatures(attestation: &TransformationAttestation, policy: &Policy) -> Vec { let mut results = Vec::new(); let enforcement = policy.effective_enforcement(policy.signatures.enforcement); @@ -266,12 +274,18 @@ fn check_signatures( if is_signed { let algo = &attestation.attestation_signature.algorithm; - let key_info = attestation.attestation_signature.key_id + let key_info = attestation + .attestation_signature + .key_id .as_ref() .map(|k| format!(" (key: {})", k)) - .or_else(|| attestation.attestation_signature.signer_identity - .as_ref() - .map(|s| format!(" (identity: {})", s))) + .or_else(|| { + attestation + .attestation_signature + .signer_identity + .as_ref() + .map(|s| format!(" (identity: {})", s)) + }) .unwrap_or_default(); results.push(RuleResult::pass( @@ -290,26 +304,38 @@ fn check_signatures( // Check root signature requirement if policy.signatures.require_root_signatures { - let all_verified = attestation.inputs.iter() + let all_verified = attestation + .inputs + .iter() .all(|i| i.signature_status == SignatureStatus::Verified); - let any_verified = attestation.inputs.iter() + let any_verified = attestation + .inputs + .iter() .any(|i| i.signature_status == SignatureStatus::Verified); if all_verified && !attestation.inputs.is_empty() { results.push(RuleResult::pass( "signatures.root", enforcement, - format!("All {} input(s) have verified signatures", attestation.inputs.len()), + format!( + "All {} input(s) have verified signatures", + attestation.inputs.len() + ), )); } else if any_verified { - let verified_count = attestation.inputs.iter() + let verified_count = attestation + .inputs + .iter() .filter(|i| i.signature_status == SignatureStatus::Verified) .count(); results.push(RuleResult::fail( "signatures.root", enforcement, - format!("Only {} of {} inputs have verified signatures", - verified_count, attestation.inputs.len()), + format!( + "Only {} of {} inputs have verified signatures", + verified_count, + attestation.inputs.len() + ), )); } else { results.push(RuleResult::fail( @@ -346,8 +372,10 @@ fn check_trusted_tool( return Some(RuleResult::fail( format!("trusted_tools.{}.version", tool_name), enforcement, - format!("Tool version {} does not meet minimum {}", - attestation.tool.version, min_version), + format!( + "Tool version {} does not meet minimum {}", + attestation.tool.version, min_version + ), )); } } @@ -357,8 +385,10 @@ fn check_trusted_tool( return Some(RuleResult::fail( format!("trusted_tools.{}.version", tool_name), enforcement, - format!("Tool version {} exceeds maximum {}", - attestation.tool.version, max_version), + format!( + "Tool version {} exceeds maximum {}", + attestation.tool.version, max_version + ), )); } } @@ -373,8 +403,10 @@ fn check_trusted_tool( return Some(RuleResult::fail( format!("trusted_tools.{}.hash", tool_name), enforcement, - format!("Tool hash {} does not match required {}", - actual_hash, required_hash), + format!( + "Tool hash {} does not match required {}", + actual_hash, required_hash + ), )); } None => { @@ -390,7 +422,10 @@ fn check_trusted_tool( Some(RuleResult::pass( format!("trusted_tools.{}", tool_name), enforcement, - format!("Tool '{}' version {} is trusted", tool_name, attestation.tool.version), + format!( + "Tool '{}' version {} is trusted", + tool_name, attestation.tool.version + ), )) } else { // Tool not in trusted list - this is a failure @@ -412,8 +447,7 @@ fn check_attestation_age( let enforcement = policy.effective_enforcement(policy.signatures.enforcement); // Parse the attestation timestamp - let attestation_time = chrono::DateTime::parse_from_rfc3339(&attestation.timestamp) - .ok()?; + let attestation_time = chrono::DateTime::parse_from_rfc3339(&attestation.timestamp).ok()?; let now = chrono::Utc::now(); let age = now.signed_duration_since(attestation_time); @@ -433,14 +467,21 @@ fn check_attestation_age( Some(RuleResult::fail( "signatures.attestation_age", enforcement, - format!("Attestation is {} days old, maximum allowed is {} days", days, max_days), + format!( + "Attestation is {} days old, maximum allowed is {} days", + days, max_days + ), )) } else { let days = age.num_days(); Some(RuleResult::pass( "signatures.attestation_age", enforcement, - format!("Attestation is {} days old (max: {} days)", days, max_age.as_secs() / (24 * 60 * 60)), + format!( + "Attestation is {} days old (max: {} days)", + days, + max_age.as_secs() / (24 * 60 * 60) + ), )) } } @@ -517,7 +558,10 @@ mod tests { let result = evaluate_policy(&attestation, &policy); // Strict policy should fail for unsigned attestation - assert!(!result.passed, "Strict policy should fail for unsigned attestation"); + assert!( + !result.passed, + "Strict policy should fail for unsigned attestation" + ); assert!(result.summary.failed_strict > 0); } @@ -531,13 +575,21 @@ mod tests { policy.slsa.enforcement = Some(Enforcement::Strict); let result = evaluate_policy(&attestation, &policy); - let slsa_rule = result.rules.iter().find(|r| r.rule == "slsa.minimum_level").unwrap(); + let slsa_rule = result + .rules + .iter() + .find(|r| r.rule == "slsa.minimum_level") + .unwrap(); assert!(slsa_rule.passed); // Policy requiring L2 should fail policy.slsa.minimum_level = 2; let result = evaluate_policy(&attestation, &policy); - let slsa_rule = result.rules.iter().find(|r| r.rule == "slsa.minimum_level").unwrap(); + let slsa_rule = result + .rules + .iter() + .find(|r| r.rule == "slsa.minimum_level") + .unwrap(); assert!(!slsa_rule.passed); } @@ -547,17 +599,24 @@ mod tests { let mut policy = Policy::permissive(); // Add loom to trusted tools - policy.add_trusted_tool("loom", crate::policy::TrustedToolPolicy { - min_version: Some("0.1.0".to_string()), - max_version: None, - required_hash: None, - public_keys: vec![], - keyless: None, - enforcement: Some(Enforcement::Strict), - }); + policy.add_trusted_tool( + "loom", + crate::policy::TrustedToolPolicy { + min_version: Some("0.1.0".to_string()), + max_version: None, + required_hash: None, + public_keys: vec![], + keyless: None, + enforcement: Some(Enforcement::Strict), + }, + ); let result = evaluate_policy(&attestation, &policy); - let tool_rule = result.rules.iter().find(|r| r.rule.starts_with("trusted_tools")).unwrap(); + let tool_rule = result + .rules + .iter() + .find(|r| r.rule.starts_with("trusted_tools")) + .unwrap(); assert!(tool_rule.passed); assert!(result.summary.tools_verified.contains(&"loom".to_string())); } @@ -572,7 +631,11 @@ mod tests { policy.add_trusted_tool("wac", crate::policy::TrustedToolPolicy::default()); let result = evaluate_policy(&attestation, &policy); - let tool_rule = result.rules.iter().find(|r| r.rule == "trusted_tools").unwrap(); + let tool_rule = result + .rules + .iter() + .find(|r| r.rule == "trusted_tools") + .unwrap(); assert!(!tool_rule.passed); assert!(tool_rule.message.contains("not in the trusted tools list")); } @@ -594,10 +657,13 @@ mod tests { let attestation = create_test_attestation(); // loom 0.2.0 let mut policy = Policy::permissive(); - policy.add_trusted_tool("loom", crate::policy::TrustedToolPolicy { - min_version: Some("0.3.0".to_string()), // Higher than 0.2.0 - ..Default::default() - }); + policy.add_trusted_tool( + "loom", + crate::policy::TrustedToolPolicy { + min_version: Some("0.3.0".to_string()), // Higher than 0.2.0 + ..Default::default() + }, + ); policy.policy.enforcement = Enforcement::Strict; let result = evaluate_policy(&attestation, &policy); diff --git a/src/lib/src/policy/mod.rs b/src/lib/src/policy/mod.rs index 64e5b9d..b21160c 100644 --- a/src/lib/src/policy/mod.rs +++ b/src/lib/src/policy/mod.rs @@ -51,14 +51,14 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::time::Duration; -pub mod slsa; pub mod eval; +pub mod slsa; #[cfg(feature = "rego")] pub mod rego; +pub use eval::{PolicyEvaluationResult, PolicySummary, RuleResult, evaluate_policy}; pub use slsa::{SlsaLevel, detect_slsa_level}; -pub use eval::{evaluate_policy, PolicyEvaluationResult, RuleResult, PolicySummary}; // ============================================================================ // Core Policy Types @@ -485,10 +485,7 @@ allowed_repos = ["pulseengine/*"] policy.policy.enforcement = Enforcement::Report; // Section without override uses default - assert_eq!( - policy.effective_enforcement(None), - Enforcement::Report - ); + assert_eq!(policy.effective_enforcement(None), Enforcement::Report); // Section with override uses override assert_eq!( diff --git a/src/lib/src/policy/rego.rs b/src/lib/src/policy/rego.rs index 269485a..6b1e7a0 100644 --- a/src/lib/src/policy/rego.rs +++ b/src/lib/src/policy/rego.rs @@ -218,8 +218,8 @@ impl RegoEngine { pub fn set_data_file(&mut self, path: &str) -> Result<(), RegoError> { let content = std::fs::read_to_string(path) .map_err(|e| RegoError::IoError(format!("{}: {}", path, e)))?; - let data: serde_json::Value = serde_json::from_str(&content) - .map_err(|e| RegoError::SerdeError(e.to_string()))?; + let data: serde_json::Value = + serde_json::from_str(&content).map_err(|e| RegoError::SerdeError(e.to_string()))?; self.set_data(data) } @@ -235,8 +235,8 @@ impl RegoEngine { } // Set input - let input_json = serde_json::to_value(input) - .map_err(|e| RegoError::SerdeError(e.to_string()))?; + let input_json = + serde_json::to_value(input).map_err(|e| RegoError::SerdeError(e.to_string()))?; let input_regorus = json_to_regorus(&input_json)?; self.engine.set_input(input_regorus); @@ -264,14 +264,20 @@ impl RegoEngine { } // Evaluate violations rule - if let Ok(value) = self.engine.eval_rule("data.wsc.policy.violations".to_string()) { + if let Ok(value) = self + .engine + .eval_rule("data.wsc.policy.violations".to_string()) + { result.violations = regorus_to_string_set(&value); } else if let Ok(value) = self.engine.eval_rule("data.policy.violations".to_string()) { result.violations = regorus_to_string_set(&value); } // Evaluate warnings rule - if let Ok(value) = self.engine.eval_rule("data.wsc.policy.warnings".to_string()) { + if let Ok(value) = self + .engine + .eval_rule("data.wsc.policy.warnings".to_string()) + { result.warnings = regorus_to_string_set(&value); } else if let Ok(value) = self.engine.eval_rule("data.policy.warnings".to_string()) { result.warnings = regorus_to_string_set(&value); @@ -312,8 +318,7 @@ impl RegoInput { /// * `attestation` - The attestation to evaluate /// * `slsa_level` - Detected SLSA level (0-4) pub fn from_attestation(attestation: &TransformationAttestation, slsa_level: u8) -> Self { - let attestation_json = serde_json::to_value(attestation) - .unwrap_or(serde_json::Value::Null); + let attestation_json = serde_json::to_value(attestation).unwrap_or(serde_json::Value::Null); Self { attestation: attestation_json, @@ -380,8 +385,9 @@ fn regorus_to_json(value: ®orus::Value) -> Result { // regorus Number can be i64 or f64 if let Some(f) = n.as_f64() { - let json_num = serde_json::Number::from_f64(f) - .ok_or_else(|| RegoError::SerdeError("Invalid number conversion".to_string()))?; + let json_num = serde_json::Number::from_f64(f).ok_or_else(|| { + RegoError::SerdeError("Invalid number conversion".to_string()) + })?; Ok(serde_json::Value::Number(json_num)) } else if let Some(i) = n.as_i64() { Ok(serde_json::Value::Number(i.into())) @@ -426,22 +432,20 @@ fn regorus_to_bool(value: ®orus::Value) -> bool { /// Extract string set from regorus::Value (for violations/warnings) fn regorus_to_string_set(value: ®orus::Value) -> Vec { match value { - regorus::Value::Set(set) => { - set.iter() - .filter_map(|v| match v { - regorus::Value::String(s) => Some(s.to_string()), - _ => Some(v.to_string()), - }) - .collect() - } - regorus::Value::Array(arr) => { - arr.iter() - .filter_map(|v| match v { - regorus::Value::String(s) => Some(s.to_string()), - _ => Some(v.to_string()), - }) - .collect() - } + regorus::Value::Set(set) => set + .iter() + .filter_map(|v| match v { + regorus::Value::String(s) => Some(s.to_string()), + _ => Some(v.to_string()), + }) + .collect(), + regorus::Value::Array(arr) => arr + .iter() + .filter_map(|v| match v { + regorus::Value::String(s) => Some(s.to_string()), + _ => Some(v.to_string()), + }) + .collect(), _ => Vec::new(), } } @@ -518,12 +522,14 @@ mod tests { "#; engine.add_policy("test.rego", policy).unwrap(); - engine.set_data(serde_json::json!({ - "trusted_tools": { - "loom": true, - "wac": true - } - })).unwrap(); + engine + .set_data(serde_json::json!({ + "trusted_tools": { + "loom": true, + "wac": true + } + })) + .unwrap(); // Test with trusted tool let input = RegoInput { diff --git a/src/lib/src/policy/slsa.rs b/src/lib/src/policy/slsa.rs index e66e521..58130b7 100644 --- a/src/lib/src/policy/slsa.rs +++ b/src/lib/src/policy/slsa.rs @@ -187,7 +187,9 @@ pub fn detect_slsa_level_detailed(attestation: &TransformationAttestation) -> Sl }; // L1: Provenance exists (we have an attestation) - analysis.reasons.push("Attestation present → L1".to_string()); + analysis + .reasons + .push("Attestation present → L1".to_string()); analysis.level = SlsaLevel::L1; // L2: Signed provenance from hosted build @@ -195,17 +197,24 @@ pub fn detect_slsa_level_detailed(attestation: &TransformationAttestation) -> Sl && !attestation.attestation_signature.signature.is_empty(); let has_builder_identity = attestation.attestation_signature.signer_identity.is_some() - || attestation.attestation_signature.certificate_chain.is_some() + || attestation + .attestation_signature + .certificate_chain + .is_some() || attestation.attestation_signature.public_key.is_some(); analysis.is_signed = is_signed; analysis.has_builder_identity = has_builder_identity; if is_signed && has_builder_identity { - analysis.reasons.push("Signed with builder identity → L2".to_string()); + analysis + .reasons + .push("Signed with builder identity → L2".to_string()); analysis.level = SlsaLevel::L2; } else if is_signed { - analysis.reasons.push("Signed but no builder identity → L1".to_string()); + analysis + .reasons + .push("Signed but no builder identity → L1".to_string()); } // L3: Hardened build (non-forgeable provenance via transparency log) @@ -213,20 +222,25 @@ pub fn detect_slsa_level_detailed(attestation: &TransformationAttestation) -> Sl analysis.has_transparency_log = has_rekor; if has_rekor && is_signed { - analysis.reasons.push("Logged to Rekor transparency log → L3".to_string()); + analysis + .reasons + .push("Logged to Rekor transparency log → L3".to_string()); analysis.level = SlsaLevel::L3; } // L4: Hermetic build (all inputs verified) let all_inputs_verified = !attestation.inputs.is_empty() - && attestation.inputs.iter().all(|input| { - input.signature_status == SignatureStatus::Verified - }); + && attestation + .inputs + .iter() + .all(|input| input.signature_status == SignatureStatus::Verified); analysis.all_inputs_pinned = all_inputs_verified; if all_inputs_verified && has_rekor && is_signed { - analysis.reasons.push("All inputs verified → L4".to_string()); + analysis + .reasons + .push("All inputs verified → L4".to_string()); analysis.level = SlsaLevel::L4; } else if !attestation.inputs.is_empty() && !all_inputs_verified { let unverified_count = attestation diff --git a/src/lib/src/provisioning/csr.rs b/src/lib/src/provisioning/csr.rs index 04d07b4..8602588 100644 --- a/src/lib/src/provisioning/csr.rs +++ b/src/lib/src/provisioning/csr.rs @@ -142,10 +142,7 @@ impl CertificateSigningRequest { rdns.push(encode_attribute_type_and_value(OID_CN, &cn)); // O (Organization) - rdns.push(encode_attribute_type_and_value( - OID_O, - &config.organization, - )); + rdns.push(encode_attribute_type_and_value(OID_O, &config.organization)); // OU (Organizational Unit) - optional if let Some(ou) = &config.organizational_unit { diff --git a/src/lib/src/provisioning/wasm_signing.rs b/src/lib/src/provisioning/wasm_signing.rs index d622edd..ac645d7 100644 --- a/src/lib/src/provisioning/wasm_signing.rs +++ b/src/lib/src/provisioning/wasm_signing.rs @@ -608,8 +608,8 @@ fn extract_public_key_from_certificate(cert_der: &[u8]) -> Result wsc::crypto::hardware_signing: match level { SecurityLevel::Software => wsc::crypto::hardware_signing::SecurityLevel::Software, SecurityLevel::HardwareBasic => wsc::crypto::hardware_signing::SecurityLevel::HardwareBasic, - SecurityLevel::HardwareBacked => wsc::crypto::hardware_signing::SecurityLevel::HardwareBacked, - SecurityLevel::HardwareCertified => wsc::crypto::hardware_signing::SecurityLevel::HardwareCertified, + SecurityLevel::HardwareBacked => { + wsc::crypto::hardware_signing::SecurityLevel::HardwareBacked + } + SecurityLevel::HardwareCertified => { + wsc::crypto::hardware_signing::SecurityLevel::HardwareCertified + } } } @@ -55,7 +59,12 @@ impl wsc::crypto::hardware_signing::Host for Cry self.provider.health_check().is_ok() } - fn get_backend_info(&mut self) -> Result { + fn get_backend_info( + &mut self, + ) -> Result< + wsc::crypto::hardware_signing::BackendInfo, + wsc::crypto::hardware_signing::HardwareError, + > { Ok(wsc::crypto::hardware_signing::BackendInfo { name: self.provider.name().to_string(), level: to_wit_security_level(self.provider.security_level()), @@ -77,15 +86,20 @@ impl wsc::crypto::hardware_signing::Host for Cry ) -> Result { // Currently we only support Ed25519 if algorithm != wsc::crypto::hardware_signing::SigningAlgorithm::Ed25519 { - return Err(wsc::crypto::hardware_signing::HardwareError::UnsupportedAlgorithm( - format!("Only Ed25519 is currently supported, got {:?}", algorithm) - )); + return Err( + wsc::crypto::hardware_signing::HardwareError::UnsupportedAlgorithm(format!( + "Only Ed25519 is currently supported, got {:?}", + algorithm + )), + ); } self.provider .generate_key() .map(|h| h.as_raw()) - .map_err(|e| wsc::crypto::hardware_signing::HardwareError::GenerationFailed(e.to_string())) + .map_err(|e| { + wsc::crypto::hardware_signing::HardwareError::GenerationFailed(e.to_string()) + }) } fn sign( @@ -103,12 +117,15 @@ impl wsc::crypto::hardware_signing::Host for Cry fn get_public_key( &mut self, handle: u64, - ) -> Result { + ) -> Result< + wsc::crypto::hardware_signing::PublicKeyInfo, + wsc::crypto::hardware_signing::HardwareError, + > { let key_handle = KeyHandle::from_raw(handle); - let public_key = self.provider - .get_public_key(key_handle) - .map_err(|e| wsc::crypto::hardware_signing::HardwareError::KeyNotFound(e.to_string()))?; + let public_key = self.provider.get_public_key(key_handle).map_err(|e| { + wsc::crypto::hardware_signing::HardwareError::KeyNotFound(e.to_string()) + })?; // Get raw public key bytes (32 bytes for Ed25519) let public_key_der = public_key.pk.as_ref().to_vec(); @@ -118,7 +135,9 @@ impl wsc::crypto::hardware_signing::Host for Cry algorithm: wsc::crypto::hardware_signing::SigningAlgorithm::Ed25519, public_key_der, // Convert key_id from Option> to Option - key_id: public_key.key_id.and_then(|bytes| String::from_utf8(bytes).ok()), + key_id: public_key + .key_id + .and_then(|bytes| String::from_utf8(bytes).ok()), }) } @@ -130,28 +149,38 @@ impl wsc::crypto::hardware_signing::Host for Cry signature: Vec, ) -> Result { if algorithm != wsc::crypto::hardware_signing::SigningAlgorithm::Ed25519 { - return Err(wsc::crypto::hardware_signing::HardwareError::UnsupportedAlgorithm( - format!("Only Ed25519 is currently supported, got {:?}", algorithm) - )); + return Err( + wsc::crypto::hardware_signing::HardwareError::UnsupportedAlgorithm(format!( + "Only Ed25519 is currently supported, got {:?}", + algorithm + )), + ); } // Parse public key - let pk = ed25519_compact::PublicKey::from_slice(&public_key_der) - .map_err(|e| wsc::crypto::hardware_signing::HardwareError::InvalidHandle( - format!("Invalid public key: {}", e) - ))?; + let pk = ed25519_compact::PublicKey::from_slice(&public_key_der).map_err(|e| { + wsc::crypto::hardware_signing::HardwareError::InvalidHandle(format!( + "Invalid public key: {}", + e + )) + })?; // Parse signature - let sig = ed25519_compact::Signature::from_slice(&signature) - .map_err(|e| wsc::crypto::hardware_signing::HardwareError::InvalidHandle( - format!("Invalid signature: {}", e) - ))?; + let sig = ed25519_compact::Signature::from_slice(&signature).map_err(|e| { + wsc::crypto::hardware_signing::HardwareError::InvalidHandle(format!( + "Invalid signature: {}", + e + )) + })?; // Verify Ok(pk.verify(&data, &sig).is_ok()) } - fn delete_key(&mut self, handle: u64) -> Result<(), wsc::crypto::hardware_signing::HardwareError> { + fn delete_key( + &mut self, + handle: u64, + ) -> Result<(), wsc::crypto::hardware_signing::HardwareError> { let key_handle = KeyHandle::from_raw(handle); self.provider @@ -183,16 +212,17 @@ impl WscRuntime

{ let mut config = Config::new(); config.wasm_component_model(true); - let engine = Engine::new(&config) - .map_err(|e| WSError::InternalError(format!("Failed to create wasmtime engine: {}", e)))?; + let engine = Engine::new(&config).map_err(|e| { + WSError::InternalError(format!("Failed to create wasmtime engine: {}", e)) + })?; let mut linker = Linker::new(&engine); // Add wsc:crypto imports to the linker - CryptoGuest::add_to_linker::, wasmtime::component::HasSelf>>( - &mut linker, - |state| state, - ) + CryptoGuest::add_to_linker::< + CryptoHostState

, + wasmtime::component::HasSelf>, + >(&mut linker, |state| state) .map_err(|e| WSError::InternalError(format!("Failed to add crypto bindings: {}", e)))?; Ok(Self { engine, linker }) @@ -249,11 +279,16 @@ mod tests { let mut state = CryptoHostState::new(provider); // Test is_available - assert!(wsc::crypto::hardware_signing::Host::is_available(&mut state)); + assert!(wsc::crypto::hardware_signing::Host::is_available( + &mut state + )); // Test get_security_level let level = wsc::crypto::hardware_signing::Host::get_security_level(&mut state); - assert_eq!(level, wsc::crypto::hardware_signing::SecurityLevel::Software); + assert_eq!( + level, + wsc::crypto::hardware_signing::SecurityLevel::Software + ); // Test generate_key let handle = wsc::crypto::hardware_signing::Host::generate_key( @@ -261,16 +296,19 @@ mod tests { wsc::crypto::hardware_signing::SigningAlgorithm::Ed25519, 0x01, // sign usage Some("test-key".to_string()), - ).unwrap(); + ) + .unwrap(); assert!(handle > 0); // Test sign let data = b"test data to sign".to_vec(); - let signature = wsc::crypto::hardware_signing::Host::sign(&mut state, handle, data.clone()).unwrap(); + let signature = + wsc::crypto::hardware_signing::Host::sign(&mut state, handle, data.clone()).unwrap(); assert_eq!(signature.len(), 64); // Ed25519 signature is 64 bytes // Test get_public_key - let pk_info = wsc::crypto::hardware_signing::Host::get_public_key(&mut state, handle).unwrap(); + let pk_info = + wsc::crypto::hardware_signing::Host::get_public_key(&mut state, handle).unwrap(); assert_eq!(pk_info.handle, handle); assert_eq!(pk_info.public_key_der.len(), 32); // Ed25519 public key is 32 bytes @@ -281,7 +319,8 @@ mod tests { wsc::crypto::hardware_signing::SigningAlgorithm::Ed25519, data, signature, - ).unwrap(); + ) + .unwrap(); assert!(verified); // Test list_keys diff --git a/src/lib/src/sct.rs b/src/lib/src/sct.rs index 0b61d83..15b4765 100644 --- a/src/lib/src/sct.rs +++ b/src/lib/src/sct.rs @@ -134,48 +134,42 @@ pub fn default_trusted_logs() -> Vec { vec![ TrustedCtLog { log_id: [ - 0xa4, 0xb9, 0x09, 0x90, 0xb4, 0x18, 0x58, 0x14, - 0x87, 0xbb, 0x13, 0xa2, 0xcc, 0x67, 0x70, 0x0a, - 0x3c, 0x35, 0x98, 0x04, 0xf9, 0x1b, 0xdf, 0xb8, - 0xe3, 0x77, 0xcd, 0x0e, 0xc8, 0x0d, 0xdc, 0x10, + 0xa4, 0xb9, 0x09, 0x90, 0xb4, 0x18, 0x58, 0x14, 0x87, 0xbb, 0x13, 0xa2, 0xcc, 0x67, + 0x70, 0x0a, 0x3c, 0x35, 0x98, 0x04, 0xf9, 0x1b, 0xdf, 0xb8, 0xe3, 0x77, 0xcd, 0x0e, + 0xc8, 0x0d, 0xdc, 0x10, ], public_key: vec![ - 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, - 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, - 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, - 0x42, 0x00, 0x04, 0xde, 0xad, 0xbe, 0xef, + 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, + 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0xde, + 0xad, 0xbe, 0xef, ], description: "Google Argon 2025".to_string(), url: "https://ct.googleapis.com/logs/argon2025/".to_string(), }, TrustedCtLog { log_id: [ - 0x63, 0xf2, 0xdb, 0xcd, 0xe8, 0x3b, 0xcc, 0x2c, - 0xcf, 0x0b, 0x72, 0x84, 0x27, 0x57, 0x6b, 0x33, - 0xa4, 0x8d, 0x61, 0x77, 0x8f, 0xbd, 0x75, 0xa6, - 0x38, 0xb1, 0xc7, 0x68, 0x54, 0x4b, 0xd8, 0x8d, + 0x63, 0xf2, 0xdb, 0xcd, 0xe8, 0x3b, 0xcc, 0x2c, 0xcf, 0x0b, 0x72, 0x84, 0x27, 0x57, + 0x6b, 0x33, 0xa4, 0x8d, 0x61, 0x77, 0x8f, 0xbd, 0x75, 0xa6, 0x38, 0xb1, 0xc7, 0x68, + 0x54, 0x4b, 0xd8, 0x8d, ], public_key: vec![ - 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, - 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, - 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, - 0x42, 0x00, 0x04, 0xca, 0xfe, 0xba, 0xbe, + 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, + 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0xca, + 0xfe, 0xba, 0xbe, ], description: "Cloudflare Nimbus 2025".to_string(), url: "https://ct.cloudflare.com/logs/nimbus2025/".to_string(), }, TrustedCtLog { log_id: [ - 0x56, 0x14, 0x06, 0x9a, 0x2f, 0xd7, 0xc2, 0xec, - 0xd3, 0xf5, 0xe1, 0xbd, 0x44, 0xb2, 0x3e, 0xc7, - 0x46, 0x76, 0xb9, 0xbc, 0x99, 0x11, 0x5c, 0xc0, - 0xef, 0x94, 0x98, 0x55, 0xd6, 0x89, 0xd0, 0xdd, + 0x56, 0x14, 0x06, 0x9a, 0x2f, 0xd7, 0xc2, 0xec, 0xd3, 0xf5, 0xe1, 0xbd, 0x44, 0xb2, + 0x3e, 0xc7, 0x46, 0x76, 0xb9, 0xbc, 0x99, 0x11, 0x5c, 0xc0, 0xef, 0x94, 0x98, 0x55, + 0xd6, 0x89, 0xd0, 0xdd, ], public_key: vec![ - 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, - 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, - 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, - 0x42, 0x00, 0x04, 0xfe, 0xed, 0xfa, 0xce, + 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, + 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0xfe, + 0xed, 0xfa, 0xce, ], description: "DigiCert Yeti 2025".to_string(), url: "https://yeti2025.ct.digicert.com/log/".to_string(), @@ -212,16 +206,9 @@ impl SctVerifier { /// # Errors /// /// Returns an error if the SCT references an unknown log. - pub fn verify_sct( - &self, - sct: &SctEntry, - cert_der: &[u8], - ) -> Result { + pub fn verify_sct(&self, sct: &SctEntry, cert_der: &[u8]) -> Result { let log = self.find_log(&sct.log_id).ok_or_else(|| { - WSError::CertificateError(format!( - "Unknown CT log ID: {}", - hex::encode(sct.log_id) - )) + WSError::CertificateError(format!("Unknown CT log ID: {}", hex::encode(sct.log_id))) })?; // Build the data that should have been signed by the CT log: @@ -265,9 +252,8 @@ impl SctVerifier { verification is not yet implemented. Do not rely on SCT results \ for security decisions." ); - let valid = !sct.signature.is_empty() - && !log.public_key.is_empty() - && sct.signature.len() >= 8; + let valid = + !sct.signature.is_empty() && !log.public_key.is_empty() && sct.signature.len() >= 8; Ok(SctVerification { log_id: sct.log_id, @@ -286,10 +272,7 @@ impl SctVerifier { /// # Errors /// /// Returns an error if the certificate cannot be parsed. - pub fn verify_embedded_scts( - &self, - cert_der: &[u8], - ) -> Result, WSError> { + pub fn verify_embedded_scts(&self, cert_der: &[u8]) -> Result, WSError> { // Parse the certificate to look for the SCT list extension. let (_, cert) = x509_parser::parse_x509_certificate(cert_der) .map_err(|e| WSError::X509Error(format!("Failed to parse certificate: {:?}", e)))?; @@ -336,9 +319,7 @@ impl SctVerifier { /// - SCT data (version, log_id, timestamp, extensions, signature) pub fn parse_sct_list(data: &[u8]) -> Result, WSError> { if data.len() < 2 { - return Err(WSError::CertificateError( - "SCT list too short".to_string(), - )); + return Err(WSError::CertificateError("SCT list too short".to_string())); } let list_len = u16::from_be_bytes([data[0], data[1]]) as usize; @@ -386,9 +367,7 @@ pub fn parse_sct_list(data: &[u8]) -> Result, WSError> { fn parse_single_sct(data: &[u8]) -> Result { // Minimum: 1 + 32 + 8 + 2 + 1 + 1 + 2 = 47 bytes (no extensions, no sig body) if data.len() < 47 { - return Err(WSError::CertificateError( - "SCT entry too short".to_string(), - )); + return Err(WSError::CertificateError("SCT entry too short".to_string())); } let version = data[0]; @@ -403,8 +382,7 @@ fn parse_single_sct(data: &[u8]) -> Result { log_id.copy_from_slice(&data[1..33]); let timestamp = u64::from_be_bytes([ - data[33], data[34], data[35], data[36], - data[37], data[38], data[39], data[40], + data[33], data[34], data[35], data[36], data[37], data[38], data[39], data[40], ]); let ext_len = u16::from_be_bytes([data[41], data[42]]) as usize; @@ -432,7 +410,7 @@ fn parse_single_sct(data: &[u8]) -> Result { }; let signature_algorithm = match sig_alg_byte { - 3 => SignatureAlgorithm::Ecdsa, // RFC 5246 SignatureAlgorithm ecdsa = 3 + 3 => SignatureAlgorithm::Ecdsa, // RFC 5246 SignatureAlgorithm ecdsa = 3 7 => SignatureAlgorithm::Ed25519, // Ed25519 (draft-josefsson-eddsa-ed25519) _ => { return Err(WSError::CertificateError(format!( @@ -551,10 +529,7 @@ impl SctMonitor { /// Extracts the Subject Alternative Names from the certificate, checks /// whether any match the monitored domains, verifies embedded SCTs, /// and flags unexpected certificates. - pub fn check_certificate( - &self, - cert_der: &[u8], - ) -> Result { + pub fn check_certificate(&self, cert_der: &[u8]) -> Result { // Parse the certificate to extract SANs. let (_, cert) = x509_parser::parse_x509_certificate(cert_der) .map_err(|e| WSError::X509Error(format!("Failed to parse certificate: {:?}", e)))?; @@ -574,16 +549,17 @@ impl SctMonitor { } // Determine primary domain for reporting. - let domain = domains.first().cloned().unwrap_or_else(|| { - cert.subject().to_string() - }); + let domain = domains + .first() + .cloned() + .unwrap_or_else(|| cert.subject().to_string()); // Check whether this certificate is for one of our monitored domains // but was *not* expected — indicating potential mis-issuance. let matches_monitored = domains.iter().any(|d| { - self.expected_domains.iter().any(|exp| { - d == exp || d.ends_with(&format!(".{}", exp)) - }) + self.expected_domains + .iter() + .any(|exp| d == exp || d.ends_with(&format!(".{}", exp))) }); // If it matches a monitored domain, flag as unexpected (the caller @@ -801,9 +777,21 @@ mod tests { assert_eq!(parsed.signature, sct.signature); // Verify camelCase serialisation is used. - assert!(json.contains("logId"), "field should be camelCase: {}", json); - assert!(json.contains("hashAlgorithm"), "field should be camelCase: {}", json); - assert!(json.contains("signatureAlgorithm"), "field should be camelCase: {}", json); + assert!( + json.contains("logId"), + "field should be camelCase: {}", + json + ); + assert!( + json.contains("hashAlgorithm"), + "field should be camelCase: {}", + json + ); + assert!( + json.contains("signatureAlgorithm"), + "field should be camelCase: {}", + json + ); } // ── 12. Parse truncated SCT list ────────────────────────────────── @@ -862,10 +850,7 @@ mod tests { description: "Custom Log".to_string(), url: "https://custom.example.com/ct/".to_string(), }; - let monitor = SctMonitor::with_logs( - vec!["mysite.com".to_string()], - vec![log.clone()], - ); + let monitor = SctMonitor::with_logs(vec!["mysite.com".to_string()], vec![log.clone()]); assert_eq!(monitor.expected_domains, vec!["mysite.com"]); let found = monitor.verifier.find_log(&[0xcc; 32]); diff --git a/src/lib/src/secure_file.rs b/src/lib/src/secure_file.rs index 158ad2a..f64bfbc 100644 --- a/src/lib/src/secure_file.rs +++ b/src/lib/src/secure_file.rs @@ -202,9 +202,8 @@ pub fn read_secure(path: &Path) -> Result, WSError> { /// See [`read_secure`] for details on the security guarantees. pub fn read_secure_string(path: &Path) -> Result { let contents = read_secure(path)?; - String::from_utf8(contents).map_err(|e| { - WSError::InternalError(format!("Invalid UTF-8 in secure file: {}", e)) - }) + String::from_utf8(contents) + .map_err(|e| WSError::InternalError(format!("Invalid UTF-8 in secure file: {}", e))) } #[cfg(test)] diff --git a/src/lib/src/signature/info.rs b/src/lib/src/signature/info.rs index 46b9994..e865aa4 100644 --- a/src/lib/src/signature/info.rs +++ b/src/lib/src/signature/info.rs @@ -6,7 +6,7 @@ //! Backported from wasmsign2 (commit 8223bec, 2025-12-18). use crate::error::*; -use crate::signature::sig_sections::{SignatureData, SIGNATURE_SECTION_HEADER_NAME}; +use crate::signature::sig_sections::{SIGNATURE_SECTION_HEADER_NAME, SignatureData}; use crate::wasm_module::{Module, Section}; use std::fs::File; use std::io::{BufReader, Read}; diff --git a/src/lib/src/signature/keyless/cert_pinning.rs b/src/lib/src/signature/keyless/cert_pinning.rs index 7477dd0..84b69b9 100644 --- a/src/lib/src/signature/keyless/cert_pinning.rs +++ b/src/lib/src/signature/keyless/cert_pinning.rs @@ -295,10 +295,12 @@ impl PinningConfig { } // Parse the X.509 certificate to extract SPKI - let (_, cert) = x509_parser::parse_x509_certificate(cert_der.as_ref()) - .map_err(|e| WSError::CertificatePinningError(format!( - "Failed to parse certificate for SPKI extraction: {:?}", e - )))?; + let (_, cert) = x509_parser::parse_x509_certificate(cert_der.as_ref()).map_err(|e| { + WSError::CertificatePinningError(format!( + "Failed to parse certificate for SPKI extraction: {:?}", + e + )) + })?; // Hash the raw SubjectPublicKeyInfo DER bytes let spki_der = cert.public_key().raw; diff --git a/src/lib/src/signature/keyless/checkpoint.rs b/src/lib/src/signature/keyless/checkpoint.rs index aefba15..5aa559c 100644 --- a/src/lib/src/signature/keyless/checkpoint.rs +++ b/src/lib/src/signature/keyless/checkpoint.rs @@ -212,11 +212,7 @@ impl ConsistencyVerifier { /// /// Returns `Err` if `new.tree_size < old.tree_size`, indicating a /// potential log truncation or rollback attack. - pub fn check_monotonic( - &self, - old: &Checkpoint, - new: &Checkpoint, - ) -> Result<(), WSError> { + pub fn check_monotonic(&self, old: &Checkpoint, new: &Checkpoint) -> Result<(), WSError> { if new.tree_size < old.tree_size { return Err(WSError::RekorError(format!( "Log rollback detected: tree size decreased from {} to {}", @@ -264,12 +260,9 @@ impl CheckpointStore for FileCheckpointStore { fn load(&self) -> Result, WSError> { match std::fs::read_to_string(&self.path) { Ok(contents) => { - let checkpoint: SignedCheckpoint = serde_json::from_str(&contents) - .map_err(|e| { - WSError::RekorError(format!( - "Failed to parse stored checkpoint: {}", - e - )) + let checkpoint: SignedCheckpoint = + serde_json::from_str(&contents).map_err(|e| { + WSError::RekorError(format!("Failed to parse stored checkpoint: {}", e)) })?; Ok(Some(checkpoint)) } @@ -283,9 +276,8 @@ impl CheckpointStore for FileCheckpointStore { } fn save(&self, checkpoint: &SignedCheckpoint) -> Result<(), WSError> { - let json = serde_json::to_string_pretty(checkpoint).map_err(|e| { - WSError::RekorError(format!("Failed to serialize checkpoint: {}", e)) - })?; + let json = serde_json::to_string_pretty(checkpoint) + .map_err(|e| WSError::RekorError(format!("Failed to serialize checkpoint: {}", e)))?; // Write to a temporary file next to the target, then rename for // atomic replacement. @@ -298,12 +290,8 @@ impl CheckpointStore for FileCheckpointStore { )) })?; - std::fs::rename(&tmp_path, &self.path).map_err(|e| { - WSError::RekorError(format!( - "Failed to rename checkpoint file: {}", - e - )) - })?; + std::fs::rename(&tmp_path, &self.path) + .map_err(|e| WSError::RekorError(format!("Failed to rename checkpoint file: {}", e)))?; Ok(()) } @@ -382,10 +370,7 @@ pub fn parse_checkpoint(text: &str) -> Result { root_hash.copy_from_slice(&root_hash_bytes); // Collect any additional extension lines (between root hash and blank line) - let other_content: Vec = body_lines[3..] - .iter() - .map(|s| s.to_string()) - .collect(); + let other_content: Vec = body_lines[3..].iter().map(|s| s.to_string()).collect(); // Parse signatures let mut signatures = Vec::new(); @@ -433,12 +418,7 @@ pub fn parse_checkpoint(text: &str) -> Result { )); } - let key_hash = u32::from_be_bytes([ - sig_bytes[0], - sig_bytes[1], - sig_bytes[2], - sig_bytes[3], - ]); + let key_hash = u32::from_be_bytes([sig_bytes[0], sig_bytes[1], sig_bytes[2], sig_bytes[3]]); let signature = sig_bytes[4..].to_vec(); signatures.push(CheckpointSignature { @@ -651,10 +631,7 @@ mod tests { let root_b64 = BASE64.encode([0u8; 32]); // Only 3 bytes — needs at least 5 (4 key_hash + 1 sig) let sig_b64 = BASE64.encode([0u8; 3]); - let note = format!( - "origin\n1\n{}\n\n\u{2014} signer {}", - root_b64, sig_b64 - ); + let note = format!("origin\n1\n{}\n\n\u{2014} signer {}", root_b64, sig_b64); let err = parse_checkpoint(¬e).unwrap_err(); assert!( err.to_string().contains("too short"), @@ -934,14 +911,25 @@ mod tests { let json = serde_json::to_string(&cp).expect("serialize"); // Verify camelCase field names - assert!(json.contains("treeSize"), "expected camelCase treeSize in JSON"); - assert!(json.contains("rootHash"), "expected camelCase rootHash in JSON"); - assert!(json.contains("otherContent"), "expected camelCase otherContent in JSON"); - assert!(json.contains("keyHash"), "expected camelCase keyHash in JSON"); + assert!( + json.contains("treeSize"), + "expected camelCase treeSize in JSON" + ); + assert!( + json.contains("rootHash"), + "expected camelCase rootHash in JSON" + ); + assert!( + json.contains("otherContent"), + "expected camelCase otherContent in JSON" + ); + assert!( + json.contains("keyHash"), + "expected camelCase keyHash in JSON" + ); // Round-trip - let parsed: SignedCheckpoint = - serde_json::from_str(&json).expect("deserialize"); + let parsed: SignedCheckpoint = serde_json::from_str(&json).expect("deserialize"); assert_eq!(parsed, cp); } diff --git a/src/lib/src/signature/keyless/format.rs b/src/lib/src/signature/keyless/format.rs index faf2d5f..8317ad1 100644 --- a/src/lib/src/signature/keyless/format.rs +++ b/src/lib/src/signature/keyless/format.rs @@ -235,31 +235,33 @@ impl KeylessSignature { // Look for Subject Alternative Name extension if let Some(san_ext) = cert.get_extension_unique(&oid_registry::OID_X509_EXT_SUBJECT_ALT_NAME)? - && let ParsedExtension::SubjectAlternativeName(san) = san_ext.parsed_extension() { - // Try different SAN types in order of preference - for name in &san.general_names { - match name { - GeneralName::RFC822Name(email) => { - return Ok(email.to_string()); - } - GeneralName::URI(uri) => { - return Ok(uri.to_string()); - } - GeneralName::DNSName(dns) => { - return Ok(dns.to_string()); - } - _ => continue, + && let ParsedExtension::SubjectAlternativeName(san) = san_ext.parsed_extension() + { + // Try different SAN types in order of preference + for name in &san.general_names { + match name { + GeneralName::RFC822Name(email) => { + return Ok(email.to_string()); } + GeneralName::URI(uri) => { + return Ok(uri.to_string()); + } + GeneralName::DNSName(dns) => { + return Ok(dns.to_string()); + } + _ => continue, } } + } // Fall back to subject common name if no SAN found for rdn in cert.subject().iter() { for attr in rdn.iter() { if attr.attr_type() == &oid_registry::OID_X509_COMMON_NAME - && let Ok(cn) = attr.as_str() { - return Ok(cn.to_string()); - } + && let Ok(cn) = attr.as_str() + { + return Ok(cn.to_string()); + } } } diff --git a/src/lib/src/signature/keyless/fulcio.rs b/src/lib/src/signature/keyless/fulcio.rs index 48eacdb..53462d0 100644 --- a/src/lib/src/signature/keyless/fulcio.rs +++ b/src/lib/src/signature/keyless/fulcio.rs @@ -108,8 +108,8 @@ impl FulcioClient { pub fn with_url(base_url: String) -> Self { #[cfg(not(target_os = "wasi"))] { - use super::transport::create_agent_with_optional_pinning; use super::cert_pinning::PinningConfig; + use super::transport::create_agent_with_optional_pinning; // Create pinning configuration for Fulcio let pinning = Some(PinningConfig::fulcio()); @@ -119,7 +119,10 @@ impl FulcioClient { Ok(agent) => agent, Err(e) => { // Log error but don't panic - fall back to standard agent - log::error!("Failed to create pinned agent for Fulcio: {}. Using standard TLS.", e); + log::error!( + "Failed to create pinned agent for Fulcio: {}. Using standard TLS.", + e + ); super::transport::create_standard_agent() } }; diff --git a/src/lib/src/signature/keyless/merkle.rs b/src/lib/src/signature/keyless/merkle.rs index 908393a..119930f 100644 --- a/src/lib/src/signature/keyless/merkle.rs +++ b/src/lib/src/signature/keyless/merkle.rs @@ -479,11 +479,20 @@ mod proofs { let result = largest_power_of_two_less_than(n); // Must be a power of 2 - assert!(result.is_power_of_two(), "Result {} is not a power of 2", result); + assert!( + result.is_power_of_two(), + "Result {} is not a power of 2", + result + ); // Must be less than n assert!(result < n, "Result {} is not less than n={}", result, n); // Must be the largest such power (next power would be >= n) - assert!(result * 2 >= n, "Result {} is not the largest power < n={}", result, n); + assert!( + result * 2 >= n, + "Result {} is not the largest power < n={}", + result, + n + ); } /// Prove: largest_power_of_two_less_than handles edge cases. @@ -519,8 +528,10 @@ mod proofs { let node_hash = compute_node_hash(&left, &right); // Leaf hash and node hash must differ (domain separation) - assert_ne!(leaf_hash, node_hash, - "Leaf hash and node hash collided — domain separation broken"); + assert_ne!( + leaf_hash, node_hash, + "Leaf hash and node hash collided — domain separation broken" + ); } /// Prove: compute_leaf_hash is deterministic. diff --git a/src/lib/src/signature/keyless/mod.rs b/src/lib/src/signature/keyless/mod.rs index bb22559..fb7c41d 100644 --- a/src/lib/src/signature/keyless/mod.rs +++ b/src/lib/src/signature/keyless/mod.rs @@ -7,8 +7,6 @@ pub mod cert_pinning; pub mod cert_verifier; /// Rekor checkpoint (Signed Tree Head) consistency verification (Phase 4.3) pub mod checkpoint; -/// Rate limiting for Sigstore API endpoints (Issue #6) -pub mod rate_limit; /// Keyless signing support for wsc /// /// This module implements keyless (ephemeral key) signing using: @@ -20,6 +18,8 @@ pub mod fulcio; pub mod merkle; pub mod oidc; pub mod proof_cache; +/// Rate limiting for Sigstore API endpoints (Issue #6) +pub mod rate_limit; pub mod rekor; pub mod rekor_verifier; pub mod signer; @@ -43,4 +43,4 @@ pub use oidc::{ }; pub use rekor::{RekorClient, RekorEntry}; pub use rekor_verifier::RekorKeyring; -pub use signer::{KeylessConfig, KeylessSigner, KeylessVerifier, KeylessVerificationResult}; +pub use signer::{KeylessConfig, KeylessSigner, KeylessVerificationResult, KeylessVerifier}; diff --git a/src/lib/src/signature/keyless/rate_limit.rs b/src/lib/src/signature/keyless/rate_limit.rs index 7e9bb64..32a3e25 100644 --- a/src/lib/src/signature/keyless/rate_limit.rs +++ b/src/lib/src/signature/keyless/rate_limit.rs @@ -114,7 +114,8 @@ impl RetryPolicy { return Duration::ZERO; } - let delay_secs = self.initial_delay.as_secs_f64() * self.multiplier.powi(attempt as i32 - 1); + let delay_secs = + self.initial_delay.as_secs_f64() * self.multiplier.powi(attempt as i32 - 1); let delay = Duration::from_secs_f64(delay_secs); std::cmp::min(delay, self.max_delay) @@ -216,7 +217,10 @@ mod tests { let result = limiter.check(); assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), WSError::RateLimitExceeded { .. })); + assert!(matches!( + result.unwrap_err(), + WSError::RateLimitExceeded { .. } + )); } #[test] diff --git a/src/lib/src/signature/keyless/rekor.rs b/src/lib/src/signature/keyless/rekor.rs index 0c8d494..0634906 100644 --- a/src/lib/src/signature/keyless/rekor.rs +++ b/src/lib/src/signature/keyless/rekor.rs @@ -176,8 +176,8 @@ impl RekorClient { pub fn with_url(base_url: String) -> Self { #[cfg(not(target_os = "wasi"))] { - use super::transport::create_agent_with_optional_pinning; use super::cert_pinning::PinningConfig; + use super::transport::create_agent_with_optional_pinning; // Create pinning configuration for Rekor let pinning = Some(PinningConfig::rekor()); @@ -187,7 +187,10 @@ impl RekorClient { Ok(agent) => agent, Err(e) => { // Log error but don't panic - fall back to standard agent - log::error!("Failed to create pinned agent for Rekor: {}. Using standard TLS.", e); + log::error!( + "Failed to create pinned agent for Rekor: {}. Using standard TLS.", + e + ); super::transport::create_standard_agent() } }; @@ -322,12 +325,10 @@ impl RekorClient { .ok_or_else(|| WSError::RekorError("Empty response from Rekor".to_string()))?; // Extract verification data - let verification = entry_data - .verification - .unwrap_or(RekorVerification { - inclusion_proof: None, - signed_entry_timestamp: None, - }); + let verification = entry_data.verification.unwrap_or(RekorVerification { + inclusion_proof: None, + signed_entry_timestamp: None, + }); // Extract inclusion proof if available let inclusion_proof = verification diff --git a/src/lib/src/signature/keyless/signer.rs b/src/lib/src/signature/keyless/signer.rs index a1b5de7..1253d33 100644 --- a/src/lib/src/signature/keyless/signer.rs +++ b/src/lib/src/signature/keyless/signer.rs @@ -11,7 +11,7 @@ use super::{ FulcioClient, KeylessSignature, OidcProvider, RekorClient, RekorEntry, RekorKeyring, detect_oidc_provider, rekor, }; -use crate::{Module, WSError, SectionLike, audit}; +use crate::{Module, SectionLike, WSError, audit}; use ecdsa::SigningKey; use p256::ecdsa::Signature; use sha2::{Digest, Sha256}; @@ -72,7 +72,6 @@ pub struct KeylessConfig { pub proof_cache: Option>, } - /// Main keyless signing interface pub struct KeylessSigner { config: KeylessConfig, @@ -253,7 +252,9 @@ impl KeylessSigner { // Step 2b: Validate OIDC issuer if expected issuer is configured (UCA-12 defense) let expected_issuer = self.config.expected_issuer.clone().or_else(|| { - std::env::var("WSC_EXPECTED_OIDC_ISSUER").ok().filter(|s| !s.is_empty()) + std::env::var("WSC_EXPECTED_OIDC_ISSUER") + .ok() + .filter(|s| !s.is_empty()) }); if let Some(ref expected) = expected_issuer { // Normalize trailing slashes for comparison (AS-13 defense) @@ -316,11 +317,7 @@ impl KeylessSigner { let artifact_hash = format!("sha256:{}", hex::encode(&module_hash)); // Log signing attempt (we now have identity and artifact hash) - audit::log_signing_attempt( - &correlation_id, - &artifact_hash, - Some(&oidc_token.identity), - ); + audit::log_signing_attempt(&correlation_id, &artifact_hash, Some(&oidc_token.identity)); // Step 7: Upload to Rekor (if not skipped) let rekor_entry = if self.config.skip_rekor { @@ -390,7 +387,10 @@ impl KeylessSigner { signature: &KeylessSignature, ) -> Result { let signature_bytes = signature.to_bytes()?; - log::debug!("Embedding keyless signature: {} bytes", signature_bytes.len()); + log::debug!( + "Embedding keyless signature: {} bytes", + signature_bytes.len() + ); // Use Module's existing attach_signature mechanism module.attach_signature(&signature_bytes) @@ -530,7 +530,10 @@ impl KeylessVerifier { // Cache hit — proof was already validated when it was cached. // The certificate chain verification above still runs on every // call, so we only skip the Rekor network round-trip. - log::info!("Using cached Rekor proof for {}", keyless_sig.rekor_entry.uuid); + log::info!( + "Using cached Rekor proof for {}", + keyless_sig.rekor_entry.uuid + ); cache_hit = true; } } diff --git a/src/lib/src/signature/keyless/transport.rs b/src/lib/src/signature/keyless/transport.rs index 7981a9e..68445a1 100644 --- a/src/lib/src/signature/keyless/transport.rs +++ b/src/lib/src/signature/keyless/transport.rs @@ -34,7 +34,7 @@ use crate::error::WSError; #[cfg(not(target_os = "wasi"))] -use crate::signature::keyless::cert_pinning::{create_pinned_rustls_config, PinningConfig}; +use crate::signature::keyless::cert_pinning::{PinningConfig, create_pinned_rustls_config}; #[cfg(not(target_os = "wasi"))] use rustls::{ClientConfig, ClientConnection, StreamOwned}; #[cfg(not(target_os = "wasi"))] @@ -223,9 +223,7 @@ pub fn create_pinned_agent(pinning: PinningConfig) -> Result Pinned TLS - let connector = () - .chain(TcpConnector::default()) - .chain(PinnedRustlsConnector::new(pinning)?); + let connector = ().chain(TcpConnector::default()).chain(PinnedRustlsConnector::new(pinning)?); // Build agent with custom connector let config = ureq::config::Config::builder() @@ -274,24 +272,23 @@ pub fn create_standard_agent() -> ureq::Agent { pub fn create_agent_with_optional_pinning( pinning: Option, ) -> Result { - let require_pinning = std::env::var("WSC_REQUIRE_CERT_PINNING") - .unwrap_or_default() - == "1"; + let require_pinning = std::env::var("WSC_REQUIRE_CERT_PINNING").unwrap_or_default() == "1"; match pinning { - Some(config) if config.is_enabled() => { - match create_pinned_agent(config) { - Ok(agent) => Ok(agent), - Err(e) => { - if require_pinning { - Err(e) - } else { - log::warn!("Failed to enable certificate pinning: {}. Falling back to standard TLS.", e); - Ok(create_standard_agent()) - } + Some(config) if config.is_enabled() => match create_pinned_agent(config) { + Ok(agent) => Ok(agent), + Err(e) => { + if require_pinning { + Err(e) + } else { + log::warn!( + "Failed to enable certificate pinning: {}. Falling back to standard TLS.", + e + ); + Ok(create_standard_agent()) } } - } + }, _ => { if require_pinning { Err(WSError::CertificatePinningError( diff --git a/src/lib/src/signature/keys.rs b/src/lib/src/signature/keys.rs index 4ca698a..923709d 100644 --- a/src/lib/src/signature/keys.rs +++ b/src/lib/src/signature/keys.rs @@ -3,10 +3,10 @@ use crate::secure_file; use ct_codecs::{Encoder, Hex}; use std::collections::HashSet; +use std::fmt; use std::fs::File; use std::io::{self, prelude::*}; use std::path::Path; -use std::fmt; use zeroize::Zeroizing; pub(crate) const ED25519_PK_ID: u8 = 0x01; @@ -112,8 +112,10 @@ impl fmt::Debug for PublicKey { f, "PublicKey {{ [{}] - key_id: {:?} }}", Hex::encode_to_string(self.pk.as_ref()).unwrap_or_else(|_| "".to_string()), - self.key_id() - .map(|key_id| format!("[{}]", Hex::encode_to_string(key_id).unwrap_or_else(|_| "".to_string()))) + self.key_id().map(|key_id| format!( + "[{}]", + Hex::encode_to_string(key_id).unwrap_or_else(|_| "".to_string()) + )) ) } } diff --git a/src/lib/src/signature/mod.rs b/src/lib/src/signature/mod.rs index bf89dff..b357eae 100644 --- a/src/lib/src/signature/mod.rs +++ b/src/lib/src/signature/mod.rs @@ -15,7 +15,6 @@ pub(crate) use hash::*; // Re-export signature data structures for fuzzing and advanced use cases pub use sig_sections::{ - SignatureData, SignedHashes, SignatureForHashes, - SIGNATURE_SECTION_HEADER_NAME, SIGNATURE_SECTION_DELIMITER_NAME, - MAX_HASHES, MAX_SIGNATURES, new_delimiter_section, + MAX_HASHES, MAX_SIGNATURES, SIGNATURE_SECTION_DELIMITER_NAME, SIGNATURE_SECTION_HEADER_NAME, + SignatureData, SignatureForHashes, SignedHashes, new_delimiter_section, }; diff --git a/src/lib/src/signature/multi.rs b/src/lib/src/signature/multi.rs index 786a2c0..684c81b 100644 --- a/src/lib/src/signature/multi.rs +++ b/src/lib/src/signature/multi.rs @@ -2,7 +2,7 @@ use crate::signature::*; use crate::wasm_module::*; use crate::*; -use ct_codecs::{verify as ct_eq, Encoder, Hex}; +use ct_codecs::{Encoder, Hex, verify as ct_eq}; use log::*; use std::collections::HashSet; use std::io::Read; @@ -134,12 +134,16 @@ impl SecretKey { SIGNATURE_VERSION, SIGNATURE_WASM_MODULE_CONTENT_TYPE, SIGNATURE_HASH_FUNCTION, - Hex::encode_to_string(&msg[SIGNATURE_WASM_DOMAIN.len() + 2..]).unwrap_or_else(|_| "".to_string()) + Hex::encode_to_string(&msg[SIGNATURE_WASM_DOMAIN.len() + 2..]) + .unwrap_or_else(|_| "".to_string()) ); let signature = sk.sk.sign(msg.to_vec(), None).to_vec(); - debug!(" = {}\n\n", Hex::encode_to_string(&signature).unwrap_or_else(|_| "".to_string())); + debug!( + " = {}\n\n", + Hex::encode_to_string(&signature).unwrap_or_else(|_| "".to_string()) + ); let signature_for_hashes = SignatureForHashes { key_id: key_id.cloned(), @@ -253,7 +257,10 @@ impl PublicKey { } debug!("Hashes matching the signature:"); for valid_hash in &valid_hashes { - debug!(" - [{}]", Hex::encode_to_string(valid_hash).unwrap_or_else(|_| "".to_string())); + debug!( + " - [{}]", + Hex::encode_to_string(valid_hash).unwrap_or_else(|_| "".to_string()) + ); } let mut hasher = Hash::new(); let mut matching_section_ranges = vec![]; @@ -268,7 +275,10 @@ impl PublicKey { continue; } let h = hasher.finalize().to_vec(); - debug!(" - [{}]", Hex::encode_to_string(&h).unwrap_or_else(|_| "".to_string())); + debug!( + " - [{}]", + Hex::encode_to_string(&h).unwrap_or_else(|_| "".to_string()) + ); if !valid_hashes.contains(&h) { return Err(WSError::VerificationFailedForPredicates); } diff --git a/src/lib/src/signature/sig_sections.rs b/src/lib/src/signature/sig_sections.rs index 3c12463..cb0e62f 100644 --- a/src/lib/src/signature/sig_sections.rs +++ b/src/lib/src/signature/sig_sections.rs @@ -61,9 +61,9 @@ impl SignatureForHashes { varint::put(&mut writer, 0)?; // No certificate chain } - writer - .into_inner() - .map_err(|e| WSError::IOError(std::io::Error::other(format!("buffer flush failed: {}", e)))) + writer.into_inner().map_err(|e| { + WSError::IOError(std::io::Error::other(format!("buffer flush failed: {}", e))) + }) } pub fn deserialize(bin: impl AsRef<[u8]>) -> Result { @@ -149,9 +149,9 @@ impl SignedHashes { for signature in &self.signatures { varint::put_slice(&mut writer, &signature.serialize()?)?; } - writer - .into_inner() - .map_err(|e| WSError::IOError(std::io::Error::other(format!("buffer flush failed: {}", e)))) + writer.into_inner().map_err(|e| { + WSError::IOError(std::io::Error::other(format!("buffer flush failed: {}", e))) + }) } pub fn deserialize(bin: impl AsRef<[u8]>) -> Result { @@ -204,9 +204,9 @@ impl SignatureData { for signed_hashes in &self.signed_hashes_set { varint::put_slice(&mut writer, &signed_hashes.serialize()?)?; } - writer - .into_inner() - .map_err(|e| WSError::IOError(std::io::Error::other(format!("buffer flush failed: {}", e)))) + writer.into_inner().map_err(|e| { + WSError::IOError(std::io::Error::other(format!("buffer flush failed: {}", e))) + }) } pub fn deserialize(bin: impl AsRef<[u8]>) -> Result { @@ -515,10 +515,10 @@ mod tests { #[test] fn test_malformed_cert_count_is_rejected() { let mut buf = Vec::new(); - varint::put(&mut buf, 0u64).unwrap(); // key_id = empty - buf.push(ED25519_PK_ID); // alg_id - varint::put_slice(&mut buf, &[1, 2, 3, 4]).unwrap(); // signature - buf.extend_from_slice(&[0x80, 0x80, 0x80, 0x80, 0x80]); // malformed cert_count + varint::put(&mut buf, 0u64).unwrap(); // key_id = empty + buf.push(ED25519_PK_ID); // alg_id + varint::put_slice(&mut buf, &[1, 2, 3, 4]).unwrap(); // signature + buf.extend_from_slice(&[0x80, 0x80, 0x80, 0x80, 0x80]); // malformed cert_count let result = SignatureForHashes::deserialize(&buf); assert!( diff --git a/src/lib/src/signature/simple.rs b/src/lib/src/signature/simple.rs index 640ba7f..b5801f2 100644 --- a/src/lib/src/signature/simple.rs +++ b/src/lib/src/signature/simple.rs @@ -236,7 +236,7 @@ impl PublicKeySet { let mut valid_pks = HashSet::new(); for (pk, valid_hashes) in valid_hashes_for_pks { // SECURITY: Use constant-time comparison to prevent timing attacks - if ct_contains_hash(&valid_hashes, &h) { + if ct_contains_hash(&valid_hashes, &h) { valid_pks.insert(pk); } } diff --git a/src/lib/src/slsa.rs b/src/lib/src/slsa.rs index 3ca357a..5b0f6cb 100644 --- a/src/lib/src/slsa.rs +++ b/src/lib/src/slsa.rs @@ -391,15 +391,9 @@ mod tests { #[test] fn test_wasm_build_provenance() { - let deps = vec![ - ResourceDescriptor::new("pkg:cargo/serde@1.0", "abc123"), - ]; - - let prov = Provenance::wasm_build( - "wasm32-wasip2", - Builder::github_actions().id, - deps, - ); + let deps = vec![ResourceDescriptor::new("pkg:cargo/serde@1.0", "abc123")]; + + let prov = Provenance::wasm_build("wasm32-wasip2", Builder::github_actions().id, deps); assert!(prov.build_definition.build_type.contains("WasmBuild")); assert_eq!(prov.build_definition.resolved_dependencies.len(), 1); @@ -408,16 +402,9 @@ mod tests { #[test] fn test_transformation_provenance() { - let inputs = vec![ - ResourceDescriptor::from_name("input.wasm", "deadbeef"), - ]; - - let prov = Provenance::transformation( - "optimization", - "loom", - "0.1.0", - inputs, - ); + let inputs = vec![ResourceDescriptor::from_name("input.wasm", "deadbeef")]; + + let prov = Provenance::transformation("optimization", "loom", "0.1.0", inputs); assert!(prov.build_definition.build_type.contains("Transformation")); assert!(prov.build_definition.build_type.contains("optimization")); @@ -441,7 +428,10 @@ mod tests { // Roundtrip let parsed: Provenance = serde_json::from_str(&json).unwrap(); - assert_eq!(parsed.build_definition.build_type, prov.build_definition.build_type); + assert_eq!( + parsed.build_definition.build_type, + prov.build_definition.build_type + ); } #[test] @@ -452,9 +442,11 @@ mod tests { let local = Builder::local(); assert!(local.id.contains("local")); - let custom = Builder::new("https://my-builder.com") - .with_version("runner", "2.0"); - assert_eq!(custom.version.as_ref().unwrap().get("runner"), Some(&"2.0".to_string())); + let custom = Builder::new("https://my-builder.com").with_version("runner", "2.0"); + assert_eq!( + custom.version.as_ref().unwrap().get("runner"), + Some(&"2.0".to_string()) + ); } #[test] diff --git a/src/lib/src/time.rs b/src/lib/src/time.rs index 42fc322..d840f79 100644 --- a/src/lib/src/time.rs +++ b/src/lib/src/time.rs @@ -288,7 +288,10 @@ pub struct TimeValidationConfig { impl std::fmt::Debug for TimeValidationConfig { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("TimeValidationConfig") - .field("time_source", &self.time_source.as_ref().map(|_| "")) + .field( + "time_source", + &self.time_source.as_ref().map(|_| ""), + ) .field("max_signature_age", &self.max_signature_age) .field("clock_skew_tolerance", &self.clock_skew_tolerance) .finish() @@ -367,7 +370,10 @@ impl Clone for TimeValidationConfig { /// * `Ok(true)` - Timestamp is valid /// * `Ok(false)` - Timestamp fails validation (but not an error) /// * `Err(_)` - Validation could not be performed -pub fn validate_timestamp(timestamp_secs: u64, config: &TimeValidationConfig) -> Result { +pub fn validate_timestamp( + timestamp_secs: u64, + config: &TimeValidationConfig, +) -> Result { // Check against minimum (build time) if timestamp_secs < BUILD_TIMESTAMP { log::warn!( @@ -432,19 +438,36 @@ pub fn parse_timestamp(timestamp: &str) -> Result { // Simple parser for common format if timestamp.len() >= 20 && timestamp.ends_with('Z') { // Parse: 2024-01-15T12:30:45Z or 2024-01-15T12:30:45.123Z - let parts: Vec<&str> = timestamp[..19].split(|c| c == '-' || c == 'T' || c == ':').collect(); + let parts: Vec<&str> = timestamp[..19] + .split(|c| c == '-' || c == 'T' || c == ':') + .collect(); if parts.len() == 6 { - let year: i32 = parts[0].parse().map_err(|_| WSError::TimeError("Invalid year".into()))?; - let month: u32 = parts[1].parse().map_err(|_| WSError::TimeError("Invalid month".into()))?; - let day: u32 = parts[2].parse().map_err(|_| WSError::TimeError("Invalid day".into()))?; - let hour: u32 = parts[3].parse().map_err(|_| WSError::TimeError("Invalid hour".into()))?; - let minute: u32 = parts[4].parse().map_err(|_| WSError::TimeError("Invalid minute".into()))?; - let second: u32 = parts[5].parse().map_err(|_| WSError::TimeError("Invalid second".into()))?; + let year: i32 = parts[0] + .parse() + .map_err(|_| WSError::TimeError("Invalid year".into()))?; + let month: u32 = parts[1] + .parse() + .map_err(|_| WSError::TimeError("Invalid month".into()))?; + let day: u32 = parts[2] + .parse() + .map_err(|_| WSError::TimeError("Invalid day".into()))?; + let hour: u32 = parts[3] + .parse() + .map_err(|_| WSError::TimeError("Invalid hour".into()))?; + let minute: u32 = parts[4] + .parse() + .map_err(|_| WSError::TimeError("Invalid minute".into()))?; + let second: u32 = parts[5] + .parse() + .map_err(|_| WSError::TimeError("Invalid second".into()))?; // Convert to Unix timestamp using simplified calculation // (accurate for dates 1970-2100) let days = days_since_epoch(year, month, day)?; - let secs = (days as u64) * 86400 + (hour as u64) * 3600 + (minute as u64) * 60 + (second as u64); + let secs = (days as u64) * 86400 + + (hour as u64) * 3600 + + (minute as u64) * 60 + + (second as u64); return Ok(secs); } } @@ -576,8 +599,7 @@ mod tests { #[test] fn test_validate_timestamp_with_max_age() { - let config = TimeValidationConfig::with_system_time() - .max_age(Duration::from_secs(3600)); // 1 hour max age + let config = TimeValidationConfig::with_system_time().max_age(Duration::from_secs(3600)); // 1 hour max age let now = SystemTimeSource.now_unix().unwrap(); @@ -604,8 +626,7 @@ mod tests { #[test] fn test_validate_timestamp_future_with_skew() { - let config = TimeValidationConfig::with_system_time() - .clock_skew(Duration::from_secs(300)); // 5 min tolerance + let config = TimeValidationConfig::with_system_time().clock_skew(Duration::from_secs(300)); // 5 min tolerance let now = SystemTimeSource.now_unix().unwrap(); diff --git a/src/lib/src/transcoding.rs b/src/lib/src/transcoding.rs index 7afcacb..db92974 100644 --- a/src/lib/src/transcoding.rs +++ b/src/lib/src/transcoding.rs @@ -288,7 +288,11 @@ impl TranscodingAttestationBuilder { } /// Set the target architecture and output format - pub fn target(mut self, architecture: impl Into, output_format: impl Into) -> Self { + pub fn target( + mut self, + architecture: impl Into, + output_format: impl Into, + ) -> Self { self.target_architecture = Some(architecture.into()); self.target_output_format = Some(output_format.into()); self @@ -325,11 +329,7 @@ impl TranscodingAttestationBuilder { } /// Set source verification results - pub fn source_verification( - mut self, - signature_verified: bool, - chain_verified: bool, - ) -> Self { + pub fn source_verification(mut self, signature_verified: bool, chain_verified: bool) -> Self { self.signature_verified = Some(signature_verified); self.chain_verified = Some(chain_verified); self @@ -368,7 +368,9 @@ impl TranscodingAttestationBuilder { let compiler = CompilerInfo { name: self.compiler_name.ok_or(crate::WSError::InvalidArgument)?, - version: self.compiler_version.ok_or(crate::WSError::InvalidArgument)?, + version: self + .compiler_version + .ok_or(crate::WSError::InvalidArgument)?, digest: self.compiler_digest, uri: self.compiler_uri, }; @@ -398,17 +400,16 @@ impl TranscodingAttestationBuilder { None }; - let verification = - if self.signature_verified.is_some() || self.chain_verified.is_some() { - Some(SourceVerification { - signature_verified: self.signature_verified.unwrap_or(false), - chain_verified: self.chain_verified.unwrap_or(false), - policy: self.verification_policy, - verified_at: self.verified_at, - }) - } else { - None - }; + let verification = if self.signature_verified.is_some() || self.chain_verified.is_some() { + Some(SourceVerification { + signature_verified: self.signature_verified.unwrap_or(false), + chain_verified: self.chain_verified.unwrap_or(false), + policy: self.verification_policy, + verified_at: self.verified_at, + }) + } else { + None + }; Ok(TranscodingPredicate { source, @@ -495,10 +496,7 @@ mod tests { fn test_builder_full() { let predicate = sample_predicate(); - assert_eq!( - predicate.source.digest.sha256_value(), - Some("aabbccdd") - ); + assert_eq!(predicate.source.digest.sha256_value(), Some("aabbccdd")); assert_eq!(predicate.source.signature_status, "verified"); assert_eq!( predicate.source.signer_identity.as_deref(), @@ -557,7 +555,10 @@ mod tests { .compiler("synth", "0.1.0") .target("aarch64", "elf") .build(); - assert!(result.is_err(), "build() should fail without signature_status"); + assert!( + result.is_err(), + "build() should fail without signature_status" + ); } #[test] @@ -672,11 +673,8 @@ mod tests { #[test] fn test_statement_serialization_roundtrip() { let predicate = sample_predicate(); - let statement = create_transcoding_statement( - "firmware.elf", - DigestSet::sha256("deadbeef"), - predicate, - ); + let statement = + create_transcoding_statement("firmware.elf", DigestSet::sha256("deadbeef"), predicate); let json = statement.to_json_pretty().unwrap(); @@ -689,8 +687,7 @@ mod tests { assert!(json.contains("aarch64")); // Roundtrip - let parsed: Statement = - Statement::from_json(&json).unwrap(); + let parsed: Statement = Statement::from_json(&json).unwrap(); assert_eq!(parsed.subject[0].name, "firmware.elf"); assert_eq!(parsed.predicate.compiler.name, "synth"); assert_eq!(parsed.predicate.target.architecture, "aarch64"); @@ -710,15 +707,11 @@ mod tests { .build() .unwrap(); - let statement = create_transcoding_statement( - "app.mcuboot", - DigestSet::sha256("112233"), - predicate, - ); + let statement = + create_transcoding_statement("app.mcuboot", DigestSet::sha256("112233"), predicate); let bytes = statement.to_json_bytes().unwrap(); - let parsed: Statement = - Statement::from_json_bytes(&bytes).unwrap(); + let parsed: Statement = Statement::from_json_bytes(&bytes).unwrap(); assert_eq!(parsed.subject[0].name, "app.mcuboot"); assert_eq!(parsed.predicate.target.output_format, "mcuboot"); @@ -737,10 +730,7 @@ mod tests { #[test] fn test_constants() { - assert_eq!( - TRANSCODING_PREDICATE_V1, - "https://wsc.dev/transcoding/v1" - ); + assert_eq!(TRANSCODING_PREDICATE_V1, "https://wsc.dev/transcoding/v1"); assert_eq!( WASM_NATIVE_BUILD_TYPE, "https://wsc.dev/WasmNativeTranscode/v1" diff --git a/src/lib/src/wasm_module/mod.rs b/src/lib/src/wasm_module/mod.rs index 621f7f6..604e8ee 100644 --- a/src/lib/src/wasm_module/mod.rs +++ b/src/lib/src/wasm_module/mod.rs @@ -189,7 +189,8 @@ impl SectionLike for CustomSection { SIGNATURE_SECTION_DELIMITER_NAME => format!( "custom section: [{}]\n- delimiter: [{}]\n", self.name, - Hex::encode_to_string(self.payload()).unwrap_or_else(|_| "".to_string()) + Hex::encode_to_string(self.payload()) + .unwrap_or_else(|_| "".to_string()) ), SIGNATURE_SECTION_HEADER_NAME => { let signature_data = match SignatureData::deserialize(self.payload()) { @@ -212,17 +213,22 @@ impl SectionLike for CustomSection { for signed_parts in &signature_data.signed_hashes_set { let _ = writeln!(s, " - hashes:"); for hash in &signed_parts.hashes { - let hex = Hex::encode_to_string(hash).unwrap_or_else(|_| "".to_string()); + let hex = Hex::encode_to_string(hash) + .unwrap_or_else(|_| "".to_string()); let _ = writeln!(s, " - [{}]", hex); } let _ = writeln!(s, " - signatures:"); for signature in &signed_parts.signatures { - let hex = Hex::encode_to_string(&signature.signature).unwrap_or_else(|_| "".to_string()); + let hex = Hex::encode_to_string(&signature.signature) + .unwrap_or_else(|_| "".to_string()); let _ = write!(s, " - [{}]", hex); match &signature.key_id { - None => { let _ = writeln!(s, " (no key id)"); } + None => { + let _ = writeln!(s, " (no key id)"); + } Some(key_id) => { - let key_hex = Hex::encode_to_string(key_id).unwrap_or_else(|_| "".to_string()); + let key_hex = Hex::encode_to_string(key_id) + .unwrap_or_else(|_| "".to_string()); let _ = writeln!(s, " (key id: [{}])", key_hex); } } diff --git a/src/lib/tests/airgapped_e2e.rs b/src/lib/tests/airgapped_e2e.rs index e1e830a..79a96c5 100644 --- a/src/lib/tests/airgapped_e2e.rs +++ b/src/lib/tests/airgapped_e2e.rs @@ -11,9 +11,8 @@ use wsc::{ Module, airgapped::{ - AirGappedConfig, AirGappedVerifier, SignedTrustBundle, TrustBundle, - MemoryTrustStore, MemoryKeyStore, - fetch_sigstore_trusted_root, trusted_root_to_bundle, + AirGappedConfig, AirGappedVerifier, MemoryKeyStore, MemoryTrustStore, SignedTrustBundle, + TrustBundle, fetch_sigstore_trusted_root, trusted_root_to_bundle, }, keyless::{KeylessConfig, KeylessSigner}, }; @@ -47,10 +46,16 @@ fn test_bundle_fetch_and_parse() { match result { Ok(root) => { println!("Successfully fetched Sigstore trusted root:"); - println!(" Certificate Authorities: {}", root.certificate_authorities.len()); + println!( + " Certificate Authorities: {}", + root.certificate_authorities.len() + ); println!(" Transparency Logs: {}", root.tlogs.len()); - assert!(!root.certificate_authorities.is_empty(), "Should have at least one CA"); + assert!( + !root.certificate_authorities.is_empty(), + "Should have at least one CA" + ); assert!(!root.tlogs.is_empty(), "Should have at least one tlog"); // Convert to bundle @@ -123,16 +128,17 @@ fn test_full_airgapped_flow_with_sigstore() { // Step 1: Fetch trust bundle from Sigstore TUF println!("1. Fetching trust bundle from Sigstore TUF..."); - let trusted_root = fetch_sigstore_trusted_root() - .expect("Failed to fetch Sigstore trusted root"); + let trusted_root = + fetch_sigstore_trusted_root().expect("Failed to fetch Sigstore trusted root"); - println!(" Found {} CAs, {} transparency logs", + println!( + " Found {} CAs, {} transparency logs", trusted_root.certificate_authorities.len(), trusted_root.tlogs.len() ); - let bundle = trusted_root_to_bundle(&trusted_root, 1, 90) - .expect("Failed to create trust bundle"); + let bundle = + trusted_root_to_bundle(&trusted_root, 1, 90).expect("Failed to create trust bundle"); println!(" Bundle ID: {}", &bundle.bundle_id[..16]); @@ -140,8 +146,8 @@ fn test_full_airgapped_flow_with_sigstore() { use ed25519_compact::KeyPair; let bundle_keypair = KeyPair::generate(); let seed = bundle_keypair.sk.seed(); - let signed_bundle = SignedTrustBundle::sign(bundle, seed.as_ref()) - .expect("Failed to sign bundle"); + let signed_bundle = + SignedTrustBundle::sign(bundle, seed.as_ref()).expect("Failed to sign bundle"); println!(" Bundle signed with test key"); @@ -149,15 +155,19 @@ fn test_full_airgapped_flow_with_sigstore() { println!("\n2. Signing WASM module with keyless signing..."); let config = KeylessConfig::default(); - let signer = KeylessSigner::with_config(config) - .expect("Failed to create keyless signer"); + let signer = KeylessSigner::with_config(config).expect("Failed to create keyless signer"); let module = create_test_module(); - let (signed_module, keyless_sig) = signer.sign_module(module) - .expect("Failed to sign module"); + let (signed_module, keyless_sig) = signer.sign_module(module).expect("Failed to sign module"); - println!(" Identity: {}", keyless_sig.get_identity().unwrap_or_default()); - println!(" Issuer: {}", keyless_sig.get_issuer().unwrap_or_default()); + println!( + " Identity: {}", + keyless_sig.get_identity().unwrap_or_default() + ); + println!( + " Issuer: {}", + keyless_sig.get_issuer().unwrap_or_default() + ); println!(" Rekor entry: {}", keyless_sig.rekor_entry.uuid); // Step 3: Verify using air-gapped verifier @@ -167,7 +177,8 @@ fn test_full_airgapped_flow_with_sigstore() { &signed_bundle, bundle_keypair.pk.as_ref(), AirGappedConfig::default(), - ).expect("Failed to create air-gapped verifier"); + ) + .expect("Failed to create air-gapped verifier"); // Compute module hash let mut module_bytes = Vec::new(); @@ -193,8 +204,14 @@ fn test_full_airgapped_flow_with_sigstore() { println!("\n❌ Verification failed: {}", e); println!("\n📋 Debug info:"); println!(" Cert chain length: {}", keyless_sig.cert_chain.len()); - println!(" Bundle CAs: {}", signed_bundle.bundle.certificate_authorities.len()); - println!(" Bundle logs: {}", signed_bundle.bundle.transparency_logs.len()); + println!( + " Bundle CAs: {}", + signed_bundle.bundle.certificate_authorities.len() + ); + println!( + " Bundle logs: {}", + signed_bundle.bundle.transparency_logs.len() + ); // This is expected to fail initially - certificate chain verification // against the bundle's root certs needs the full implementation @@ -217,11 +234,13 @@ fn test_keyless_sign_then_airgapped_verify() { // Sign test module let module = create_test_module(); - let (_signed_module, keyless_sig) = signer.sign_module(module) - .expect("Failed to sign module"); + let (_signed_module, keyless_sig) = signer.sign_module(module).expect("Failed to sign module"); println!("Keyless signature created:"); - println!(" Identity: {}", keyless_sig.get_identity().unwrap_or_default()); + println!( + " Identity: {}", + keyless_sig.get_identity().unwrap_or_default() + ); println!(" Rekor UUID: {}", keyless_sig.rekor_entry.uuid); println!(" Rekor index: {}", keyless_sig.rekor_entry.log_index); @@ -247,15 +266,16 @@ fn test_bundle_anti_rollback() { // Try to create verifier with older bundle (version 1) let config = AirGappedConfig::default().with_rollback_protection(); - let verifier = AirGappedVerifier::::new( - &signed_bundle, - &public_key, - config, - ).unwrap(); + let verifier = + AirGappedVerifier::::new(&signed_bundle, &public_key, config) + .unwrap(); // Adding state should fail due to rollback protection let result = verifier.with_device_state(state); - assert!(result.is_err(), "Should reject bundle older than device state"); + assert!( + result.is_err(), + "Should reject bundle older than device state" + ); println!("Anti-rollback protection working correctly"); } diff --git a/src/lib/tests/keyless_integration.rs b/src/lib/tests/keyless_integration.rs index f6776ac..650dbbb 100644 --- a/src/lib/tests/keyless_integration.rs +++ b/src/lib/tests/keyless_integration.rs @@ -214,10 +214,7 @@ fn test_keyless_signing_without_oidc_fails() { "No OIDC provider detected (expected in dev environment): {}", e ); - assert!( - matches!(e, WSError::NoOidcProvider) - || matches!(e, WSError::OidcError(_)) - ); + assert!(matches!(e, WSError::NoOidcProvider) || matches!(e, WSError::OidcError(_))); } } }