284 lines
9.4 KiB
Markdown
284 lines
9.4 KiB
Markdown
# Build-Time DSSE Attestation Walkthrough
|
||
|
||
> **Status:** Complete — implements the November 2025 advisory "Embed in-toto attestations (DSSE-wrapped) into .NET 10/C# builds." Updated 2025-11-27 with CLI verification commands (`DSSE-CLI-401-021`).
|
||
> **Owners:** Attestor Guild · DevOps Guild · Docs Guild.
|
||
|
||
This guide shows how to emit signed, in-toto compliant DSSE envelopes for every container build step (scan → package → push) using Stella Ops Authority keys. The same primitives power our Signer/Attestor services, but this walkthrough targets developer pipelines (GitHub/GitLab, dotnet builds, container scanners).
|
||
|
||
---
|
||
|
||
## 1. Concepts refresher
|
||
|
||
| Term | Meaning |
|
||
|------|---------|
|
||
| **In-toto Statement** | JSON document describing what happened (predicate) to which artifact (subject). |
|
||
| **DSSE** | Dead Simple Signing Envelope: wraps the statement, base64 payload, and signatures. |
|
||
| **Authority Signer** | Stella Ops client that signs data via file-based keys, HSM/KMS, or keyless Fulcio certs. |
|
||
| **PAE** | Pre-Authentication Encoding: canonical “DSSEv1 <len> <payloadType> <len> <payload>” byte layout that is signed. |
|
||
|
||
Requirements:
|
||
|
||
1. .NET 10 SDK (preview) for C# helper code.
|
||
2. Authority key material (dev: file-based Ed25519; prod: Authority/KMS signer).
|
||
3. Artifact digest (e.g., `pkg:docker/registry/app@sha256:...`) per step.
|
||
|
||
---
|
||
|
||
## 2. Helpers (drop-in library)
|
||
|
||
Create `src/StellaOps.Attestation` with:
|
||
|
||
```csharp
|
||
public sealed record InTotoStatement(
|
||
string _type,
|
||
IReadOnlyList<Subject> subject,
|
||
string predicateType,
|
||
object predicate);
|
||
|
||
public sealed record Subject(string name, IReadOnlyDictionary<string,string> digest);
|
||
|
||
public sealed record DsseEnvelope(
|
||
string payloadType,
|
||
string payload,
|
||
IReadOnlyList<Signature> signatures);
|
||
|
||
public sealed record Signature(string keyid, string sig);
|
||
|
||
public interface IAuthoritySigner
|
||
{
|
||
Task<string> GetKeyIdAsync(CancellationToken ct = default);
|
||
Task<byte[]> SignAsync(ReadOnlyMemory<byte> pae, CancellationToken ct = default);
|
||
}
|
||
```
|
||
|
||
DSSE helper:
|
||
|
||
```csharp
|
||
public static class DsseHelper
|
||
{
|
||
public static async Task<DsseEnvelope> WrapAsync(
|
||
InTotoStatement statement,
|
||
IAuthoritySigner signer,
|
||
string payloadType = "application/vnd.in-toto+json",
|
||
CancellationToken ct = default)
|
||
{
|
||
var payloadBytes = JsonSerializer.SerializeToUtf8Bytes(statement,
|
||
new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull });
|
||
var pae = PreAuthEncode(payloadType, payloadBytes);
|
||
|
||
var signature = await signer.SignAsync(pae, ct).ConfigureAwait(false);
|
||
var keyId = await signer.GetKeyIdAsync(ct).ConfigureAwait(false);
|
||
|
||
return new DsseEnvelope(
|
||
payloadType,
|
||
Convert.ToBase64String(payloadBytes),
|
||
new[] { new Signature(keyId, Convert.ToBase64String(signature)) });
|
||
}
|
||
|
||
private static byte[] PreAuthEncode(string payloadType, byte[] payload)
|
||
{
|
||
static byte[] Cat(params byte[][] parts)
|
||
{
|
||
var len = parts.Sum(p => p.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);
|
||
}
|
||
}
|
||
```
|
||
|
||
Authority signer examples:
|
||
|
||
/dev (file Ed25519):
|
||
```csharp
|
||
public sealed class FileEd25519Signer : IAuthoritySigner, IDisposable
|
||
{
|
||
private readonly Ed25519 _ed;
|
||
private readonly string _keyId;
|
||
|
||
public FileEd25519Signer(byte[] privateKeySeed, string keyId)
|
||
{
|
||
_ed = new Ed25519(privateKeySeed);
|
||
_keyId = keyId;
|
||
}
|
||
|
||
public Task<string> GetKeyIdAsync(CancellationToken ct) => Task.FromResult(_keyId);
|
||
|
||
public Task<byte[]> SignAsync(ReadOnlyMemory<byte> pae, CancellationToken ct)
|
||
=> Task.FromResult(_ed.Sign(pae.Span.ToArray()));
|
||
|
||
public void Dispose() => _ed.Dispose();
|
||
}
|
||
```
|
||
|
||
Prod (Authority KMS):
|
||
|
||
Reuse the existing `StellaOps.Signer.KmsSigner` adapter—wrap it behind `IAuthoritySigner`.
|
||
|
||
---
|
||
|
||
## 3. Emitting attestations per step
|
||
|
||
Subject helper:
|
||
```csharp
|
||
static Subject ImageSubject(string imageDigest) => new(
|
||
name: imageDigest,
|
||
digest: new Dictionary<string,string>{{"sha256", imageDigest.Replace("sha256:", "", StringComparison.Ordinal)}});
|
||
```
|
||
|
||
### 3.1 Scan
|
||
|
||
```csharp
|
||
var scanStmt = new InTotoStatement(
|
||
_type: "https://in-toto.io/Statement/v1",
|
||
subject: new[]{ ImageSubject(imageDigest) },
|
||
predicateType: "https://stella.ops/predicates/scanner-evidence/v1",
|
||
predicate: new {
|
||
scanner = "StellaOps.Scanner 0.9.0",
|
||
findingsSha256 = scanResultsHash,
|
||
startedAt = startedIso,
|
||
finishedAt = finishedIso,
|
||
rulePack = "lattice:default@2025-11-01"
|
||
});
|
||
|
||
var scanEnvelope = await DsseHelper.WrapAsync(scanStmt, signer);
|
||
await File.WriteAllTextAsync("artifacts/attest-scan.dsse.json", JsonSerializer.Serialize(scanEnvelope));
|
||
```
|
||
|
||
### 3.2 Package (SLSA provenance)
|
||
|
||
```csharp
|
||
var pkgStmt = new InTotoStatement(
|
||
"https://in-toto.io/Statement/v1",
|
||
new[]{ ImageSubject(imageDigest) },
|
||
"https://slsa.dev/provenance/v1",
|
||
new {
|
||
builder = new { id = "stella://builder/dockerfile" },
|
||
buildType = "dockerfile/v1",
|
||
invocation = new { configSource = repoUrl, entryPoint = dockerfilePath },
|
||
materials = new[] { new { uri = repoUrl, digest = new { git = gitSha } } }
|
||
});
|
||
|
||
var pkgEnvelope = await DsseHelper.WrapAsync(pkgStmt, signer);
|
||
await File.WriteAllTextAsync("artifacts/attest-package.dsse.json", JsonSerializer.Serialize(pkgEnvelope));
|
||
```
|
||
|
||
### 3.3 Push
|
||
|
||
```csharp
|
||
var pushStmt = new InTotoStatement(
|
||
"https://in-toto.io/Statement/v1",
|
||
new[]{ ImageSubject(imageDigest) },
|
||
"https://stella.ops/predicates/push/v1",
|
||
new { registry = registryUrl, repository = repoName, tags, pushedAt = DateTimeOffset.UtcNow });
|
||
|
||
var pushEnvelope = await DsseHelper.WrapAsync(pushStmt, signer);
|
||
await File.WriteAllTextAsync("artifacts/attest-push.dsse.json", JsonSerializer.Serialize(pushEnvelope));
|
||
```
|
||
|
||
---
|
||
|
||
## 4. CI integration
|
||
|
||
### 4.1 GitLab example
|
||
|
||
```yaml
|
||
.attest-template: &attest
|
||
image: mcr.microsoft.com/dotnet/sdk:10.0-preview
|
||
before_script:
|
||
- dotnet build src/StellaOps.Attestation/StellaOps.Attestation.csproj
|
||
variables:
|
||
AUTHORITY_KEY_FILE: "$CI_PROJECT_DIR/secrets/ed25519.key"
|
||
IMAGE_DIGEST: "$CI_REGISTRY_IMAGE@${CI_COMMIT_SHA}"
|
||
|
||
attest:scan:
|
||
stage: scan
|
||
script:
|
||
- dotnet run --project tools/StellaOps.Attestor.Tool -- step scan --subject "$IMAGE_DIGEST" --out artifacts/attest-scan.dsse.json
|
||
artifacts:
|
||
paths: [artifacts/attest-scan.dsse.json]
|
||
|
||
attest:package:
|
||
stage: package
|
||
script:
|
||
- dotnet run --project tools/StellaOps.Attestor.Tool -- step package --subject "$IMAGE_DIGEST" --out artifacts/attest-package.dsse.json
|
||
|
||
attest:push:
|
||
stage: push
|
||
script:
|
||
- dotnet run --project tools/StellaOps.Attestor.Tool -- step push --subject "$IMAGE_DIGEST" --registry "$CI_REGISTRY" --tags "$CI_COMMIT_REF_NAME"
|
||
```
|
||
|
||
### 4.2 GitHub Actions snippet
|
||
|
||
```yaml
|
||
jobs:
|
||
attest:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
- uses: actions/setup-dotnet@v4
|
||
with: { dotnet-version: '10.0.x' }
|
||
- name: Build attestor helpers
|
||
run: dotnet build src/StellaOps.Attestation/StellaOps.Attestation.csproj
|
||
- name: Emit scan attestation
|
||
run: dotnet run --project tools/StellaOps.Attestor.Tool -- step scan --subject "${{ env.IMAGE_DIGEST }}" --out artifacts/attest-scan.dsse.json
|
||
env:
|
||
AUTHORITY_KEY_REF: ${{ secrets.AUTHORITY_KEY_REF }}
|
||
```
|
||
|
||
---
|
||
|
||
## 5. Verification
|
||
|
||
* `stella attest verify --envelope artifacts/attest-scan.dsse.json` — offline verification using the CLI.
|
||
* Additional verification options:
|
||
* `--policy policy.json` — apply custom verification policy
|
||
* `--root keys/root.pem` — specify trusted root certificate
|
||
* `--transparency-checkpoint checkpoint.json` — verify against Rekor checkpoint
|
||
* Manual validation:
|
||
1. Base64 decode payload → ensure `_type` = `https://in-toto.io/Statement/v1`, `subject[].digest.sha256` matches artifact.
|
||
2. Recompute PAE and verify signature with the Authority public key.
|
||
3. Attach envelope to Rekor (optional) via existing Attestor API.
|
||
|
||
---
|
||
|
||
## 6. Storage conventions
|
||
|
||
Store DSSE files next to build outputs:
|
||
|
||
```
|
||
artifacts/
|
||
attest-scan.dsse.json
|
||
attest-package.dsse.json
|
||
attest-push.dsse.json
|
||
```
|
||
|
||
Include the SHA-256 digest of each envelope in promotion manifests (`docs/release/promotion-attestations.md`) so downstream verifiers can trace chain of custody.
|
||
|
||
---
|
||
|
||
## 7. References
|
||
|
||
- [In-toto Statement v1](https://in-toto.io/spec/v1)
|
||
- [DSSE specification](https://github.com/secure-systems-lab/dsse)
|
||
- `docs/modules/signer/architecture.md`
|
||
- `docs/modules/attestor/architecture.md`
|
||
- `docs/release/promotion-attestations.md`
|
||
|
||
This file was updated as part of `DSSE-LIB-401-020` and `DSSE-CLI-401-021` (completed 2025-11-27). See `docs/modules/cli/guides/attest.md` for CI/CD workflow snippets.
|