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:
- Created and signed
- Serialized to persistent storage
- Deserialized and rebundled
- Verified offline without network access
- 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
- Create Payload: Build an in-toto statement with subject digests and predicate
- Sign: Create DSSE envelope with signature(s)
- Bundle: Wrap envelope in Sigstore-compatible bundle format
- Serialize: Convert to JSON bytes
- Deserialize: Parse JSON back to bundle structure
- Extract: Get DSSE envelope from bundle
- Re-bundle: Create new bundle from extracted envelope
- 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
- Trusted Root: Pre-downloaded Sigstore TUF root
- Cached Certificates: Fulcio certificate chain
- 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();
}