SPDX 3.0.1 Build Profile Integration
Sprint: SPRINT_20260107_004_003_BE Status: Partial - unsigned supported-field export only Last Updated: 2026-07-17
Overview
The SPDX 3.0.1 Build profile captures provenance information about how an artifact was built. StellaOps currently provides a bidirectional library mapping for the fields represented by Spdx3Build and a live unsigned export endpoint for that supported subset.
Standalone DSSE signing, relationship, and combined-document helpers also exist in the library. They are not composed into the live Attestor export endpoint. Builder identity and material elements are not represented by the current mapper and are rejected instead of being silently discarded.
Build Profile Structure
Core Build Element
The Spdx3Build element represents build/CI information:
{
"@type": "Build",
"spdxId": "urn:stellaops:build:abc123",
"build_buildType": "https://stellaops.org/build/container-scan/v1",
"build_buildId": "build-12345",
"build_buildStartTime": "2026-01-07T12:00:00Z",
"build_buildEndTime": "2026-01-07T12:05:00Z",
"build_configSourceUri": ["https://github.com/..."],
"build_configSourceDigest": [{"algorithm": "sha256", "hashValue": "..."}],
"build_configSourceEntrypoint": [".github/workflows/build.yml"],
"build_environment": {"CI": "true"},
"build_parameter": {"target": "release"}
}
Property Mapping
| SLSA/in-toto | SPDX 3.0.1 Build |
|---|---|
| buildType | build_buildType |
| invocation.configSource.uri | build_configSourceUri |
| invocation.configSource.digest | build_configSourceDigest |
| invocation.configSource.entryPoint | build_configSourceEntrypoint |
| invocation.environment | build_environment |
| invocation.parameters | build_parameter |
| metadata.buildStartedOn | build_buildStartTime |
| metadata.buildFinishedOn | build_buildEndTime |
| metadata.buildInvocationId | build_buildId |
Not mapped by the current Spdx3Build model:
| Input | Current behavior |
|---|---|
builder.id, builder.version | Rejected; no CreationInfo.createdBy element is created |
materials[] and material digests | Rejected; no material elements are created or round-tripped |
API Usage
Mapping Attestations
Use BuildAttestationMapper to convert the supported field subset between the internal payload and SPDX 3.0.1:
var mapper = new BuildAttestationMapper();
// From in-toto to SPDX 3.0.1
var attestation = new BuildAttestationPayload
{
BuildType = "https://slsa.dev/provenance/v1",
Metadata = new BuildMetadata
{
BuildInvocationId = "run-12345",
BuildStartedOn = DateTimeOffset.UtcNow
}
};
var build = mapper.MapToSpdx3(attestation, "https://stellaops.io/spdx");
// From SPDX 3.0.1 to in-toto
var payload = mapper.MapFromSpdx3(build);
CanMapToSpdx3 returns false, and MapToSpdx3 throws NotSupportedException, when builder or material data would otherwise be lost.
Live export endpoint
POST /api/v1/attestations:export-build currently supports only:
{
"buildType": "https://slsa.dev/provenance/v1",
"buildId": "run-12345",
"format": "Spdx3",
"sign": false
}
The endpoint requires the attest:create scope claim. DSSE/signing, Both, builder, and material requests return 501 feature_not_implemented until those fields have a real live composition.
Signing with DSSE
DsseSpdx3Signer can sign SPDX 3.0.1 documents as a standalone library primitive:
var signer = new DsseSpdx3Signer(serializer, signingProvider, timeProvider);
var options = new DsseSpdx3SigningOptions
{
PrimaryKeyId = "key-123",
PrimaryAlgorithm = "ES256",
// Optional: Add post-quantum hybrid signature
SecondaryKeyId = "pq-key-456",
SecondaryAlgorithm = "ML-DSA-65"
};
// Sign a Build element
var envelope = await signer.SignBuildProfileAsync(build, null, options);
// Or sign a full document
var envelope = await signer.SignAsync(document, options);
The live Attestor host does not currently register this signer or a signing provider for attestations:export-build; the API therefore does not advertise or return a DSSE envelope.
Combined Documents
CombinedDocumentBuilder can merge supported profile elements in library callers:
var document = CombinedDocumentBuilder.Create(timeProvider)
.WithDocumentId("https://stellaops.io/spdx/combined/12345")
.WithName("Combined SBOM and Build Provenance")
.WithSoftwareProfile(sbom)
.WithBuildProfile(build)
.Build();
// Or use the extension method
var combined = sbom.WithBuildProvenance(
attestation,
documentId: "https://stellaops.io/spdx/combined/12345",
spdxIdPrefix: "https://stellaops.io/spdx");
WithBuildAttestation uses the same mapper. It does not add builder or material elements, and unsupported payloads fail closed.
SLSA Boundary
The mapper preserves several fields also used by SLSA provenance, but this feature alone does not establish a SLSA level. In particular, the live endpoint has no builder identity mapping, material mapping, or signing integration.
Build Relationships
The following relationships connect Build elements to other SPDX elements:
| Relationship | Direction | Description |
|---|---|---|
| GENERATES | Build -> Package | Build produces this artifact |
| GENERATED_FROM | Package -> File | Artifact was built from these sources |
| BUILD_TOOL_OF | Tool -> Build | Tool was used in this build |
Example relationship generation:
var relationships = new BuildRelationshipBuilder(build.SpdxId)
.Generates(packageId)
.GeneratedFrom(sourceFileIds)
.UsedBuildTool(toolId)
.Build();
DSSE Envelope Format
The DSSE envelope wraps the entire SPDX 3.0.1 document:
{
"payloadType": "application/spdx+json",
"payload": "<base64url-encoded SPDX 3.0.1 JSON>",
"signatures": [
{
"keyid": "key-123",
"sig": "<base64url-encoded signature>"
}
]
}
PAE (Pre-Authentication Encoding)
Signatures are computed over the PAE:
DSSEv1 <len(payloadType)> <payloadType> <len(payload)> <payload>
This prevents ambiguity attacks and ensures the payload type is included in the signature.
Verification
To verify a signed SPDX 3.0.1 envelope:
var trustedKeys = new List<DsseVerificationKey>
{
new() { KeyId = "key-123", PublicKey = publicKeyBytes }
};
var isValid = await signer.VerifyAsync(envelope, trustedKeys);
if (isValid)
{
var document = signer.ExtractDocument(envelope);
// Process verified document
}
Offline Support
The library primitives support air-gapped use:
- Standalone signing and verification can use offline key material.
- Unsigned mapping requires no network call.
- Live build-export signing remains unavailable rather than falling back to an implicit or remote signer.
See Attestor Air-Gap Guide for details.
