using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; using Org.BouncyCastle.Crypto.Generators; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Security; using StellaOps.Attestor.Envelope; using StellaOps.Canonical.Json; using StellaOps.Scanner.Reachability.Witnesses; using Xunit; using StellaOps.TestKit; namespace StellaOps.Scanner.Reachability.Tests; /// /// Tests for . /// Sprint: SPRINT_3700_0001_0001 (WIT-007D) /// Golden fixture tests for DSSE sign/verify. /// public class WitnessDsseSignerTests { private static CancellationToken TestCancellationToken => TestContext.Current.CancellationToken; /// /// Creates a deterministic Ed25519 key pair for testing. /// private static (byte[] privateKey, byte[] publicKey) CreateTestKeyPair() { // Use a fixed seed for deterministic tests var generator = new Ed25519KeyPairGenerator(); generator.Init(new Ed25519KeyGenerationParameters(new SecureRandom(new FixedRandomGenerator()))); var keyPair = generator.GenerateKeyPair(); var privateParams = (Ed25519PrivateKeyParameters)keyPair.Private; var publicParams = (Ed25519PublicKeyParameters)keyPair.Public; // Ed25519 private key = 32-byte seed + 32-byte public key var privateKey = new byte[64]; privateParams.Encode(privateKey, 0); var publicKey = publicParams.GetEncoded(); // Append public key to make 64-byte expanded form Array.Copy(publicKey, 0, privateKey, 32, 32); return (privateKey, publicKey); } [Trait("Category", TestCategories.Unit)] [Fact] public void SignWitness_WithValidKey_ReturnsSuccess() { // Arrange var witness = CreateTestWitness(); var (privateKey, publicKey) = CreateTestKeyPair(); var key = EnvelopeKey.CreateEd25519Signer(privateKey, publicKey); var signer = new WitnessDsseSigner(); // Act var result = signer.SignWitness(witness, key, TestCancellationToken); // Assert Assert.True(result.IsSuccess, result.Error); Assert.NotNull(result.Envelope); Assert.Equal(WitnessSchema.DssePayloadType, result.Envelope.PayloadType); Assert.Single(result.Envelope.Signatures); Assert.NotEmpty(result.PayloadBytes!); } [Trait("Category", TestCategories.Unit)] [Fact] public void VerifyWitness_WithValidSignature_ReturnsSuccess() { // Arrange var witness = CreateTestWitness(); var (privateKey, publicKey) = CreateTestKeyPair(); var signingKey = EnvelopeKey.CreateEd25519Signer(privateKey, publicKey); var signer = new WitnessDsseSigner(); // Sign the witness var signResult = signer.SignWitness(witness, signingKey, TestCancellationToken); Assert.True(signResult.IsSuccess, signResult.Error); // Create public key for verification var verifyKey = EnvelopeKey.CreateEd25519Verifier(publicKey); // Act var verifyResult = signer.VerifyWitness(signResult.Envelope!, verifyKey, TestCancellationToken); // Assert Assert.True(verifyResult.IsSuccess, verifyResult.Error); Assert.NotNull(verifyResult.Witness); Assert.Equal(witness.WitnessId, verifyResult.Witness.WitnessId); Assert.Equal(witness.Vuln.Id, verifyResult.Witness.Vuln.Id); } [Trait("Category", TestCategories.Unit)] [Fact] public void VerifyWitness_WithWrongKey_ReturnsFails() { // Arrange var witness = CreateTestWitness(); var (privateKey, publicKey) = CreateTestKeyPair(); var signingKey = EnvelopeKey.CreateEd25519Signer(privateKey, publicKey); var signer = new WitnessDsseSigner(); // Sign the witness var signResult = signer.SignWitness(witness, signingKey, TestCancellationToken); Assert.True(signResult.IsSuccess, signResult.Error); // Create a different key for verification (different keyId) var generator = new Ed25519KeyPairGenerator(); generator.Init(new Ed25519KeyGenerationParameters(new SecureRandom())); var wrongKeyPair = generator.GenerateKeyPair(); var wrongPublicKey = ((Ed25519PublicKeyParameters)wrongKeyPair.Public).GetEncoded(); var wrongKey = EnvelopeKey.CreateEd25519Verifier(wrongPublicKey); // Act - verify with wrong key (keyId won't match) var verifyResult = signer.VerifyWitness(signResult.Envelope!, wrongKey, TestCancellationToken); // Assert Assert.False(verifyResult.IsSuccess); Assert.Contains("No signature found for key ID", verifyResult.Error); } [Trait("Category", TestCategories.Unit)] [Fact] public void SignWitness_ProducesDeterministicPayload() { // Arrange var witness = CreateTestWitness(); var (privateKey, publicKey) = CreateTestKeyPair(); var key = EnvelopeKey.CreateEd25519Signer(privateKey, publicKey); var signer = new WitnessDsseSigner(); // Act var result1 = signer.SignWitness(witness, key, TestCancellationToken); var result2 = signer.SignWitness(witness, key, TestCancellationToken); // Assert: payloads should be identical (deterministic serialization) Assert.True(result1.IsSuccess); Assert.True(result2.IsSuccess); Assert.Equal(result1.PayloadBytes, result2.PayloadBytes); } [Trait("Category", TestCategories.Unit)] [Fact] public void SignWitness_UsesCanonicalPayloadAndDssePae() { // Arrange var witness = CreateTestWitness(); var (privateKey, publicKey) = CreateTestKeyPair(); var signingKey = EnvelopeKey.CreateEd25519Signer(privateKey, publicKey); var signer = new WitnessDsseSigner(new EnvelopeSignatureService()); var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, WriteIndented = false, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, Encoder = JavaScriptEncoder.Default }; // Act var signResult = signer.SignWitness(witness, signingKey, TestCancellationToken); // Assert Assert.True(signResult.IsSuccess, signResult.Error); Assert.NotNull(signResult.Envelope); Assert.NotNull(signResult.PayloadBytes); var payloadBytes = signResult.PayloadBytes!; var expectedPayload = CanonJson.Canonicalize(witness, options); Assert.Equal(expectedPayload, payloadBytes); var verifyKey = EnvelopeKey.CreateEd25519Verifier(publicKey); var signatureBytes = Convert.FromBase64String(signResult.Envelope!.Signatures[0].Signature); var envelopeSignature = new EnvelopeSignature(signingKey.KeyId, signingKey.AlgorithmId, signatureBytes); var verifyResult = new EnvelopeSignatureService().VerifyDsse( WitnessSchema.DssePayloadType, payloadBytes, envelopeSignature, verifyKey, TestCancellationToken); Assert.True(verifyResult.IsSuccess); } [Trait("Category", TestCategories.Unit)] [Fact] public void VerifyWitness_WithInvalidPayloadType_ReturnsFails() { // Arrange var witness = CreateTestWitness(); var (privateKey, publicKey) = CreateTestKeyPair(); var signingKey = EnvelopeKey.CreateEd25519Signer(privateKey, publicKey); var signer = new WitnessDsseSigner(); var signResult = signer.SignWitness(witness, signingKey, TestCancellationToken); Assert.True(signResult.IsSuccess); // Create envelope with wrong payload type var wrongEnvelope = new DsseEnvelope( payloadType: "application/wrong-type", payload: signResult.Envelope!.Payload, signatures: signResult.Envelope.Signatures); var verifyKey = EnvelopeKey.CreateEd25519Verifier(publicKey); // Act var verifyResult = signer.VerifyWitness(wrongEnvelope, verifyKey, TestCancellationToken); // Assert Assert.False(verifyResult.IsSuccess); Assert.Contains("Invalid payload type", verifyResult.Error); } [Trait("Category", TestCategories.Unit)] [Fact] public void RoundTrip_PreservesAllWitnessFields() { // Arrange var witness = CreateTestWitness(); var (privateKey, publicKey) = CreateTestKeyPair(); var signingKey = EnvelopeKey.CreateEd25519Signer(privateKey, publicKey); var verifyKey = EnvelopeKey.CreateEd25519Verifier(publicKey); var signer = new WitnessDsseSigner(); // Act var signResult = signer.SignWitness(witness, signingKey, TestCancellationToken); var verifyResult = signer.VerifyWitness(signResult.Envelope!, verifyKey, TestCancellationToken); // Assert Assert.True(signResult.IsSuccess); Assert.True(verifyResult.IsSuccess); var roundTripped = verifyResult.Witness!; Assert.Equal(witness.WitnessSchema, roundTripped.WitnessSchema); Assert.Equal(witness.WitnessId, roundTripped.WitnessId); Assert.Equal(witness.Artifact.SbomDigest, roundTripped.Artifact.SbomDigest); Assert.Equal(witness.Artifact.ComponentPurl, roundTripped.Artifact.ComponentPurl); Assert.Equal(witness.Vuln.Id, roundTripped.Vuln.Id); Assert.Equal(witness.Vuln.Source, roundTripped.Vuln.Source); Assert.Equal(witness.Entrypoint.Kind, roundTripped.Entrypoint.Kind); Assert.Equal(witness.Entrypoint.Name, roundTripped.Entrypoint.Name); Assert.Equal(witness.Entrypoint.SymbolId, roundTripped.Entrypoint.SymbolId); Assert.Equal(witness.Sink.Symbol, roundTripped.Sink.Symbol); Assert.Equal(witness.Sink.SymbolId, roundTripped.Sink.SymbolId); Assert.Equal(witness.Sink.SinkType, roundTripped.Sink.SinkType); Assert.Equal(witness.Path.Count, roundTripped.Path.Count); Assert.Equal(witness.Evidence.CallgraphDigest, roundTripped.Evidence.CallgraphDigest); } private static PathWitness CreateTestWitness() { return new PathWitness { WitnessId = "wit:sha256:abc123def456", Artifact = new WitnessArtifact { SbomDigest = "sha256:sbom123456", ComponentPurl = "pkg:nuget/Newtonsoft.Json@12.0.3" }, Vuln = new WitnessVuln { Id = "CVE-2024-12345", Source = "NVD", AffectedRange = "<=12.0.3" }, Entrypoint = new WitnessEntrypoint { Kind = "http", Name = "GET /api/users", SymbolId = "sym:entry:001" }, Path = new List { new PathStep { Symbol = "UserController.GetUsers", SymbolId = "sym:step:001", File = "Controllers/UserController.cs", Line = 42 }, new PathStep { Symbol = "JsonConvert.DeserializeObject", SymbolId = "sym:step:002", File = null, Line = null } }, Sink = new WitnessSink { Symbol = "JsonConvert.DeserializeObject", SymbolId = "sym:sink:001", SinkType = "deserialization" }, Evidence = new WitnessEvidence { CallgraphDigest = "blake3:graph123456", SurfaceDigest = "sha256:surface789", BuildId = "build:xyz123" }, ObservedAt = new DateTimeOffset(2025, 12, 19, 12, 0, 0, TimeSpan.Zero) }; } /// /// Fixed random generator for deterministic key generation in tests. /// private sealed class FixedRandomGenerator : Org.BouncyCastle.Crypto.Prng.IRandomGenerator { private byte _value = 0x42; public void AddSeedMaterial(byte[] seed) { } public void AddSeedMaterial(ReadOnlySpan seed) { } public void AddSeedMaterial(long seed) { } public void NextBytes(byte[] bytes) => NextBytes(bytes, 0, bytes.Length); public void NextBytes(byte[] bytes, int start, int len) { for (int i = start; i < start + len; i++) { bytes[i] = _value++; } } public void NextBytes(Span bytes) { for (int i = 0; i < bytes.Length; i++) { bytes[i] = _value++; } } } }