Add Canonical JSON serialization library with tests and documentation
- Implemented CanonJson class for deterministic JSON serialization and hashing. - Added unit tests for CanonJson functionality, covering various scenarios including key sorting, handling of nested objects, arrays, and special characters. - Created project files for the Canonical JSON library and its tests, including necessary package references. - Added README.md for library usage and API reference. - Introduced RabbitMqIntegrationFactAttribute for conditional RabbitMQ integration tests.
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
using Org.BouncyCastle.Crypto.Generators;
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
using Org.BouncyCastle.Security;
|
||||
using StellaOps.Attestor.Envelope;
|
||||
using StellaOps.Scanner.Reachability.Witnesses;
|
||||
using Xunit;
|
||||
|
||||
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
|
||||
{
|
||||
/// <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);
|
||||
}
|
||||
|
||||
[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);
|
||||
|
||||
// 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!);
|
||||
}
|
||||
|
||||
[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);
|
||||
Assert.True(signResult.IsSuccess, signResult.Error);
|
||||
|
||||
// Create public key for verification
|
||||
var verifyKey = EnvelopeKey.CreateEd25519Verifier(publicKey);
|
||||
|
||||
// Act
|
||||
var verifyResult = signer.VerifyWitness(signResult.Envelope!, verifyKey);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
[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);
|
||||
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);
|
||||
|
||||
// Assert
|
||||
Assert.False(verifyResult.IsSuccess);
|
||||
Assert.Contains("No signature found for key ID", verifyResult.Error);
|
||||
}
|
||||
|
||||
[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);
|
||||
var result2 = signer.SignWitness(witness, key);
|
||||
|
||||
// Assert: payloads should be identical (deterministic serialization)
|
||||
Assert.True(result1.IsSuccess);
|
||||
Assert.True(result2.IsSuccess);
|
||||
Assert.Equal(result1.PayloadBytes, result2.PayloadBytes);
|
||||
}
|
||||
|
||||
[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);
|
||||
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);
|
||||
|
||||
// Assert
|
||||
Assert.False(verifyResult.IsSuccess);
|
||||
Assert.Contains("Invalid payload type", verifyResult.Error);
|
||||
}
|
||||
|
||||
[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);
|
||||
var verifyResult = signer.VerifyWitness(signResult.Envelope!, verifyKey);
|
||||
|
||||
// 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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user