// ----------------------------------------------------------------------------- // SignatureRequiredGateTests.cs // Sprint: SPRINT_20260112_017_POLICY_signature_required_gate // Tasks: SIG-GATE-009 // Description: Unit tests for signature required gate. // ----------------------------------------------------------------------------- using System.Collections.Immutable; using StellaOps.Policy.Confidence.Models; using StellaOps.Policy.Gates; using StellaOps.Policy.TrustLattice; using Xunit; namespace StellaOps.Policy.Tests.Gates; [Trait("Category", "Unit")] public sealed class SignatureRequiredGateTests { private static MergeResult CreateMergeResult() => new() { Status = VexStatus.Affected, Confidence = 0.8, HasConflicts = false, AllClaims = ImmutableArray.Empty, WinningClaim = new ScoredClaim { SourceId = "test", Status = VexStatus.Affected, OriginalScore = 0.8, AdjustedScore = 0.8, ScopeSpecificity = 1, Accepted = true, Reason = "test" }, Conflicts = ImmutableArray.Empty }; private static PolicyGateContext CreateContext(string environment = "production") => new() { Environment = environment }; [Fact] public async Task EvaluateAsync_Disabled_ReturnsPass() { var options = new SignatureRequiredGateOptions { Enabled = false }; var gate = new SignatureRequiredGate(options); var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext()); Assert.True(result.Passed); Assert.Equal("disabled", result.Reason); } [Fact] public async Task EvaluateAsync_MissingSignature_ReturnsFail() { var options = new SignatureRequiredGateOptions(); var signatures = new List(); // No signatures var gate = new SignatureRequiredGate(options, _ => signatures); var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext()); Assert.False(result.Passed); Assert.Equal("signature_validation_failed", result.Reason); } [Fact] public async Task EvaluateAsync_AllValidSignatures_ReturnsPass() { var options = new SignatureRequiredGateOptions(); var signatures = new List { new() { EvidenceType = "sbom", HasSignature = true, SignatureValid = true }, new() { EvidenceType = "vex", HasSignature = true, SignatureValid = true }, new() { EvidenceType = "attestation", HasSignature = true, SignatureValid = true } }; var gate = new SignatureRequiredGate(options, _ => signatures); var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext()); Assert.True(result.Passed); Assert.Equal("signatures_verified", result.Reason); } [Fact] public async Task EvaluateAsync_InvalidSignature_ReturnsFail() { var options = new SignatureRequiredGateOptions(); var signatures = new List { new() { EvidenceType = "sbom", HasSignature = true, SignatureValid = false, VerificationErrors = new[] { "Invalid hash" } }, new() { EvidenceType = "vex", HasSignature = true, SignatureValid = true }, new() { EvidenceType = "attestation", HasSignature = true, SignatureValid = true } }; var gate = new SignatureRequiredGate(options, _ => signatures); var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext()); Assert.False(result.Passed); Assert.Contains("failures", result.Details.Keys); } [Fact] public async Task EvaluateAsync_NotRequiredType_PassesWithoutSignature() { var options = new SignatureRequiredGateOptions { EvidenceTypes = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["sbom"] = new EvidenceSignatureConfig { Required = false }, ["vex"] = new EvidenceSignatureConfig { Required = true }, ["attestation"] = new EvidenceSignatureConfig { Required = true } } }; var signatures = new List { // No SBOM signature - but it's not required new() { EvidenceType = "vex", HasSignature = true, SignatureValid = true }, new() { EvidenceType = "attestation", HasSignature = true, SignatureValid = true } }; var gate = new SignatureRequiredGate(options, _ => signatures); var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext()); Assert.True(result.Passed); } [Theory] [InlineData("build@company.com", new[] { "build@company.com" }, true)] [InlineData("release@company.com", new[] { "*@company.com" }, true)] [InlineData("external@other.com", new[] { "*@company.com" }, false)] [InlineData("build@company.com", new[] { "other@company.com" }, false)] public async Task EvaluateAsync_IssuerValidation_EnforcesConstraints( string signerIdentity, string[] trustedIssuers, bool expectedPass) { var options = new SignatureRequiredGateOptions { EvidenceTypes = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["sbom"] = new EvidenceSignatureConfig { Required = true, TrustedIssuers = new HashSet(trustedIssuers, StringComparer.OrdinalIgnoreCase) }, ["vex"] = new EvidenceSignatureConfig { Required = false }, ["attestation"] = new EvidenceSignatureConfig { Required = false } } }; var signatures = new List { new() { EvidenceType = "sbom", HasSignature = true, SignatureValid = true, SignerIdentity = signerIdentity } }; var gate = new SignatureRequiredGate(options, _ => signatures); var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext()); Assert.Equal(expectedPass, result.Passed); } [Theory] [InlineData("ES256", true)] [InlineData("RS256", true)] [InlineData("EdDSA", true)] [InlineData("UNKNOWN", false)] public async Task EvaluateAsync_AlgorithmValidation_EnforcesAccepted(string algorithm, bool expectedPass) { var options = new SignatureRequiredGateOptions { EvidenceTypes = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["sbom"] = new EvidenceSignatureConfig { Required = true }, ["vex"] = new EvidenceSignatureConfig { Required = false }, ["attestation"] = new EvidenceSignatureConfig { Required = false } } }; var signatures = new List { new() { EvidenceType = "sbom", HasSignature = true, SignatureValid = true, Algorithm = algorithm } }; var gate = new SignatureRequiredGate(options, _ => signatures); var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext()); Assert.Equal(expectedPass, result.Passed); } [Fact] public async Task EvaluateAsync_KeyIdValidation_EnforcesConstraints() { var options = new SignatureRequiredGateOptions { EvidenceTypes = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["sbom"] = new EvidenceSignatureConfig { Required = true, TrustedKeyIds = new HashSet(StringComparer.OrdinalIgnoreCase) { "key-001", "key-002" } }, ["vex"] = new EvidenceSignatureConfig { Required = false }, ["attestation"] = new EvidenceSignatureConfig { Required = false } } }; var signatures = new List { new() { EvidenceType = "sbom", HasSignature = true, SignatureValid = true, KeyId = "key-999", IsKeyless = false } }; var gate = new SignatureRequiredGate(options, _ => signatures); var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext()); Assert.False(result.Passed); } [Fact] public async Task EvaluateAsync_KeylessSignature_ValidWithTransparencyLog() { var options = new SignatureRequiredGateOptions { EnableKeylessVerification = true, RequireTransparencyLogInclusion = true, EvidenceTypes = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["sbom"] = new EvidenceSignatureConfig { Required = true }, ["vex"] = new EvidenceSignatureConfig { Required = false }, ["attestation"] = new EvidenceSignatureConfig { Required = false } } }; var signatures = new List { new() { EvidenceType = "sbom", HasSignature = true, SignatureValid = true, IsKeyless = true, HasTransparencyLogInclusion = true, CertificateChainValid = true } }; var gate = new SignatureRequiredGate(options, _ => signatures); var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext()); Assert.True(result.Passed); } [Fact] public async Task EvaluateAsync_KeylessSignature_FailsWithoutTransparencyLog() { var options = new SignatureRequiredGateOptions { EnableKeylessVerification = true, RequireTransparencyLogInclusion = true, EvidenceTypes = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["sbom"] = new EvidenceSignatureConfig { Required = true }, ["vex"] = new EvidenceSignatureConfig { Required = false }, ["attestation"] = new EvidenceSignatureConfig { Required = false } } }; var signatures = new List { new() { EvidenceType = "sbom", HasSignature = true, SignatureValid = true, IsKeyless = true, HasTransparencyLogInclusion = false } }; var gate = new SignatureRequiredGate(options, _ => signatures); var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext()); Assert.False(result.Passed); } [Fact] public async Task EvaluateAsync_KeylessDisabled_FailsKeylessSignature() { var options = new SignatureRequiredGateOptions { EnableKeylessVerification = false, EvidenceTypes = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["sbom"] = new EvidenceSignatureConfig { Required = true }, ["vex"] = new EvidenceSignatureConfig { Required = false }, ["attestation"] = new EvidenceSignatureConfig { Required = false } } }; var signatures = new List { new() { EvidenceType = "sbom", HasSignature = true, SignatureValid = true, IsKeyless = true } }; var gate = new SignatureRequiredGate(options, _ => signatures); var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext()); Assert.False(result.Passed); } [Fact] public async Task EvaluateAsync_EnvironmentOverride_SkipsTypes() { var options = new SignatureRequiredGateOptions { EvidenceTypes = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["sbom"] = new EvidenceSignatureConfig { Required = true }, ["vex"] = new EvidenceSignatureConfig { Required = true }, ["attestation"] = new EvidenceSignatureConfig { Required = true } }, Environments = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["development"] = new EnvironmentSignatureConfig { SkipEvidenceTypes = new HashSet(StringComparer.OrdinalIgnoreCase) { "sbom", "vex" } } } }; var signatures = new List { // Only attestation signature in development new() { EvidenceType = "attestation", HasSignature = true, SignatureValid = true } }; var gate = new SignatureRequiredGate(options, _ => signatures); var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext(environment: "development")); Assert.True(result.Passed); } [Fact] public async Task EvaluateAsync_EnvironmentOverride_AddsIssuers() { var options = new SignatureRequiredGateOptions { EvidenceTypes = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["sbom"] = new EvidenceSignatureConfig { Required = true, TrustedIssuers = new HashSet(StringComparer.OrdinalIgnoreCase) { "prod@company.com" } }, ["vex"] = new EvidenceSignatureConfig { Required = false }, ["attestation"] = new EvidenceSignatureConfig { Required = false } }, Environments = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["staging"] = new EnvironmentSignatureConfig { AdditionalIssuers = new HashSet(StringComparer.OrdinalIgnoreCase) { "staging@company.com" } } } }; var signatures = new List { new() { EvidenceType = "sbom", HasSignature = true, SignatureValid = true, SignerIdentity = "staging@company.com" } }; var gate = new SignatureRequiredGate(options, _ => signatures); var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext(environment: "staging")); Assert.True(result.Passed); } [Fact] public async Task EvaluateAsync_InvalidCertificateChain_Fails() { var options = new SignatureRequiredGateOptions { EvidenceTypes = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["sbom"] = new EvidenceSignatureConfig { Required = true }, ["vex"] = new EvidenceSignatureConfig { Required = false }, ["attestation"] = new EvidenceSignatureConfig { Required = false } } }; var signatures = new List { new() { EvidenceType = "sbom", HasSignature = true, SignatureValid = true, IsKeyless = true, HasTransparencyLogInclusion = true, CertificateChainValid = false } }; var gate = new SignatureRequiredGate(options, _ => signatures); var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext()); Assert.False(result.Passed); } [Fact] public async Task EvaluateAsync_WildcardIssuerMatch_MatchesSubdomains() { var options = new SignatureRequiredGateOptions { EvidenceTypes = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["sbom"] = new EvidenceSignatureConfig { Required = true, TrustedIssuers = new HashSet(StringComparer.OrdinalIgnoreCase) { "*@*.company.com" } }, ["vex"] = new EvidenceSignatureConfig { Required = false }, ["attestation"] = new EvidenceSignatureConfig { Required = false } } }; var signatures = new List { new() { EvidenceType = "sbom", HasSignature = true, SignatureValid = true, SignerIdentity = "build@ci.company.com" } }; var gate = new SignatureRequiredGate(options, _ => signatures); var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext()); Assert.True(result.Passed); } }