Files
rustfs/crates/security-governance/src/supply_chain.rs
T
安正超 3d0e6ce0da feat: add security governance policy contracts (#3271)
* feat: add security governance policy contracts

* fix: require signatures for release assets

* docs: update policy verification counts

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-06-08 02:42:03 +00:00

278 lines
8.2 KiB
Rust

// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::collections::BTreeSet;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ArtifactSourceKind {
WorkspaceBuild,
ThirdPartyDownload,
GeneratedReleaseAsset,
}
impl ArtifactSourceKind {
pub const fn requires_digest(self) -> bool {
matches!(self, Self::ThirdPartyDownload | Self::GeneratedReleaseAsset)
}
pub const fn requires_provenance(self) -> bool {
matches!(self, Self::GeneratedReleaseAsset)
}
pub const fn requires_signature(self) -> bool {
matches!(self, Self::GeneratedReleaseAsset)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ArtifactIntegrityPolicy {
artifact: &'static str,
source: ArtifactSourceKind,
digest_required: bool,
signature_required: bool,
provenance_required: bool,
}
impl ArtifactIntegrityPolicy {
pub const fn new(
artifact: &'static str,
source: ArtifactSourceKind,
digest_required: bool,
signature_required: bool,
provenance_required: bool,
) -> Self {
Self {
artifact,
source,
digest_required,
signature_required,
provenance_required,
}
}
pub const fn artifact(self) -> &'static str {
self.artifact
}
pub const fn source(self) -> ArtifactSourceKind {
self.source
}
pub const fn digest_required(self) -> bool {
self.digest_required
}
pub const fn signature_required(self) -> bool {
self.signature_required
}
pub const fn provenance_required(self) -> bool {
self.provenance_required
}
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum SupplyChainPolicyError {
#[error("artifact integrity policy at index {index} has an empty artifact")]
EmptyArtifact { index: usize },
#[error("artifact integrity policy for {artifact} must require a digest")]
DigestRequired { artifact: &'static str },
#[error("artifact integrity policy for {artifact} must require provenance")]
ProvenanceRequired { artifact: &'static str },
#[error("artifact integrity policy for {artifact} must require a signature")]
SignatureRequired { artifact: &'static str },
#[error("duplicate artifact integrity policy for {artifact}")]
DuplicateArtifact { artifact: &'static str },
}
pub fn validate_artifact_integrity_policies(policies: &[ArtifactIntegrityPolicy]) -> Result<(), SupplyChainPolicyError> {
let mut artifacts = BTreeSet::new();
for (index, policy) in policies.iter().copied().enumerate() {
if policy.artifact.trim().is_empty() {
return Err(SupplyChainPolicyError::EmptyArtifact { index });
}
if policy.source.requires_digest() && !policy.digest_required {
return Err(SupplyChainPolicyError::DigestRequired {
artifact: policy.artifact,
});
}
if policy.source.requires_provenance() && !policy.provenance_required {
return Err(SupplyChainPolicyError::ProvenanceRequired {
artifact: policy.artifact,
});
}
if policy.source.requires_signature() && !policy.signature_required {
return Err(SupplyChainPolicyError::SignatureRequired {
artifact: policy.artifact,
});
}
if !artifacts.insert(policy.artifact) {
return Err(SupplyChainPolicyError::DuplicateArtifact {
artifact: policy.artifact,
});
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validates_artifact_integrity_policies() {
let policies = [
ArtifactIntegrityPolicy::new("rustfs-server", ArtifactSourceKind::WorkspaceBuild, false, true, false),
ArtifactIntegrityPolicy::new(
"rustfs-cli-windows-amd64.zip",
ArtifactSourceKind::GeneratedReleaseAsset,
true,
true,
true,
),
];
assert!(validate_artifact_integrity_policies(&policies).is_ok());
assert_eq!(policies[0].artifact(), "rustfs-server");
assert_eq!(policies[0].source(), ArtifactSourceKind::WorkspaceBuild);
assert!(policies[1].digest_required());
assert!(policies[1].signature_required());
assert!(policies[1].provenance_required());
}
#[test]
fn rejects_empty_artifacts() {
let policies = [ArtifactIntegrityPolicy::new(
" ",
ArtifactSourceKind::WorkspaceBuild,
false,
false,
false,
)];
let err = validate_artifact_integrity_policies(&policies).expect_err("empty artifact should fail validation");
assert_eq!(err, SupplyChainPolicyError::EmptyArtifact { index: 0 });
}
#[test]
fn rejects_external_artifacts_without_digest() {
let policies = [ArtifactIntegrityPolicy::new(
"third-party-tool",
ArtifactSourceKind::ThirdPartyDownload,
false,
false,
false,
)];
let err = validate_artifact_integrity_policies(&policies).expect_err("third-party artifact should require digest");
assert_eq!(
err,
SupplyChainPolicyError::DigestRequired {
artifact: "third-party-tool"
}
);
}
#[test]
fn rejects_generated_artifacts_without_digest() {
let policies = [ArtifactIntegrityPolicy::new(
"rustfs-cli-windows-amd64.zip",
ArtifactSourceKind::GeneratedReleaseAsset,
false,
true,
true,
)];
let err = validate_artifact_integrity_policies(&policies).expect_err("generated artifact should require digest");
assert_eq!(
err,
SupplyChainPolicyError::DigestRequired {
artifact: "rustfs-cli-windows-amd64.zip"
}
);
}
#[test]
fn rejects_generated_artifacts_without_provenance() {
let policies = [ArtifactIntegrityPolicy::new(
"rustfs-cli-windows-amd64.zip",
ArtifactSourceKind::GeneratedReleaseAsset,
true,
true,
false,
)];
let err = validate_artifact_integrity_policies(&policies).expect_err("generated artifact should require provenance");
assert_eq!(
err,
SupplyChainPolicyError::ProvenanceRequired {
artifact: "rustfs-cli-windows-amd64.zip"
}
);
}
#[test]
fn rejects_generated_artifacts_without_signature() {
let policies = [ArtifactIntegrityPolicy::new(
"rustfs-cli-windows-amd64.zip",
ArtifactSourceKind::GeneratedReleaseAsset,
true,
false,
true,
)];
let err = validate_artifact_integrity_policies(&policies).expect_err("generated artifact should require signature");
assert_eq!(
err,
SupplyChainPolicyError::SignatureRequired {
artifact: "rustfs-cli-windows-amd64.zip"
}
);
}
#[test]
fn rejects_duplicate_artifacts() {
let policies = [
ArtifactIntegrityPolicy::new("rustfs-server", ArtifactSourceKind::WorkspaceBuild, false, true, false),
ArtifactIntegrityPolicy::new("rustfs-server", ArtifactSourceKind::WorkspaceBuild, false, false, false),
];
let err = validate_artifact_integrity_policies(&policies).expect_err("duplicate artifact should fail validation");
assert_eq!(
err,
SupplyChainPolicyError::DuplicateArtifact {
artifact: "rustfs-server"
}
);
}
}