more audit work

This commit is contained in:
master
2026-01-08 10:21:51 +02:00
parent 43c02081ef
commit 51cf4bc16c
546 changed files with 36721 additions and 4003 deletions

View File

@@ -0,0 +1,19 @@
# Attestor SPDX3 Build Profile Tests Charter
## Purpose & Scope
- Working directory: `src/Attestor/__Libraries/__Tests/StellaOps.Attestor.Spdx3.Tests/`.
- Roles: QA automation, backend engineer.
- Focus: deterministic unit tests for SPDX3 build mapping and validation.
## Required Reading
- `docs/README.md`
- `docs/07_HIGH_LEVEL_ARCHITECTURE.md`
- `docs/modules/attestor/architecture.md`
- `docs/modules/platform/architecture-overview.md`
- `docs/implplan/permament/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`
## Working Agreement
- Use fixed timestamps and IDs in fixtures.
- Avoid Random, Guid.NewGuid, DateTime.UtcNow in tests.
- Cover error paths and deterministic ID generation.
- Update `TASKS.md` and sprint tracker as statuses change.

View File

@@ -0,0 +1,176 @@
// <copyright file="BuildAttestationMapperTests.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using System.Collections.Immutable;
using FluentAssertions;
using StellaOps.Spdx3.Model.Build;
using Xunit;
namespace StellaOps.Attestor.Spdx3.Tests;
/// <summary>
/// Unit tests for <see cref="BuildAttestationMapper"/>.
/// Sprint: SPRINT_20260107_004_003 Task BP-009
/// </summary>
[Trait("Category", "Unit")]
public sealed class BuildAttestationMapperTests
{
private readonly BuildAttestationMapper _mapper = new();
private const string SpdxIdPrefix = "https://stellaops.io/spdx/test";
[Fact]
public void MapToSpdx3_WithFullAttestation_MapsAllFields()
{
// Arrange
var attestation = new BuildAttestationPayload
{
BuildType = "https://slsa.dev/provenance/v1",
Builder = new BuilderInfo { Id = "https://github.com/actions/runner", Version = "2.300.0" },
Invocation = new BuildInvocation
{
ConfigSource = new ConfigSource
{
Uri = "https://github.com/stellaops/app",
Digest = new Dictionary<string, string> { ["sha256"] = "abc123" },
EntryPoint = ".github/workflows/build.yml"
},
Environment = new Dictionary<string, string> { ["CI"] = "true" },
Parameters = new Dictionary<string, string> { ["target"] = "release" }
},
Metadata = new BuildMetadata
{
BuildInvocationId = "run-12345",
BuildStartedOn = new DateTimeOffset(2026, 1, 7, 12, 0, 0, TimeSpan.Zero),
BuildFinishedOn = new DateTimeOffset(2026, 1, 7, 12, 5, 0, TimeSpan.Zero)
}
};
// Act
var build = _mapper.MapToSpdx3(attestation, SpdxIdPrefix);
// Assert
build.Should().NotBeNull();
build.BuildType.Should().Be("https://slsa.dev/provenance/v1");
build.BuildId.Should().Be("run-12345");
build.BuildStartTime.Should().Be(attestation.Metadata.BuildStartedOn);
build.BuildEndTime.Should().Be(attestation.Metadata.BuildFinishedOn);
build.ConfigSourceUri.Should().ContainSingle().Which.Should().Be("https://github.com/stellaops/app");
build.ConfigSourceDigest.Should().ContainSingle().Which.Algorithm.Should().Be("sha256");
build.ConfigSourceEntrypoint.Should().ContainSingle().Which.Should().Be(".github/workflows/build.yml");
build.Environment.Should().ContainKey("CI").WhoseValue.Should().Be("true");
build.Parameter.Should().ContainKey("target").WhoseValue.Should().Be("release");
build.SpdxId.Should().StartWith(SpdxIdPrefix);
}
[Fact]
public void MapToSpdx3_WithMinimalAttestation_MapsRequiredFields()
{
// Arrange
var attestation = new BuildAttestationPayload
{
BuildType = "https://stellaops.org/build/scan/v1"
};
// Act
var build = _mapper.MapToSpdx3(attestation, SpdxIdPrefix);
// Assert
build.Should().NotBeNull();
build.BuildType.Should().Be("https://stellaops.org/build/scan/v1");
build.SpdxId.Should().StartWith(SpdxIdPrefix);
build.ConfigSourceUri.Should().BeEmpty();
build.Environment.Should().BeEmpty();
}
[Fact]
public void MapFromSpdx3_WithFullBuild_MapsToAttestation()
{
// Arrange
var build = new Spdx3Build
{
SpdxId = "https://stellaops.io/spdx/test/build/123",
BuildType = "https://slsa.dev/provenance/v1",
BuildId = "build-123",
BuildStartTime = new DateTimeOffset(2026, 1, 7, 12, 0, 0, TimeSpan.Zero),
BuildEndTime = new DateTimeOffset(2026, 1, 7, 12, 5, 0, TimeSpan.Zero),
ConfigSourceUri = ImmutableArray.Create("https://github.com/stellaops/app"),
ConfigSourceDigest = ImmutableArray.Create(Spdx3Hash.Sha256("abc123")),
ConfigSourceEntrypoint = ImmutableArray.Create("Dockerfile"),
Environment = ImmutableDictionary<string, string>.Empty.Add("CI", "true"),
Parameter = ImmutableDictionary<string, string>.Empty.Add("target", "release")
};
// Act
var attestation = _mapper.MapFromSpdx3(build);
// Assert
attestation.Should().NotBeNull();
attestation.BuildType.Should().Be("https://slsa.dev/provenance/v1");
attestation.Metadata!.BuildInvocationId.Should().Be("build-123");
attestation.Metadata!.BuildStartedOn.Should().Be(build.BuildStartTime);
attestation.Metadata!.BuildFinishedOn.Should().Be(build.BuildEndTime);
attestation.Invocation!.ConfigSource!.Uri.Should().Be("https://github.com/stellaops/app");
attestation.Invocation!.Environment.Should().ContainKey("CI");
}
[Fact]
public void CanMapToSpdx3_WithValidAttestation_ReturnsTrue()
{
// Arrange
var attestation = new BuildAttestationPayload
{
BuildType = "https://slsa.dev/provenance/v1"
};
// Act
var result = _mapper.CanMapToSpdx3(attestation);
// Assert
result.Should().BeTrue();
}
[Fact]
public void CanMapToSpdx3_WithEmptyBuildType_ReturnsFalse()
{
// Arrange
var attestation = new BuildAttestationPayload
{
BuildType = ""
};
// Act
var result = _mapper.CanMapToSpdx3(attestation);
// Assert
result.Should().BeFalse();
}
[Fact]
public void CanMapToSpdx3_WithNull_ReturnsFalse()
{
// Act
var result = _mapper.CanMapToSpdx3(null!);
// Assert
result.Should().BeFalse();
}
[Fact]
public void MapToSpdx3_GeneratesDeterministicSpdxId()
{
// Arrange
var attestation = new BuildAttestationPayload
{
BuildType = "https://slsa.dev/provenance/v1",
Metadata = new BuildMetadata { BuildInvocationId = "fixed-id-123" }
};
// Act
var build1 = _mapper.MapToSpdx3(attestation, SpdxIdPrefix);
var build2 = _mapper.MapToSpdx3(attestation, SpdxIdPrefix);
// Assert
build1.SpdxId.Should().Be(build2.SpdxId);
}
}

View File

@@ -0,0 +1,185 @@
// <copyright file="BuildProfileValidatorTests.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using System.Collections.Immutable;
using FluentAssertions;
using StellaOps.Spdx3.Model.Build;
using Xunit;
namespace StellaOps.Attestor.Spdx3.Tests;
/// <summary>
/// Unit tests for <see cref="BuildProfileValidator"/>.
/// Sprint: SPRINT_20260107_004_003 Task BP-009
/// </summary>
[Trait("Category", "Unit")]
public sealed class BuildProfileValidatorTests
{
[Fact]
public void Validate_WithValidBuild_ReturnsSuccess()
{
// Arrange
var build = new Spdx3Build
{
SpdxId = "https://stellaops.io/spdx/test/build/123",
BuildType = "https://slsa.dev/provenance/v1",
BuildId = "build-123",
BuildStartTime = new DateTimeOffset(2026, 1, 7, 12, 0, 0, TimeSpan.Zero),
BuildEndTime = new DateTimeOffset(2026, 1, 7, 12, 5, 0, TimeSpan.Zero)
};
// Act
var result = BuildProfileValidator.Validate(build);
// Assert
result.IsValid.Should().BeTrue();
result.ErrorsOnly.Should().BeEmpty();
}
[Fact]
public void Validate_WithMissingBuildType_ReturnsError()
{
// Arrange
var build = new Spdx3Build
{
SpdxId = "https://stellaops.io/spdx/test/build/123",
BuildType = "",
BuildId = "build-123"
};
// Act
var result = BuildProfileValidator.Validate(build);
// Assert
result.IsValid.Should().BeFalse();
result.ErrorsOnly.Should().ContainSingle()
.Which.Field.Should().Be("buildType");
}
[Fact]
public void Validate_WithInvalidBuildTypeUri_ReturnsError()
{
// Arrange
var build = new Spdx3Build
{
SpdxId = "https://stellaops.io/spdx/test/build/123",
BuildType = "not-a-uri",
BuildId = "build-123"
};
// Act
var result = BuildProfileValidator.Validate(build);
// Assert
result.IsValid.Should().BeFalse();
result.ErrorsOnly.Should().ContainSingle()
.Which.Message.Should().Contain("valid URI");
}
[Fact]
public void Validate_WithEndTimeBeforeStartTime_ReturnsError()
{
// Arrange
var build = new Spdx3Build
{
SpdxId = "https://stellaops.io/spdx/test/build/123",
BuildType = "https://slsa.dev/provenance/v1",
BuildId = "build-123",
BuildStartTime = new DateTimeOffset(2026, 1, 7, 12, 5, 0, TimeSpan.Zero),
BuildEndTime = new DateTimeOffset(2026, 1, 7, 12, 0, 0, TimeSpan.Zero) // Before start
};
// Act
var result = BuildProfileValidator.Validate(build);
// Assert
result.IsValid.Should().BeFalse();
result.ErrorsOnly.Should().ContainSingle()
.Which.Field.Should().Be("buildEndTime");
}
[Fact]
public void Validate_WithMissingBuildId_ReturnsWarning()
{
// Arrange
var build = new Spdx3Build
{
SpdxId = "https://stellaops.io/spdx/test/build/123",
BuildType = "https://slsa.dev/provenance/v1"
};
// Act
var result = BuildProfileValidator.Validate(build);
// Assert
result.IsValid.Should().BeTrue(); // Warnings don't fail validation
result.WarningsOnly.Should().ContainSingle()
.Which.Field.Should().Be("buildId");
}
[Fact]
public void Validate_WithDigestWithoutUri_ReturnsWarning()
{
// Arrange
var build = new Spdx3Build
{
SpdxId = "https://stellaops.io/spdx/test/build/123",
BuildType = "https://slsa.dev/provenance/v1",
BuildId = "build-123",
ConfigSourceDigest = ImmutableArray.Create(Spdx3Hash.Sha256("abc123"))
// Note: ConfigSourceUri is empty
};
// Act
var result = BuildProfileValidator.Validate(build);
// Assert
result.IsValid.Should().BeTrue();
result.WarningsOnly.Should().Contain(w => w.Field == "configSourceDigest");
}
[Fact]
public void Validate_WithUnknownHashAlgorithm_ReturnsWarning()
{
// Arrange
var build = new Spdx3Build
{
SpdxId = "https://stellaops.io/spdx/test/build/123",
BuildType = "https://slsa.dev/provenance/v1",
BuildId = "build-123",
ConfigSourceUri = ImmutableArray.Create("https://github.com/test/repo"),
ConfigSourceDigest = ImmutableArray.Create(new Spdx3Hash
{
Algorithm = "unknown-algo",
HashValue = "abc123"
})
};
// Act
var result = BuildProfileValidator.Validate(build);
// Assert
result.IsValid.Should().BeTrue();
result.WarningsOnly.Should().Contain(w => w.Field == "configSourceDigest.algorithm");
}
[Fact]
public void Validate_WithMissingSpdxId_ReturnsError()
{
// Arrange
var build = new Spdx3Build
{
SpdxId = "",
BuildType = "https://slsa.dev/provenance/v1",
BuildId = "build-123"
};
// Act
var result = BuildProfileValidator.Validate(build);
// Assert
result.IsValid.Should().BeFalse();
result.ErrorsOnly.Should().Contain(e => e.Field == "spdxId");
}
}

View File

@@ -0,0 +1,280 @@
// <copyright file="CombinedDocumentBuilderTests.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using System.Collections.Immutable;
using FluentAssertions;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Spdx3.Model;
using StellaOps.Spdx3.Model.Build;
using StellaOps.Spdx3.Model.Software;
using Xunit;
namespace StellaOps.Attestor.Spdx3.Tests;
/// <summary>
/// Unit tests for <see cref="CombinedDocumentBuilder"/>.
/// Sprint: SPRINT_20260107_004_003 Task BP-008
/// </summary>
[Trait("Category", "Unit")]
public sealed class CombinedDocumentBuilderTests
{
private readonly FakeTimeProvider _timeProvider;
private static readonly DateTimeOffset FixedTimestamp =
new(2026, 1, 8, 12, 0, 0, TimeSpan.Zero);
public CombinedDocumentBuilderTests()
{
_timeProvider = new FakeTimeProvider(FixedTimestamp);
}
[Fact]
public void Build_WithSoftwareAndBuildProfiles_CreatesCombinedDocument()
{
// Arrange
var sbom = CreateTestSbom();
var build = CreateTestBuild();
// Act
var document = CombinedDocumentBuilder.Create(_timeProvider)
.WithDocumentId("https://stellaops.io/spdx/combined/12345")
.WithName("Combined SBOM and Build")
.WithSoftwareProfile(sbom)
.WithBuildProfile(build)
.Build();
// Assert
document.Should().NotBeNull();
document.Profiles.Should().Contain(Spdx3ProfileIdentifier.Core);
document.Profiles.Should().Contain(Spdx3ProfileIdentifier.Software);
document.Profiles.Should().Contain(Spdx3ProfileIdentifier.Build);
}
[Fact]
public void Build_WithBuildProfile_CreatesGeneratesRelationship()
{
// Arrange
var sbom = CreateTestSbom();
var build = CreateTestBuild();
// Act
var document = CombinedDocumentBuilder.Create(_timeProvider)
.WithDocumentId("https://stellaops.io/spdx/combined/12345")
.WithSoftwareProfile(sbom)
.WithBuildProfile(build)
.Build();
// Assert
var relationships = document.Relationships.ToList();
relationships.Should().Contain(r =>
r.RelationshipType == Spdx3RelationshipType.Generates &&
r.From == build.SpdxId);
}
[Fact]
public void Build_WithBuildAttestation_MapsBuildFromAttestation()
{
// Arrange
var sbom = CreateTestSbom();
var attestation = new BuildAttestationPayload
{
BuildType = "https://slsa.dev/provenance/v1",
Metadata = new BuildMetadata
{
BuildInvocationId = "run-12345",
BuildStartedOn = FixedTimestamp
}
};
// Act
var document = CombinedDocumentBuilder.Create(_timeProvider)
.WithDocumentId("https://stellaops.io/spdx/combined/12345")
.WithSoftwareProfile(sbom)
.WithBuildAttestation(attestation, "https://stellaops.io/spdx")
.Build();
// Assert
document.Elements.Should().Contain(e => e is Spdx3Build);
var buildElement = document.Elements.OfType<Spdx3Build>().First();
buildElement.BuildType.Should().Be("https://slsa.dev/provenance/v1");
buildElement.BuildId.Should().Be("run-12345");
}
[Fact]
public void Build_WithoutDocumentId_ThrowsInvalidOperationException()
{
// Arrange
var sbom = CreateTestSbom();
// Act
var act = () => CombinedDocumentBuilder.Create(_timeProvider)
.WithSoftwareProfile(sbom)
.Build();
// Assert
act.Should().Throw<InvalidOperationException>()
.WithMessage("*Document SPDX ID is required*");
}
[Fact]
public void Build_CreatesDefaultCreationInfo()
{
// Arrange
var sbom = CreateTestSbom();
// Act
var document = CombinedDocumentBuilder.Create(_timeProvider)
.WithDocumentId("https://stellaops.io/spdx/doc/12345")
.WithSoftwareProfile(sbom)
.Build();
// Assert
document.CreationInfos.Should().HaveCount(1);
var creationInfo = document.CreationInfos.First();
creationInfo.SpecVersion.Should().Be(Spdx3CreationInfo.Spdx301Version);
creationInfo.Created.Should().Be(FixedTimestamp);
}
[Fact]
public void Build_WithCustomCreationInfo_UsesProvidedInfo()
{
// Arrange
var sbom = CreateTestSbom();
var customCreationInfo = new Spdx3CreationInfo
{
Id = "custom-creation-info",
SpecVersion = Spdx3CreationInfo.Spdx301Version,
Created = FixedTimestamp.AddHours(-1),
CreatedBy = ImmutableArray.Create("custom-author"),
Profile = ImmutableArray.Create(Spdx3ProfileIdentifier.Core)
};
// Act
var document = CombinedDocumentBuilder.Create(_timeProvider)
.WithDocumentId("https://stellaops.io/spdx/doc/12345")
.WithSoftwareProfile(sbom)
.WithCreationInfo(customCreationInfo)
.Build();
// Assert
document.CreationInfos.Should().Contain(customCreationInfo);
}
[Fact]
public void WithBuildProvenance_ExtensionMethod_CreatesCombinedDocument()
{
// Arrange
var sbom = CreateTestSbom();
var attestation = new BuildAttestationPayload
{
BuildType = "https://stellaops.org/build/scan/v1"
};
// Act
var combined = sbom.WithBuildProvenance(
attestation,
documentId: "https://stellaops.io/spdx/combined/ext-12345",
spdxIdPrefix: "https://stellaops.io/spdx",
timeProvider: _timeProvider);
// Assert
combined.Should().NotBeNull();
combined.Profiles.Should().Contain(Spdx3ProfileIdentifier.Build);
combined.Elements.Should().Contain(e => e is Spdx3Build);
}
[Fact]
public void Build_PreservesAllSbomElements()
{
// Arrange
var sbom = CreateTestSbomWithMultiplePackages();
// Act
var document = CombinedDocumentBuilder.Create(_timeProvider)
.WithDocumentId("https://stellaops.io/spdx/doc/12345")
.WithSoftwareProfile(sbom)
.Build();
// Assert
var packages = document.Packages.ToList();
packages.Should().HaveCount(3);
}
private static Spdx3Document CreateTestSbom()
{
var creationInfo = new Spdx3CreationInfo
{
SpecVersion = Spdx3CreationInfo.Spdx301Version,
Created = FixedTimestamp.AddDays(-1),
CreatedBy = ImmutableArray<string>.Empty,
Profile = ImmutableArray.Create(Spdx3ProfileIdentifier.Core, Spdx3ProfileIdentifier.Software)
};
var rootPackage = new Spdx3Package
{
SpdxId = "https://stellaops.io/spdx/pkg/root",
Type = "software_Package",
Name = "test-root-package",
PackageVersion = "1.0.0"
};
return new Spdx3Document(
elements: new Spdx3Element[] { rootPackage },
creationInfos: new[] { creationInfo },
profiles: new[] { Spdx3ProfileIdentifier.Core, Spdx3ProfileIdentifier.Software });
}
private static Spdx3Document CreateTestSbomWithMultiplePackages()
{
var creationInfo = new Spdx3CreationInfo
{
SpecVersion = Spdx3CreationInfo.Spdx301Version,
Created = FixedTimestamp.AddDays(-1),
CreatedBy = ImmutableArray<string>.Empty,
Profile = ImmutableArray.Create(Spdx3ProfileIdentifier.Core, Spdx3ProfileIdentifier.Software)
};
var packages = new Spdx3Package[]
{
new()
{
SpdxId = "https://stellaops.io/spdx/pkg/root",
Type = "software_Package",
Name = "root-package",
PackageVersion = "1.0.0"
},
new()
{
SpdxId = "https://stellaops.io/spdx/pkg/dep1",
Type = "software_Package",
Name = "dependency-1",
PackageVersion = "2.0.0"
},
new()
{
SpdxId = "https://stellaops.io/spdx/pkg/dep2",
Type = "software_Package",
Name = "dependency-2",
PackageVersion = "3.0.0"
}
};
return new Spdx3Document(
elements: packages,
creationInfos: new[] { creationInfo },
profiles: new[] { Spdx3ProfileIdentifier.Core, Spdx3ProfileIdentifier.Software });
}
private static Spdx3Build CreateTestBuild()
{
return new Spdx3Build
{
SpdxId = "https://stellaops.io/spdx/build/12345",
Type = Spdx3Build.TypeName,
BuildType = "https://slsa.dev/provenance/v1",
BuildId = "build-12345",
BuildStartTime = FixedTimestamp.AddMinutes(-5),
BuildEndTime = FixedTimestamp
};
}
}

View File

@@ -0,0 +1,307 @@
// <copyright file="DsseSpdx3SignerTests.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using System.Collections.Immutable;
using System.Text;
using FluentAssertions;
using Microsoft.Extensions.Time.Testing;
using Moq;
using StellaOps.Spdx3.Model;
using StellaOps.Spdx3.Model.Build;
using Xunit;
namespace StellaOps.Attestor.Spdx3.Tests;
/// <summary>
/// Unit tests for <see cref="DsseSpdx3Signer"/>.
/// Sprint: SPRINT_20260107_004_003 Task BP-005
/// </summary>
[Trait("Category", "Unit")]
public sealed class DsseSpdx3SignerTests
{
private readonly FakeTimeProvider _timeProvider;
private readonly Mock<ISpdx3Serializer> _serializerMock;
private readonly Mock<IDsseSigningProvider> _signingProviderMock;
private readonly DsseSpdx3Signer _signer;
private static readonly DateTimeOffset FixedTimestamp =
new(2026, 1, 7, 12, 0, 0, TimeSpan.Zero);
public DsseSpdx3SignerTests()
{
_timeProvider = new FakeTimeProvider(FixedTimestamp);
_serializerMock = new Mock<ISpdx3Serializer>();
_signingProviderMock = new Mock<IDsseSigningProvider>();
_signer = new DsseSpdx3Signer(
_serializerMock.Object,
_signingProviderMock.Object,
_timeProvider);
}
[Fact]
public async Task SignAsync_WithValidDocument_ReturnsEnvelope()
{
// Arrange
var document = CreateTestDocument();
var options = new DsseSpdx3SigningOptions { PrimaryKeyId = "key-123" };
var payloadBytes = Encoding.UTF8.GetBytes("{\"test\":\"document\"}");
_serializerMock
.Setup(s => s.SerializeToBytes(document))
.Returns(payloadBytes);
_signingProviderMock
.Setup(s => s.SignAsync(
It.IsAny<byte[]>(),
"key-123",
null,
It.IsAny<CancellationToken>()))
.ReturnsAsync(new DsseSignatureResult
{
KeyId = "key-123",
SignatureBytes = new byte[] { 0x01, 0x02, 0x03 }
});
// Act
var envelope = await _signer.SignAsync(document, options);
// Assert
envelope.Should().NotBeNull();
envelope.PayloadType.Should().Be(DsseSpdx3Signer.Spdx3PayloadType);
envelope.Payload.Should().NotBeNullOrEmpty();
envelope.Signatures.Should().HaveCount(1);
envelope.Signatures[0].KeyId.Should().Be("key-123");
envelope.SignedAt.Should().Be(FixedTimestamp);
}
[Fact]
public async Task SignAsync_WithSecondaryKey_ReturnsTwoSignatures()
{
// Arrange
var document = CreateTestDocument();
var options = new DsseSpdx3SigningOptions
{
PrimaryKeyId = "key-123",
PrimaryAlgorithm = "ES256",
SecondaryKeyId = "pq-key-456",
SecondaryAlgorithm = "ML-DSA-65"
};
var payloadBytes = Encoding.UTF8.GetBytes("{\"test\":\"document\"}");
_serializerMock
.Setup(s => s.SerializeToBytes(document))
.Returns(payloadBytes);
_signingProviderMock
.Setup(s => s.SignAsync(
It.IsAny<byte[]>(),
"key-123",
"ES256",
It.IsAny<CancellationToken>()))
.ReturnsAsync(new DsseSignatureResult
{
KeyId = "key-123",
SignatureBytes = new byte[] { 0x01, 0x02, 0x03 },
Algorithm = "ES256"
});
_signingProviderMock
.Setup(s => s.SignAsync(
It.IsAny<byte[]>(),
"pq-key-456",
"ML-DSA-65",
It.IsAny<CancellationToken>()))
.ReturnsAsync(new DsseSignatureResult
{
KeyId = "pq-key-456",
SignatureBytes = new byte[] { 0x04, 0x05, 0x06 },
Algorithm = "ML-DSA-65"
});
// Act
var envelope = await _signer.SignAsync(document, options);
// Assert
envelope.Signatures.Should().HaveCount(2);
envelope.Signatures[0].KeyId.Should().Be("key-123");
envelope.Signatures[1].KeyId.Should().Be("pq-key-456");
}
[Fact]
public async Task SignBuildProfileAsync_CreatesBuildDocument()
{
// Arrange
var build = new Spdx3Build
{
SpdxId = "https://stellaops.io/spdx/build/12345",
Type = Spdx3Build.TypeName,
BuildType = "https://slsa.dev/provenance/v1",
BuildId = "build-12345"
};
var options = new DsseSpdx3SigningOptions { PrimaryKeyId = "key-123" };
_serializerMock
.Setup(s => s.SerializeToBytes(It.IsAny<Spdx3Document>()))
.Returns(Encoding.UTF8.GetBytes("{\"build\":\"test\"}"));
_signingProviderMock
.Setup(s => s.SignAsync(
It.IsAny<byte[]>(),
It.IsAny<string>(),
It.IsAny<string?>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new DsseSignatureResult
{
KeyId = "key-123",
SignatureBytes = new byte[] { 0x01, 0x02, 0x03 }
});
// Act
var envelope = await _signer.SignBuildProfileAsync(build, null, options);
// Assert
envelope.Should().NotBeNull();
envelope.PayloadType.Should().Be(DsseSpdx3Signer.Spdx3PayloadType);
_serializerMock.Verify(
s => s.SerializeToBytes(It.Is<Spdx3Document>(d =>
d.Elements.Any(e => e is Spdx3Build))),
Times.Once);
}
[Fact]
public async Task VerifyAsync_WithValidSignature_ReturnsTrue()
{
// Arrange
var envelope = new DsseSpdx3Envelope
{
PayloadType = DsseSpdx3Signer.Spdx3PayloadType,
Payload = "eyJ0ZXN0IjoiZG9jdW1lbnQifQ", // base64url of {"test":"document"}
Signatures = ImmutableArray.Create(new DsseSpdx3Signature
{
KeyId = "key-123",
Sig = "AQID" // base64url of [0x01, 0x02, 0x03]
})
};
var trustedKeys = new List<DsseVerificationKey>
{
new() { KeyId = "key-123", PublicKey = new byte[] { 0x10, 0x20 } }
};
_signingProviderMock
.Setup(s => s.VerifyAsync(
It.IsAny<byte[]>(),
It.IsAny<byte[]>(),
It.Is<DsseVerificationKey>(k => k.KeyId == "key-123"),
It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
// Act
var result = await _signer.VerifyAsync(envelope, trustedKeys);
// Assert
result.Should().BeTrue();
}
[Fact]
public async Task VerifyAsync_WithUntrustedKey_ReturnsFalse()
{
// Arrange
var envelope = new DsseSpdx3Envelope
{
PayloadType = DsseSpdx3Signer.Spdx3PayloadType,
Payload = "eyJ0ZXN0IjoiZG9jdW1lbnQifQ",
Signatures = ImmutableArray.Create(new DsseSpdx3Signature
{
KeyId = "untrusted-key",
Sig = "AQID"
})
};
var trustedKeys = new List<DsseVerificationKey>
{
new() { KeyId = "key-123", PublicKey = new byte[] { 0x10, 0x20 } }
};
// Act
var result = await _signer.VerifyAsync(envelope, trustedKeys);
// Assert
result.Should().BeFalse();
}
[Fact]
public void ExtractDocument_WithValidEnvelope_ReturnsDocument()
{
// Arrange
var originalDocument = CreateTestDocument();
var payloadBytes = Encoding.UTF8.GetBytes("{\"test\":\"document\"}");
var payload = Convert.ToBase64String(payloadBytes)
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
var envelope = new DsseSpdx3Envelope
{
PayloadType = DsseSpdx3Signer.Spdx3PayloadType,
Payload = payload,
Signatures = ImmutableArray<DsseSpdx3Signature>.Empty
};
_serializerMock
.Setup(s => s.Deserialize(It.IsAny<byte[]>()))
.Returns(originalDocument);
// Act
var extracted = _signer.ExtractDocument(envelope);
// Assert
extracted.Should().NotBeNull();
extracted.Should().Be(originalDocument);
}
[Fact]
public void ExtractDocument_WithWrongPayloadType_ReturnsNull()
{
// Arrange
var envelope = new DsseSpdx3Envelope
{
PayloadType = "application/vnd.in-toto+json",
Payload = "eyJ0ZXN0IjoiZG9jdW1lbnQifQ",
Signatures = ImmutableArray<DsseSpdx3Signature>.Empty
};
// Act
var extracted = _signer.ExtractDocument(envelope);
// Assert
extracted.Should().BeNull();
}
[Fact]
public void PayloadType_IsCorrectSpdxMediaType()
{
// Assert
DsseSpdx3Signer.Spdx3PayloadType.Should().Be("application/spdx+json");
}
private static Spdx3Document CreateTestDocument()
{
var creationInfo = new Spdx3CreationInfo
{
SpecVersion = Spdx3CreationInfo.Spdx301Version,
Created = FixedTimestamp,
CreatedBy = ImmutableArray<string>.Empty,
Profile = ImmutableArray.Create(Spdx3ProfileIdentifier.Core, Spdx3ProfileIdentifier.Build)
};
return new Spdx3Document(
elements: Array.Empty<Spdx3Element>(),
creationInfos: new[] { creationInfo },
profiles: new[] { Spdx3ProfileIdentifier.Core, Spdx3ProfileIdentifier.Build });
}
}

View File

@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" Version="9.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.9.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\StellaOps.Attestor.Spdx3\StellaOps.Attestor.Spdx3.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,10 @@
# Attestor SPDX3 Build Profile Tests Task Board
This board mirrors active sprint tasks for this module.
Source of truth: `docs/implplan/permament/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0849-M | DONE | Revalidated 2026-01-08. |
| AUDIT-0849-T | DONE | Revalidated 2026-01-08. |
| AUDIT-0849-A | DONE | Waived (test project; revalidated 2026-01-08). |