Files
git.stella-ops.org/src/Scanner/__Tests/StellaOps.Scanner.Reachability.Tests/WitnessDsseSignerTests.cs

334 lines
13 KiB
C#

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;
/// <summary>
/// Tests for <see cref="WitnessDsseSigner"/>.
/// Sprint: SPRINT_3700_0001_0001 (WIT-007D)
/// Golden fixture tests for DSSE sign/verify.
/// </summary>
public class WitnessDsseSignerTests
{
private static CancellationToken TestCancellationToken => TestContext.Current.CancellationToken;
/// <summary>
/// Creates a deterministic Ed25519 key pair for testing.
/// </summary>
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<PathStep>
{
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<T>",
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)
};
}
/// <summary>
/// Fixed random generator for deterministic key generation in tests.
/// </summary>
private sealed class FixedRandomGenerator : Org.BouncyCastle.Crypto.Prng.IRandomGenerator
{
private byte _value = 0x42;
public void AddSeedMaterial(byte[] seed) { }
public void AddSeedMaterial(ReadOnlySpan<byte> 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<byte> bytes)
{
for (int i = 0; i < bytes.Length; i++)
{
bytes[i] = _value++;
}
}
}
}