save progress

This commit is contained in:
StellaOps Bot
2026-01-02 15:52:31 +02:00
parent 2dec7e6a04
commit f46bde5575
174 changed files with 20793 additions and 8307 deletions

View File

@@ -0,0 +1,46 @@
using System;
using System.Text;
using StellaOps.Attestation;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Attestation.Tests;
public sealed class DsseEnvelopeExtensionsTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void FromBase64_RoundTripsPayloadAndSignature()
{
var payloadBytes = Encoding.UTF8.GetBytes("{}");
var payloadBase64 = Convert.ToBase64String(payloadBytes);
var signatureBase64 = Convert.ToBase64String(Convert.FromHexString("deadbeef"));
var envelope = DsseEnvelopeExtensions.FromBase64(
"example/type",
payloadBase64,
new (string? KeyId, string SignatureBase64)[] { (KeyId: "key-1", SignatureBase64: signatureBase64) });
Assert.Equal(payloadBase64, envelope.GetPayloadBase64());
Assert.Single(envelope.Signatures);
Assert.Equal("key-1", envelope.Signatures[0].KeyId);
Assert.Equal(signatureBase64, envelope.Signatures[0].Signature);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void FromBase64_ThrowsOnInvalidSignatureBase64()
{
var payloadBytes = Encoding.UTF8.GetBytes("{}");
var payloadBase64 = Convert.ToBase64String(payloadBytes);
var ex = Assert.Throws<ArgumentException>(() =>
DsseEnvelopeExtensions.FromBase64(
"example/type",
payloadBase64,
new (string? KeyId, string SignatureBase64)[] { (KeyId: "key-1", SignatureBase64: "not-base64") }));
Assert.Contains("base64", ex.Message, StringComparison.OrdinalIgnoreCase);
}
}

View File

@@ -15,24 +15,30 @@ public class DsseHelperTests
{
private sealed class FakeSigner : IAuthoritySigner
{
public byte[]? LastPayload { get; private set; }
public Task<string> GetKeyIdAsync(System.Threading.CancellationToken cancellationToken = default)
=> Task.FromResult("fake-key");
public Task<byte[]> SignAsync(ReadOnlyMemory<byte> paePayload, System.Threading.CancellationToken cancellationToken = default)
=> Task.FromResult(Convert.FromHexString("deadbeef"));
{
LastPayload = paePayload.ToArray();
return Task.FromResult(Convert.FromHexString("deadbeef"));
}
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task WrapAsync_ProducesDsseEnvelope()
{
var signer = new FakeSigner();
var stmt = new InTotoStatement(
Type: "https://in-toto.io/Statement/v1",
Subject: new[] { new Subject("demo", new System.Collections.Generic.Dictionary<string, string> { { "sha256", "abcd" } }) },
PredicateType: "demo/predicate",
Predicate: new { hello = "world" });
var envelope = await DsseHelper.WrapAsync(stmt, new FakeSigner());
var envelope = await DsseHelper.WrapAsync(stmt, signer, TestContext.Current.CancellationToken);
envelope.PayloadType.Should().Be("https://in-toto.io/Statement/v1");
var roundtrip = JsonSerializer.Deserialize<InTotoStatement>(envelope.Payload.Span);
@@ -51,9 +57,27 @@ public class DsseHelperTests
var pae = DsseHelper.PreAuthenticationEncoding(payloadType, payload);
// Verify PAE contains expected components (payload type and payload)
var paeString = Encoding.UTF8.GetString(pae);
paeString.Should().Contain(payloadType);
paeString.Should().Contain("{}");
var expected = Encoding.UTF8.GetBytes($"DSSEv1 {payloadType.Length} {payloadType} {payload.Length} {{}}");
pae.Should().Equal(expected);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task WrapAsync_UsesDefaultPayloadTypeWhenMissing()
{
var signer = new FakeSigner();
var stmt = new InTotoStatement(
Type: "",
Subject: new[] { new Subject("demo", new System.Collections.Generic.Dictionary<string, string> { { "sha256", "abcd" } }) },
PredicateType: "demo/predicate",
Predicate: new { hello = "world" });
var envelope = await DsseHelper.WrapAsync(stmt, signer, TestContext.Current.CancellationToken);
envelope.PayloadType.Should().Be("https://in-toto.io/Statement/v1");
signer.LastPayload.Should().NotBeNull();
var expectedPayload = JsonSerializer.SerializeToUtf8Bytes(stmt, new JsonSerializerOptions(JsonSerializerDefaults.Web) { WriteIndented = false });
var expectedPae = DsseHelper.PreAuthenticationEncoding(envelope.PayloadType, expectedPayload);
signer.LastPayload!.Should().Equal(expectedPae);
}
}

View File

@@ -48,7 +48,27 @@ public static class DsseEnvelopeExtensions
ArgumentNullException.ThrowIfNull(signatures);
var payloadBytes = Convert.FromBase64String(payloadBase64);
var dsseSignatures = signatures.Select(s => new DsseSignature(s.SignatureBase64, s.KeyId));
var dsseSignatures = new List<DsseSignature>();
var index = 0;
foreach (var signature in signatures)
{
if (string.IsNullOrWhiteSpace(signature.SignatureBase64))
{
throw new ArgumentException("Signature must be provided.", nameof(signatures));
}
try
{
Convert.FromBase64String(signature.SignatureBase64);
}
catch (FormatException ex)
{
throw new ArgumentException($"Signature at index {index} must be base64-encoded.", nameof(signatures), ex);
}
dsseSignatures.Add(new DsseSignature(signature.SignatureBase64, signature.KeyId));
index++;
}
return new DsseEnvelope(payloadType, payloadBytes, dsseSignatures);
}

View File

@@ -10,33 +10,42 @@ namespace StellaOps.Attestation;
public static class DsseHelper
{
private const string DefaultPayloadType = "https://in-toto.io/Statement/v1";
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
{
WriteIndented = false
};
public static byte[] PreAuthenticationEncoding(string payloadType, ReadOnlySpan<byte> payload)
{
static byte[] Cat(params byte[][] parts)
{
var len = 0;
foreach (var part in parts)
{
len += part.Length;
}
var buf = new byte[len];
var offset = 0;
foreach (var part in parts)
{
Buffer.BlockCopy(part, 0, buf, offset, part.Length);
offset += part.Length;
}
return buf;
}
var header = Encoding.UTF8.GetBytes("DSSEv1");
var pt = Encoding.UTF8.GetBytes(payloadType);
var lenPt = Encoding.UTF8.GetBytes(pt.Length.ToString(CultureInfo.InvariantCulture));
var lenPayload = Encoding.UTF8.GetBytes(payload.Length.ToString(CultureInfo.InvariantCulture));
var space = Encoding.UTF8.GetBytes(" ");
return Cat(header, space, lenPt, space, pt, space, lenPayload, space, payload.ToArray());
var totalLength = header.Length + space.Length + lenPt.Length + space.Length + pt.Length +
space.Length + lenPayload.Length + space.Length + payload.Length;
var buffer = new byte[totalLength];
var offset = 0;
static void CopyBytes(byte[] source, byte[] destination, ref int index)
{
Buffer.BlockCopy(source, 0, destination, index, source.Length);
index += source.Length;
}
CopyBytes(header, buffer, ref offset);
CopyBytes(space, buffer, ref offset);
CopyBytes(lenPt, buffer, ref offset);
CopyBytes(space, buffer, ref offset);
CopyBytes(pt, buffer, ref offset);
CopyBytes(space, buffer, ref offset);
CopyBytes(lenPayload, buffer, ref offset);
CopyBytes(space, buffer, ref offset);
payload.CopyTo(buffer.AsSpan(offset));
return buffer;
}
public static async Task<DsseEnvelope> WrapAsync(InTotoStatement statement, IAuthoritySigner signer, CancellationToken cancellationToken = default)
@@ -44,13 +53,13 @@ public static class DsseHelper
ArgumentNullException.ThrowIfNull(statement);
ArgumentNullException.ThrowIfNull(signer);
var payloadBytes = JsonSerializer.SerializeToUtf8Bytes(statement, statement.GetType());
var pae = PreAuthenticationEncoding(statement.Type ?? string.Empty, payloadBytes);
var payloadType = string.IsNullOrWhiteSpace(statement.Type) ? DefaultPayloadType : statement.Type;
var payloadBytes = JsonSerializer.SerializeToUtf8Bytes(statement, SerializerOptions);
var pae = PreAuthenticationEncoding(payloadType, payloadBytes);
var signatureBytes = await signer.SignAsync(pae, cancellationToken).ConfigureAwait(false);
var keyId = await signer.GetKeyIdAsync(cancellationToken).ConfigureAwait(false);
var dsseSignature = DsseSignature.FromBytes(signatureBytes, keyId);
var payloadType = statement.Type ?? "https://in-toto.io/Statement/v1";
return new DsseEnvelope(payloadType, payloadBytes, new[] { dsseSignature });
}
}

View File

@@ -3,7 +3,7 @@
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>

View File

@@ -7,4 +7,4 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| --- | --- | --- |
| AUDIT-0043-M | DONE | Maintainability audit for StellaOps.Attestation. |
| AUDIT-0043-T | DONE | Test coverage audit for StellaOps.Attestation. |
| AUDIT-0043-A | TODO | Pending approval for changes. |
| AUDIT-0043-A | DONE | Applied DSSE payloadType alignment and base64 validation with tests. |

View File

@@ -5,6 +5,7 @@
// Description: Fluent builder for constructing Sigstore bundles
// -----------------------------------------------------------------------------
using System.Globalization;
using StellaOps.Attestor.Bundle.Models;
using StellaOps.Attestor.Bundle.Serialization;
@@ -38,11 +39,24 @@ public sealed class SigstoreBundleBuilder
ArgumentException.ThrowIfNullOrWhiteSpace(payload);
ArgumentNullException.ThrowIfNull(signatures);
EnsureBase64(payload, nameof(payload));
var signatureList = signatures.ToList();
if (signatureList.Count == 0)
{
throw new ArgumentException("At least one signature is required.", nameof(signatures));
}
foreach (var signature in signatureList)
{
ArgumentNullException.ThrowIfNull(signature);
EnsureBase64(signature.Sig, nameof(signatures));
}
_dsseEnvelope = new BundleDsseEnvelope
{
PayloadType = payloadType,
Payload = payload,
Signatures = signatures.ToList()
Signatures = signatureList
};
return this;
@@ -83,6 +97,7 @@ public sealed class SigstoreBundleBuilder
public SigstoreBundleBuilder WithCertificateBase64(string base64DerCertificate)
{
ArgumentException.ThrowIfNullOrWhiteSpace(base64DerCertificate);
EnsureBase64(base64DerCertificate, nameof(base64DerCertificate));
_certificate = new CertificateInfo
{
RawBytes = base64DerCertificate
@@ -140,6 +155,13 @@ public sealed class SigstoreBundleBuilder
string version = "0.0.1",
InclusionProof? inclusionProof = null)
{
EnsureNumber(logIndex, nameof(logIndex));
EnsureNumber(integratedTime, nameof(integratedTime));
EnsureBase64(logIdKeyId, nameof(logIdKeyId));
EnsureBase64(canonicalizedBody, nameof(canonicalizedBody));
ArgumentException.ThrowIfNullOrWhiteSpace(kind);
ArgumentException.ThrowIfNullOrWhiteSpace(version);
var entry = new TransparencyLogEntry
{
LogIndex = logIndex,
@@ -260,4 +282,29 @@ public sealed class SigstoreBundleBuilder
var bundle = Build();
return SigstoreBundleSerializer.SerializeToUtf8Bytes(bundle);
}
private static void EnsureBase64(string value, string paramName)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("Value must be provided.", paramName);
}
try
{
Convert.FromBase64String(value);
}
catch (FormatException ex)
{
throw new ArgumentException("Value must be base64-encoded.", paramName, ex);
}
}
private static void EnsureNumber(string value, string paramName)
{
if (!long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
{
throw new ArgumentException("Value must be an integer string.", paramName);
}
}
}

View File

@@ -144,6 +144,24 @@ public static class SigstoreBundleSerializer
throw new SigstoreBundleException("Bundle verificationMaterial is required");
}
if (bundle.VerificationMaterial.Certificate is null &&
bundle.VerificationMaterial.PublicKey is null)
{
throw new SigstoreBundleException("Bundle verificationMaterial must include certificate or publicKey");
}
if (bundle.VerificationMaterial.Certificate is not null &&
string.IsNullOrWhiteSpace(bundle.VerificationMaterial.Certificate.RawBytes))
{
throw new SigstoreBundleException("Bundle certificate rawBytes is required");
}
if (bundle.VerificationMaterial.PublicKey is not null &&
string.IsNullOrWhiteSpace(bundle.VerificationMaterial.PublicKey.RawBytes))
{
throw new SigstoreBundleException("Bundle publicKey rawBytes is required");
}
if (bundle.DsseEnvelope is null)
{
throw new SigstoreBundleException("Bundle dsseEnvelope is required");

View File

@@ -7,4 +7,4 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| --- | --- | --- |
| AUDIT-0045-M | DONE | Maintainability audit for StellaOps.Attestor.Bundle. |
| AUDIT-0045-T | DONE | Test coverage audit for StellaOps.Attestor.Bundle. |
| AUDIT-0045-A | TODO | Pending approval for changes. |
| AUDIT-0045-A | DONE | Applied bundle validation hardening, verifier fixes, and test coverage. |

View File

@@ -8,6 +8,7 @@
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Globalization;
using Microsoft.Extensions.Logging;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Crypto.Signers;
@@ -287,7 +288,22 @@ public sealed class SigstoreBundleVerifier
}
// Construct PAE (Pre-Authentication Encoding) for DSSE
var payloadBytes = Convert.FromBase64String(envelope.Payload);
byte[] payloadBytes;
try
{
payloadBytes = Convert.FromBase64String(envelope.Payload);
}
catch (FormatException ex)
{
errors.Add(new BundleVerificationError
{
Code = BundleVerificationErrorCode.DsseSignatureInvalid,
Message = "DSSE envelope payload is not valid base64",
Exception = ex
});
return new VerificationCheckResult(false, CheckResult.Failed, errors);
}
var paeMessage = ConstructPae(envelope.PayloadType, payloadBytes);
// Verify at least one signature
@@ -304,6 +320,15 @@ public sealed class SigstoreBundleVerifier
break;
}
}
catch (FormatException ex)
{
errors.Add(new BundleVerificationError
{
Code = BundleVerificationErrorCode.DsseSignatureInvalid,
Message = "DSSE signature is not valid base64",
Exception = ex
});
}
catch (Exception ex)
{
_logger?.LogDebug(ex, "Signature verification attempt failed");
@@ -320,7 +345,9 @@ public sealed class SigstoreBundleVerifier
return new VerificationCheckResult(false, CheckResult.Failed, errors);
}
return new VerificationCheckResult(true, CheckResult.Passed, errors);
return errors.Count == 0
? new VerificationCheckResult(true, CheckResult.Passed, errors)
: new VerificationCheckResult(false, CheckResult.Failed, errors);
}
private static byte[] ConstructPae(string payloadType, byte[] payload)
@@ -331,8 +358,8 @@ public sealed class SigstoreBundleVerifier
const byte Space = 0x20;
var typeBytes = Encoding.UTF8.GetBytes(payloadType);
var typeLenBytes = Encoding.UTF8.GetBytes(typeBytes.Length.ToString());
var payloadLenBytes = Encoding.UTF8.GetBytes(payload.Length.ToString());
var typeLenBytes = Encoding.UTF8.GetBytes(typeBytes.Length.ToString(CultureInfo.InvariantCulture));
var payloadLenBytes = Encoding.UTF8.GetBytes(payload.Length.ToString(CultureInfo.InvariantCulture));
var prefixBytes = Encoding.UTF8.GetBytes(DssePrefix);
var totalLength = prefixBytes.Length + 1 + typeLenBytes.Length + 1 +
@@ -426,23 +453,29 @@ public sealed class SigstoreBundleVerifier
await Task.CompletedTask; // Async for future extensibility
var errors = new List<BundleVerificationError>();
var sawProof = false;
foreach (var entry in tlogEntries)
{
if (entry.InclusionProof is null)
{
// Skip entries without inclusion proofs
errors.Add(new BundleVerificationError
{
Code = BundleVerificationErrorCode.InclusionProofInvalid,
Message = $"Missing inclusion proof for log index {entry.LogIndex}"
});
continue;
}
try
{
var valid = VerifyMerkleInclusionProof(entry);
sawProof = true;
var valid = VerifyMerkleInclusionProof(entry, out var errorCode);
if (!valid)
{
errors.Add(new BundleVerificationError
{
Code = BundleVerificationErrorCode.InclusionProofInvalid,
Code = errorCode,
Message = $"Merkle inclusion proof verification failed for log index {entry.LogIndex}"
});
}
@@ -458,6 +491,15 @@ public sealed class SigstoreBundleVerifier
}
}
if (!sawProof && tlogEntries.Count > 0)
{
errors.Add(new BundleVerificationError
{
Code = BundleVerificationErrorCode.InclusionProofInvalid,
Message = "No inclusion proofs present in transparency log entries"
});
}
if (errors.Count > 0)
{
return new VerificationCheckResult(false, CheckResult.Failed, errors);
@@ -466,8 +508,10 @@ public sealed class SigstoreBundleVerifier
return new VerificationCheckResult(true, CheckResult.Passed, errors);
}
private bool VerifyMerkleInclusionProof(TransparencyLogEntry entry)
private bool VerifyMerkleInclusionProof(TransparencyLogEntry entry, out BundleVerificationErrorCode errorCode)
{
errorCode = BundleVerificationErrorCode.InclusionProofInvalid;
if (entry.InclusionProof is null)
{
return false;
@@ -475,9 +519,14 @@ public sealed class SigstoreBundleVerifier
var proof = entry.InclusionProof;
if (!string.Equals(proof.LogIndex, entry.LogIndex, StringComparison.Ordinal))
{
return false;
}
// Parse values
if (!long.TryParse(proof.LogIndex, out var leafIndex) ||
!long.TryParse(proof.TreeSize, out var treeSize))
if (!long.TryParse(proof.LogIndex, NumberStyles.Integer, CultureInfo.InvariantCulture, out var leafIndex) ||
!long.TryParse(proof.TreeSize, NumberStyles.Integer, CultureInfo.InvariantCulture, out var treeSize))
{
return false;
}
@@ -500,7 +549,13 @@ public sealed class SigstoreBundleVerifier
// Verify Merkle path
var computedRoot = ComputeMerkleRoot(leafHash, leafIndex, treeSize, hashes);
return computedRoot.SequenceEqual(expectedRoot);
if (!computedRoot.SequenceEqual(expectedRoot))
{
errorCode = BundleVerificationErrorCode.RootHashMismatch;
return false;
}
return true;
}
private static byte[] ComputeLeafHash(byte[] data)

View File

@@ -333,4 +333,62 @@ public class SigstoreBundleBuilderTests
var decoded = Convert.FromBase64String(bundle.VerificationMaterial.Certificate!.RawBytes);
decoded.Should().BeEquivalentTo(certBytes);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void WithDsseEnvelope_InvalidPayloadBase64_Throws()
{
var builder = new SigstoreBundleBuilder();
var act = () => builder.WithDsseEnvelope(
"application/vnd.in-toto+json",
"not-base64",
new[] { new BundleSignature { Sig = Convert.ToBase64String(new byte[64]) } });
act.Should().Throw<ArgumentException>()
.WithMessage("*base64*");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void WithDsseEnvelope_InvalidSignatureBase64_Throws()
{
var builder = new SigstoreBundleBuilder();
var act = () => builder.WithDsseEnvelope(
"application/vnd.in-toto+json",
Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{}")),
new[] { new BundleSignature { Sig = "not-base64" } });
act.Should().Throw<ArgumentException>()
.WithMessage("*base64*");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void WithRekorEntry_InvalidLogIndex_Throws()
{
var builder = new SigstoreBundleBuilder();
var act = () => builder.WithRekorEntry(
logIndex: "not-a-number",
logIdKeyId: Convert.ToBase64String(new byte[32]),
integratedTime: "1703500000",
canonicalizedBody: Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{}")));
act.Should().Throw<ArgumentException>()
.WithMessage("*integer*");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void WithCertificateBase64_InvalidBase64_Throws()
{
var builder = new SigstoreBundleBuilder();
var act = () => builder.WithCertificateBase64("not-base64");
act.Should().Throw<ArgumentException>()
.WithMessage("*base64*");
}
}

View File

@@ -193,6 +193,18 @@ public class SigstoreBundleSerializerTests
.WithMessage("*dsseEnvelope*");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Deserialize_MissingVerificationKeyMaterial_ThrowsSigstoreBundleException()
{
var json = """{"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json","verificationMaterial":{},"dsseEnvelope":{"payloadType":"test","payload":"e30=","signatures":[{"sig":"AAAA"}]}}""";
var act = () => SigstoreBundleSerializer.Deserialize(json);
act.Should().Throw<SigstoreBundleException>()
.WithMessage("*certificate*publicKey*");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Serialize_NullBundle_ThrowsArgumentNullException()

View File

@@ -1,4 +1,4 @@
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
// SigstoreBundleVerifierTests.cs
// Sprint: SPRINT_8200_0001_0005 - Sigstore Bundle Implementation
// Tasks: BUNDLE-8200-020, BUNDLE-8200-021 - Bundle verification tests
@@ -36,7 +36,7 @@ public class SigstoreBundleVerifierTests
};
// Act
var result = await _verifier.VerifyAsync(bundle);
var result = await _verifier.VerifyAsync(bundle, cancellationToken: TestContext.Current.CancellationToken);
// Assert
result.IsValid.Should().BeFalse();
@@ -61,7 +61,7 @@ public class SigstoreBundleVerifierTests
};
// Act
var result = await _verifier.VerifyAsync(bundle);
var result = await _verifier.VerifyAsync(bundle, cancellationToken: TestContext.Current.CancellationToken);
// Assert
result.IsValid.Should().BeFalse();
@@ -89,7 +89,7 @@ public class SigstoreBundleVerifierTests
};
// Act
var result = await _verifier.VerifyAsync(bundle);
var result = await _verifier.VerifyAsync(bundle, cancellationToken: TestContext.Current.CancellationToken);
// Assert
result.IsValid.Should().BeFalse();
@@ -104,16 +104,23 @@ public class SigstoreBundleVerifierTests
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var certBytes = CreateSelfSignedCertificateBytes(ecdsa);
var bundle = new SigstoreBundleBuilder()
.WithDsseEnvelope(
"application/vnd.in-toto+json",
Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{}")),
Array.Empty<BundleSignature>())
.WithCertificateBase64(Convert.ToBase64String(certBytes))
.Build();
var bundle = new SigstoreBundle
{
MediaType = SigstoreBundleConstants.MediaTypeV03,
VerificationMaterial = new VerificationMaterial
{
Certificate = new CertificateInfo { RawBytes = Convert.ToBase64String(certBytes) }
},
DsseEnvelope = new BundleDsseEnvelope
{
PayloadType = "application/vnd.in-toto+json",
Payload = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{}")),
Signatures = Array.Empty<BundleSignature>()
}
};
// Act
var result = await _verifier.VerifyAsync(bundle);
var result = await _verifier.VerifyAsync(bundle, cancellationToken: TestContext.Current.CancellationToken);
// Assert
result.IsValid.Should().BeFalse();
@@ -137,7 +144,7 @@ public class SigstoreBundleVerifierTests
.Build();
// Act
var result = await _verifier.VerifyAsync(bundle);
var result = await _verifier.VerifyAsync(bundle, cancellationToken: TestContext.Current.CancellationToken);
// Assert
result.IsValid.Should().BeFalse();
@@ -167,7 +174,7 @@ public class SigstoreBundleVerifierTests
.Build();
// Act
var result = await _verifier.VerifyAsync(bundle);
var result = await _verifier.VerifyAsync(bundle, cancellationToken: TestContext.Current.CancellationToken);
// Assert
result.IsValid.Should().BeTrue();
@@ -199,7 +206,7 @@ public class SigstoreBundleVerifierTests
.Build();
// Act
var result = await _verifier.VerifyAsync(bundle);
var result = await _verifier.VerifyAsync(bundle, cancellationToken: TestContext.Current.CancellationToken);
// Assert
result.IsValid.Should().BeFalse();
@@ -233,7 +240,7 @@ public class SigstoreBundleVerifierTests
};
// Act
var result = await _verifier.VerifyAsync(bundle, options);
var result = await _verifier.VerifyAsync(bundle, options, TestContext.Current.CancellationToken);
// Assert
result.Checks.CertificateChain.Should().Be(CheckResult.Failed);
@@ -262,19 +269,109 @@ public class SigstoreBundleVerifierTests
.Build();
// Act
var result = await _verifier.VerifyAsync(bundle);
var result = await _verifier.VerifyAsync(bundle, cancellationToken: TestContext.Current.CancellationToken);
// Assert
result.Checks.InclusionProof.Should().Be(CheckResult.Skipped);
result.Checks.TransparencyLog.Should().Be(CheckResult.Skipped);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Verify_InclusionProofMissing_ReturnsFailed()
{
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var certBytes = CreateSelfSignedCertificateBytes(ecdsa);
var payload = System.Text.Encoding.UTF8.GetBytes("{}");
var payloadType = "application/vnd.in-toto+json";
var paeMessage = ConstructPae(payloadType, payload);
var signature = ecdsa.SignData(paeMessage, HashAlgorithmName.SHA256);
var bundle = new SigstoreBundleBuilder()
.WithDsseEnvelope(
payloadType,
Convert.ToBase64String(payload),
new[] { new BundleSignature { Sig = Convert.ToBase64String(signature) } })
.WithCertificateBase64(Convert.ToBase64String(certBytes))
.WithRekorEntry(
logIndex: "12",
logIdKeyId: Convert.ToBase64String(new byte[32]),
integratedTime: "1710000000",
canonicalizedBody: Convert.ToBase64String(new byte[16]))
.Build();
var result = await _verifier.VerifyAsync(bundle, cancellationToken: TestContext.Current.CancellationToken);
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.Code == BundleVerificationErrorCode.InclusionProofInvalid);
result.Checks.InclusionProof.Should().Be(CheckResult.Failed);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Verify_InvalidPayloadBase64_ReturnsFailed()
{
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var certBytes = CreateSelfSignedCertificateBytes(ecdsa);
var payloadType = "application/vnd.in-toto+json";
var bundle = new SigstoreBundle
{
MediaType = SigstoreBundleConstants.MediaTypeV03,
VerificationMaterial = new VerificationMaterial
{
Certificate = new CertificateInfo { RawBytes = Convert.ToBase64String(certBytes) }
},
DsseEnvelope = new BundleDsseEnvelope
{
PayloadType = payloadType,
Payload = "not-base64",
Signatures = new[] { new BundleSignature { Sig = Convert.ToBase64String(new byte[64]) } }
}
};
var result = await _verifier.VerifyAsync(bundle, cancellationToken: TestContext.Current.CancellationToken);
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.Code == BundleVerificationErrorCode.DsseSignatureInvalid);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Verify_InvalidSignatureBase64_ReturnsFailed()
{
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var certBytes = CreateSelfSignedCertificateBytes(ecdsa);
var payload = System.Text.Encoding.UTF8.GetBytes("{}");
var payloadType = "application/vnd.in-toto+json";
var bundle = new SigstoreBundle
{
MediaType = SigstoreBundleConstants.MediaTypeV03,
VerificationMaterial = new VerificationMaterial
{
Certificate = new CertificateInfo { RawBytes = Convert.ToBase64String(certBytes) }
},
DsseEnvelope = new BundleDsseEnvelope
{
PayloadType = payloadType,
Payload = Convert.ToBase64String(payload),
Signatures = new[] { new BundleSignature { Sig = "not-base64" } }
}
};
var result = await _verifier.VerifyAsync(bundle, cancellationToken: TestContext.Current.CancellationToken);
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.Code == BundleVerificationErrorCode.DsseSignatureInvalid);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Verify_NullBundle_ThrowsArgumentNullException()
{
// Act
var act = async () => await _verifier.VerifyAsync(null!);
var act = async () => await _verifier.VerifyAsync(null!, cancellationToken: TestContext.Current.CancellationToken);
// Assert
await act.Should().ThrowAsync<ArgumentNullException>();
@@ -286,8 +383,8 @@ public class SigstoreBundleVerifierTests
const byte Space = 0x20;
var typeBytes = System.Text.Encoding.UTF8.GetBytes(payloadType);
var typeLenBytes = System.Text.Encoding.UTF8.GetBytes(typeBytes.Length.ToString());
var payloadLenBytes = System.Text.Encoding.UTF8.GetBytes(payload.Length.ToString());
var typeLenBytes = System.Text.Encoding.UTF8.GetBytes(typeBytes.Length.ToString(System.Globalization.CultureInfo.InvariantCulture));
var payloadLenBytes = System.Text.Encoding.UTF8.GetBytes(payload.Length.ToString(System.Globalization.CultureInfo.InvariantCulture));
var prefixBytes = System.Text.Encoding.UTF8.GetBytes(DssePrefix);
var totalLength = prefixBytes.Length + 1 + typeLenBytes.Length + 1 +
@@ -331,3 +428,4 @@ public class SigstoreBundleVerifierTests
return cert.Export(System.Security.Cryptography.X509Certificates.X509ContentType.Cert);
}
}