diff --git a/crates/targets/src/catalog/mod.rs b/crates/targets/src/catalog/mod.rs index c3e46a7b8..a6e049f5c 100644 --- a/crates/targets/src/catalog/mod.rs +++ b/crates/targets/src/catalog/mod.rs @@ -55,6 +55,8 @@ pub fn example_external_webhook_plugin() -> ExampleInstallableTargetPlugin { target_triple: "x86_64-unknown-linux-gnu", download_uri: "https://plugins.example.test/webhook-sidecar.tar.zst", digest_sha256: "0123456789abcdef0123456789abcdef", + signature_uri: "https://plugins.example.test/webhook-sidecar.tar.zst.sig", + provenance_uri: "https://plugins.example.test/webhook-sidecar.tar.zst.intoto.jsonl", size_bytes: 8192, }], }, diff --git a/crates/targets/src/control_plane.rs b/crates/targets/src/control_plane.rs index f53bff0e4..2ba8639d4 100644 --- a/crates/targets/src/control_plane.rs +++ b/crates/targets/src/control_plane.rs @@ -169,6 +169,7 @@ pub struct TargetPluginInstallPolicy { pub allowed_download_hosts: Vec, pub require_https: bool, pub require_signature: bool, + pub require_provenance: bool, } impl Default for TargetPluginInstallPolicy { @@ -177,7 +178,8 @@ impl Default for TargetPluginInstallPolicy { allowed_providers: vec!["rustfs".to_string(), "rustfs-labs".to_string()], allowed_download_hosts: vec!["plugins.example.test".to_string()], require_https: true, - require_signature: false, + require_signature: true, + require_provenance: true, } } } @@ -201,12 +203,6 @@ pub fn validate_external_plugin_installation( )); } - if policy.require_signature { - return Err( - "signature verification is required by install policy but manifests do not expose signatures yet".to_string(), - ); - } - let distribution = distribution.ok_or_else(|| "external plugin is missing distribution metadata".to_string())?; if distribution.artifacts.is_empty() { return Err("external plugin distribution has no artifacts".to_string()); @@ -236,6 +232,33 @@ pub fn validate_external_plugin_installation( artifact.artifact_id, artifact.digest_sha256 )); } + if policy.require_signature && artifact.signature_uri.is_empty() { + return Err(format!("artifact {} must declare a signature uri", artifact.artifact_id)); + } + if policy.require_provenance && artifact.provenance_uri.is_empty() { + return Err(format!("artifact {} must declare a provenance uri", artifact.artifact_id)); + } + validate_artifact_uri("signature", artifact.artifact_id, artifact.signature_uri, policy)?; + validate_artifact_uri("provenance", artifact.artifact_id, artifact.provenance_uri, policy)?; + } + + Ok(()) +} + +fn validate_artifact_uri(label: &str, artifact_id: &str, uri: &str, policy: &TargetPluginInstallPolicy) -> Result<(), String> { + if uri.is_empty() { + return Ok(()); + } + + let parsed_uri = Url::parse(uri).map_err(|err| format!("invalid artifact {label} uri {uri}: {err}"))?; + if policy.require_https && parsed_uri.scheme() != "https" { + return Err(format!("artifact {artifact_id} must use https {label} uri, got {uri}")); + } + let host = parsed_uri + .host_str() + .ok_or_else(|| format!("artifact {artifact_id} {label} uri has no host"))?; + if !policy.allowed_download_hosts.iter().any(|allowed| allowed == host) { + return Err(format!("artifact {artifact_id} {label} host {host} is not allowed")); } Ok(()) @@ -370,6 +393,8 @@ mod tests { target_triple: "x86_64-unknown-linux-gnu", download_uri: "https://plugins.example.test/webhook-sidecar.tar.zst", digest_sha256: "0123456789abcdef0123456789abcdef", + signature_uri: "https://plugins.example.test/webhook-sidecar.tar.zst.sig", + provenance_uri: "https://plugins.example.test/webhook-sidecar.tar.zst.intoto.jsonl", size_bytes: 8192, }], }; @@ -413,6 +438,8 @@ mod tests { target_triple: "x86_64-unknown-linux-gnu", download_uri: "https://plugins.example.test/webhook-sidecar.tar.zst", digest_sha256: "0123456789abcdef0123456789abcdef", + signature_uri: "https://plugins.example.test/webhook-sidecar.tar.zst.sig", + provenance_uri: "https://plugins.example.test/webhook-sidecar.tar.zst.intoto.jsonl", size_bytes: 8192, }], }), @@ -421,4 +448,82 @@ mod tests { assert!(result.is_err()); } + + #[test] + fn validate_external_installation_rejects_missing_artifact_signature() { + let manifest = TargetPluginManifest { + plugin_id: "external:webhook-sidecar", + display_name: "Webhook Sidecar", + provider: "rustfs-labs", + version: "1.0.0", + target_type: "webhook", + supported_domains: &[], + secret_fields: &[], + }; + let policy = TargetPluginInstallPolicy::default(); + + let result = validate_external_plugin_installation( + &manifest, + &TargetPluginExternalRuntimeContract { + protocol_version: crate::SIDECAR_RUNTIME_PROTOCOL_VERSION, + transport: TargetPluginRuntimeTransport::Grpc, + }, + Some(TargetPluginDistributionManifest { + artifacts: &[TargetPluginArtifactManifest { + artifact_id: "sidecar-linux-amd64", + target_triple: "x86_64-unknown-linux-gnu", + download_uri: "https://plugins.example.test/webhook-sidecar.tar.zst", + digest_sha256: "0123456789abcdef0123456789abcdef", + signature_uri: "", + provenance_uri: "https://plugins.example.test/webhook-sidecar.intoto.jsonl", + size_bytes: 8192, + }], + }), + &policy, + ); + + assert_eq!( + result.as_ref().map_err(String::as_str), + Err("artifact sidecar-linux-amd64 must declare a signature uri") + ); + } + + #[test] + fn validate_external_installation_rejects_missing_artifact_provenance() { + let manifest = TargetPluginManifest { + plugin_id: "external:webhook-sidecar", + display_name: "Webhook Sidecar", + provider: "rustfs-labs", + version: "1.0.0", + target_type: "webhook", + supported_domains: &[], + secret_fields: &[], + }; + let policy = TargetPluginInstallPolicy::default(); + + let result = validate_external_plugin_installation( + &manifest, + &TargetPluginExternalRuntimeContract { + protocol_version: crate::SIDECAR_RUNTIME_PROTOCOL_VERSION, + transport: TargetPluginRuntimeTransport::Grpc, + }, + Some(TargetPluginDistributionManifest { + artifacts: &[TargetPluginArtifactManifest { + artifact_id: "sidecar-linux-amd64", + target_triple: "x86_64-unknown-linux-gnu", + download_uri: "https://plugins.example.test/webhook-sidecar.tar.zst", + digest_sha256: "0123456789abcdef0123456789abcdef", + signature_uri: "https://plugins.example.test/webhook-sidecar.sig", + provenance_uri: "", + size_bytes: 8192, + }], + }), + &policy, + ); + + assert_eq!( + result.as_ref().map_err(String::as_str), + Err("artifact sidecar-linux-amd64 must declare a provenance uri") + ); + } } diff --git a/crates/targets/src/manifest.rs b/crates/targets/src/manifest.rs index 6a683197f..0d7ef3b17 100644 --- a/crates/targets/src/manifest.rs +++ b/crates/targets/src/manifest.rs @@ -71,6 +71,8 @@ pub struct TargetPluginArtifactManifest { pub target_triple: &'static str, pub download_uri: &'static str, pub digest_sha256: &'static str, + pub signature_uri: &'static str, + pub provenance_uri: &'static str, pub size_bytes: u64, } @@ -297,6 +299,8 @@ mod tests { target_triple: "x86_64-unknown-linux-gnu", download_uri: "https://plugins.example.test/webhook-plugin.tar.zst", digest_sha256: "0123456789abcdef", + signature_uri: "https://plugins.example.test/webhook-plugin.tar.zst.sig", + provenance_uri: "https://plugins.example.test/webhook-plugin.tar.zst.intoto.jsonl", size_bytes: 4096, }], }, @@ -313,6 +317,8 @@ mod tests { target_triple: "x86_64-unknown-linux-gnu", download_uri: "https://plugins.example.test/webhook-plugin.tar.zst", digest_sha256: "0123456789abcdef", + signature_uri: "https://plugins.example.test/webhook-plugin.tar.zst.sig", + provenance_uri: "https://plugins.example.test/webhook-plugin.tar.zst.intoto.jsonl", size_bytes: 4096, }], }) diff --git a/rustfs/src/admin/plugin_contract.rs b/rustfs/src/admin/plugin_contract.rs index 7c41fee8a..6eb2a69ad 100644 --- a/rustfs/src/admin/plugin_contract.rs +++ b/rustfs/src/admin/plugin_contract.rs @@ -227,6 +227,8 @@ pub(crate) struct PluginArtifactContract { pub target_triple: String, pub download_uri: String, pub digest_sha256: String, + pub signature_uri: String, + pub provenance_uri: String, pub size_bytes: u64, } @@ -237,6 +239,8 @@ impl From for PluginArtifactContract { target_triple: value.target_triple.to_string(), download_uri: value.download_uri.to_string(), digest_sha256: value.digest_sha256.to_string(), + signature_uri: value.signature_uri.to_string(), + provenance_uri: value.provenance_uri.to_string(), size_bytes: value.size_bytes, } } @@ -555,6 +559,8 @@ mod tests { target_triple: "x86_64-unknown-linux-gnu".to_string(), download_uri: "https://plugins.example.test/webhook.tar.zst".to_string(), digest_sha256: "0123456789abcdef".to_string(), + signature_uri: "https://plugins.example.test/webhook.tar.zst.sig".to_string(), + provenance_uri: "https://plugins.example.test/webhook.tar.zst.intoto.jsonl".to_string(), size_bytes: 4096, }], }), @@ -572,6 +578,14 @@ mod tests { "https://plugins.example.test/webhook.tar.zst" ); assert_eq!(value["distribution"]["artifacts"][0]["digest_sha256"], "0123456789abcdef"); + assert_eq!( + value["distribution"]["artifacts"][0]["signature_uri"], + "https://plugins.example.test/webhook.tar.zst.sig" + ); + assert_eq!( + value["distribution"]["artifacts"][0]["provenance_uri"], + "https://plugins.example.test/webhook.tar.zst.intoto.jsonl" + ); assert_eq!(value["distribution"]["artifacts"][0]["size_bytes"], 4096); } }