Files
git.stella-ops.org/docs/modules/attestor/dsse-roundtrip-verification.md
2025-12-25 12:16:13 +02:00

8.6 KiB

DSSE Round-Trip Verification

This document describes the DSSE (Dead Simple Signing Envelope) round-trip verification process in StellaOps, including bundling, offline verification, and cosign compatibility.

Overview

DSSE round-trip verification ensures that attestations can be:

  1. Created and signed
  2. Serialized to persistent storage
  3. Deserialized and rebundled
  4. Verified offline without network access
  5. Verified by external tools (cosign)

Round-Trip Process

Standard Flow

┌─────────────┐    ┌──────────────┐    ┌─────────────┐
│  Payload    │───>│    Sign      │───>│   DSSE      │
│  (in-toto)  │    │              │    │  Envelope   │
└─────────────┘    └──────────────┘    └──────┬──────┘
                                              │
                                              ▼
┌─────────────┐    ┌──────────────┐    ┌─────────────┐
│   Verify    │<───│  Deserialize │<───│   Bundle    │
│   (Pass)    │    │              │    │   (JSON)    │
└──────┬──────┘    └──────────────┘    └─────────────┘
       │
       ▼
┌─────────────┐    ┌──────────────┐    ┌─────────────┐
│  Re-bundle  │───>│  Serialize   │───>│  Archive    │
│             │    │              │    │  (.tar.gz)  │
└──────┬──────┘    └──────────────┘    └─────────────┘
       │
       ▼
┌─────────────┐
│  Re-verify  │
│   (Pass)    │
└─────────────┘

Steps

  1. Create Payload: Build an in-toto statement with subject digests and predicate
  2. Sign: Create DSSE envelope with signature(s)
  3. Bundle: Wrap envelope in Sigstore-compatible bundle format
  4. Serialize: Convert to JSON bytes
  5. Deserialize: Parse JSON back to bundle structure
  6. Extract: Get DSSE envelope from bundle
  7. Re-bundle: Create new bundle from extracted envelope
  8. Re-verify: Confirm signature validity

Verification Types

Signature Verification

Verifies the cryptographic signature against the payload:

var result = await signatureService.VerifyAsync(envelope, cancellationToken);
if (!result.IsValid)
{
    throw new VerificationException(result.Error);
}

Payload Integrity

Verifies the payload hash matches the signed content:

var computedHash = SHA256.HashData(envelope.Payload.Span);
var declaredHash = ParseDigest(envelope.PayloadDigest);
if (!computedHash.SequenceEqual(declaredHash))
{
    throw new IntegrityException("Payload hash mismatch");
}

Certificate Chain Validation

For keyless (Fulcio) signatures:

var chain = envelope.Signatures[0].CertificateChain;
var result = await certificateValidator.ValidateAsync(chain, options);
// Checks: expiry, revocation, issuer, extended key usage

Determinism Requirements

Round-trip verification requires deterministic serialization:

Property Requirement
Key Order Alphabetical
Whitespace No trailing whitespace, single space after colons
Encoding UTF-8, no BOM
Numbers No unnecessary trailing zeros
Arrays Stable ordering

Verification

var bytes1 = CanonJson.CanonicalizeVersioned(envelope);
var envelope2 = JsonSerializer.Deserialize<DsseEnvelope>(bytes1);
var bytes2 = CanonJson.CanonicalizeVersioned(envelope2);

// bytes1 and bytes2 must be identical
Assert.Equal(bytes1.ToArray(), bytes2.ToArray());

Multi-Signature Support

DSSE envelopes can contain multiple signatures from different signers:

var envelope = new DsseEnvelope(
    payloadType: "application/vnd.in-toto+json",
    payload: canonicalPayload,
    signatures:
    [
        signerA.Sign(canonicalPayload),  // Builder signature
        signerB.Sign(canonicalPayload),  // Witness signature
        signerC.Sign(canonicalPayload)   // Approver signature
    ]);

Verification checks all signatures:

foreach (var signature in envelope.Signatures)
{
    var result = await VerifySignatureAsync(envelope.Payload, signature);
    if (!result.IsValid)
    {
        return VerificationResult.Failure(
            $"Signature {signature.KeyId} failed: {result.Error}");
    }
}

Cosign Compatibility

StellaOps DSSE envelopes are compatible with Sigstore cosign for verification.

Envelope Format

{
  "payloadType": "application/vnd.in-toto+json",
  "payload": "<base64-encoded-payload>",
  "signatures": [
    {
      "keyid": "SHA256:abc123...",
      "sig": "<base64-encoded-signature>"
    }
  ]
}

Cosign Verification Commands

See Cosign Verification Examples for detailed commands.

Offline Verification

For air-gapped environments, verification can proceed without network access:

Prerequisites

  1. Trusted Root: Pre-downloaded Sigstore TUF root
  2. Cached Certificates: Fulcio certificate chain
  3. Rekor Entry (optional): Cached transparency log entry

Offline Flow

var verifier = new OfflineVerifier(
    trustedRoot: TrustedRoot.LoadFromFile("trusted_root.json"),
    rekorEntries: RekorCache.LoadFromDirectory("rekor-cache/"));

var result = await verifier.VerifyAsync(envelope);

Bundle for Offline Use

# Export attestation bundle with all dependencies
stellaops export attestation-bundle \
  --artifact sha256:abc123... \
  --include-certificate-chain \
  --include-rekor-entry \
  --output bundle.tar.gz

Error Handling

Common Verification Failures

Error Cause Resolution
SignatureInvalid Signature doesn't match payload Re-sign with correct key
CertificateExpired Signing certificate expired Use Rekor entry timestamp
PayloadTampered Payload modified after signing Restore original payload
KeyNotTrusted Key not in trusted set Add key to trust policy
ParseError Malformed envelope JSON Validate envelope format

Example Error Handling

try
{
    var result = await verifier.VerifyAsync(envelope);
    if (!result.IsValid)
    {
        logger.LogWarning("Verification failed: {Reason}", result.FailureReason);
        // Handle policy violation
    }
}
catch (CertificateExpiredException ex)
{
    // Fall back to Rekor timestamp verification
    var result = await verifier.VerifyWithRekorTimestampAsync(
        envelope, ex.Certificate, rekorEntry);
}
catch (JsonException ex)
{
    logger.LogError(ex, "Failed to parse envelope");
    throw new VerificationException("Malformed envelope", ex);
}

Testing Round-Trip Verification

Unit Test Example

[Fact]
public async Task RoundTrip_SignVerifyRebundle_Succeeds()
{
    // Arrange
    var payload = CreateInTotoStatement();
    var signer = CreateTestSigner();

    // Act - Sign
    var envelope = await signer.SignAsync(payload);

    // Act - Serialize and deserialize
    var json = JsonSerializer.Serialize(envelope);
    var restored = JsonSerializer.Deserialize<DsseEnvelope>(json);

    // Act - Verify restored envelope
    var result = await signer.VerifyAsync(restored);

    // Assert
    result.IsValid.Should().BeTrue();
    restored.Payload.ToArray().Should().Equal(envelope.Payload.ToArray());
}

Integration Test Example

[Fact]
public async Task RoundTrip_ArchiveExtractVerify_Succeeds()
{
    // Arrange
    var envelope = await CreateSignedEnvelope();
    var archive = new AttestationArchive();

    // Act - Archive
    var archivePath = Path.GetTempFileName() + ".tar.gz";
    await archive.WriteAsync(envelope, archivePath);

    // Act - Extract and verify
    var extracted = await archive.ReadAsync(archivePath);
    var result = await verifier.VerifyAsync(extracted);

    // Assert
    result.IsValid.Should().BeTrue();
}