Add property-based tests for SBOM/VEX document ordering and Unicode normalization determinism
- Implement `SbomVexOrderingDeterminismProperties` for testing component list and vulnerability metadata hash consistency. - Create `UnicodeNormalizationDeterminismProperties` to validate NFC normalization and Unicode string handling. - Add project file for `StellaOps.Testing.Determinism.Properties` with necessary dependencies. - Introduce CI/CD template validation tests including YAML syntax checks and documentation content verification. - Create validation script for CI/CD templates ensuring all required files and structures are present.
This commit is contained in:
@@ -231,9 +231,264 @@ cosign verify-attestation \
|
||||
|
||||
See [Cosign Verification Examples](./cosign-verification-examples.md) for more details.
|
||||
|
||||
---
|
||||
|
||||
# Aggregated Attestation Bundle Format
|
||||
|
||||
This section describes the StellaOps Attestation Bundle format for aggregating multiple attestations for long-term verification.
|
||||
|
||||
## Overview
|
||||
|
||||
Aggregated attestation bundles collect multiple attestations from a time period into a single verifiable package. This enables:
|
||||
|
||||
- **Long-term verification** of keyless-signed artifacts after certificate expiry
|
||||
- **Organizational endorsement** via optional org-key signature
|
||||
- **Offline verification** with bundled Rekor inclusion proofs
|
||||
- **Regulatory compliance** with audit-ready evidence packages
|
||||
|
||||
## Bundle Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"metadata": {
|
||||
"bundleId": "sha256:abc123...",
|
||||
"version": "1.0",
|
||||
"createdAt": "2025-12-26T02:00:00Z",
|
||||
"periodStart": "2025-12-01T00:00:00Z",
|
||||
"periodEnd": "2025-12-31T23:59:59Z",
|
||||
"attestationCount": 1542,
|
||||
"tenantId": "tenant-1",
|
||||
"orgKeyFingerprint": "sha256:def456..."
|
||||
},
|
||||
"attestations": [
|
||||
{
|
||||
"entryId": "uuid-1",
|
||||
"rekorUuid": "24296fb2...",
|
||||
"rekorLogIndex": 12345678,
|
||||
"artifactDigest": "sha256:...",
|
||||
"predicateType": "verdict.stella/v1",
|
||||
"signedAt": "2025-12-15T10:30:00Z",
|
||||
"signingMode": "keyless",
|
||||
"signingIdentity": {
|
||||
"issuer": "https://token.actions.githubusercontent.com",
|
||||
"subject": "repo:org/repo:ref:refs/heads/main",
|
||||
"san": "https://github.com/org/repo/.github/workflows/release.yml@refs/heads/main"
|
||||
},
|
||||
"inclusionProof": {
|
||||
"checkpoint": {
|
||||
"origin": "rekor.sigstore.dev - ...",
|
||||
"size": 12000000,
|
||||
"rootHash": "base64...",
|
||||
"timestamp": "2025-12-15T10:30:05Z"
|
||||
},
|
||||
"path": ["base64hash1", "base64hash2", ...]
|
||||
},
|
||||
"envelope": {
|
||||
"payloadType": "application/vnd.in-toto+json",
|
||||
"payload": "base64...",
|
||||
"signatures": [{"sig": "base64...", "keyid": ""}],
|
||||
"certificateChain": ["base64cert1", ...]
|
||||
}
|
||||
}
|
||||
],
|
||||
"merkleTree": {
|
||||
"algorithm": "SHA256",
|
||||
"root": "sha256:abc123...",
|
||||
"leafCount": 1542
|
||||
},
|
||||
"orgSignature": {
|
||||
"keyId": "org-signing-key-2025",
|
||||
"algorithm": "ECDSA_P256",
|
||||
"signature": "base64...",
|
||||
"signedAt": "2025-12-26T02:05:00Z",
|
||||
"certificateChain": ["base64cert1", ...]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
### Metadata
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `bundleId` | string | Content-addressed ID: `sha256:<merkle_root>` |
|
||||
| `version` | string | Bundle schema version (currently "1.0") |
|
||||
| `createdAt` | ISO 8601 | Bundle creation timestamp (UTC) |
|
||||
| `periodStart` | ISO 8601 | Start of attestation collection period |
|
||||
| `periodEnd` | ISO 8601 | End of attestation collection period |
|
||||
| `attestationCount` | int | Number of attestations in bundle |
|
||||
| `tenantId` | string | Optional tenant identifier |
|
||||
| `orgKeyFingerprint` | string | Fingerprint of org signing key (if signed) |
|
||||
|
||||
### Attestations
|
||||
|
||||
Each attestation entry contains:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `entryId` | string | Unique entry identifier |
|
||||
| `rekorUuid` | string | Rekor transparency log UUID |
|
||||
| `rekorLogIndex` | long | Rekor log index |
|
||||
| `artifactDigest` | string | SHA256 digest of attested artifact |
|
||||
| `predicateType` | string | In-toto predicate type |
|
||||
| `signedAt` | ISO 8601 | When attestation was signed |
|
||||
| `signingMode` | string | `keyless`, `kms`, `hsm`, or `fido2` |
|
||||
| `signingIdentity` | object | Signer identity information |
|
||||
| `inclusionProof` | object | Rekor Merkle inclusion proof |
|
||||
| `envelope` | object | DSSE envelope with signatures and certificates |
|
||||
|
||||
### Merkle Tree
|
||||
|
||||
Deterministic Merkle tree over attestation hashes:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `algorithm` | string | Hash algorithm (always "SHA256") |
|
||||
| `root` | string | Merkle root: `sha256:<64-hex>` |
|
||||
| `leafCount` | int | Number of leaves (= attestation count) |
|
||||
|
||||
### Org Signature
|
||||
|
||||
Optional organizational endorsement:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `keyId` | string | Signing key identifier |
|
||||
| `algorithm` | string | `ECDSA_P256`, `Ed25519`, or `RSA_PSS_SHA256` |
|
||||
| `signature` | string | Base64-encoded signature |
|
||||
| `signedAt` | ISO 8601 | Signature timestamp |
|
||||
| `certificateChain` | array | PEM-encoded certificate chain |
|
||||
|
||||
## Determinism
|
||||
|
||||
Bundles are deterministic - same attestations produce same bundle:
|
||||
|
||||
1. **Attestation ordering**: Sorted by `entryId` lexicographically
|
||||
2. **Merkle tree**: Leaves computed as `SHA256(canonicalized_attestation_json)`
|
||||
3. **Bundle ID**: Derived from Merkle root: `sha256:<merkle_root>`
|
||||
4. **JSON serialization**: Canonical ordering (sorted keys, no whitespace)
|
||||
|
||||
## Verification
|
||||
|
||||
### Full Bundle Verification
|
||||
|
||||
```csharp
|
||||
using StellaOps.Attestor.Bundling.Verification;
|
||||
|
||||
var verifier = new AttestationBundleVerifier();
|
||||
var result = await verifier.VerifyAsync(bundle);
|
||||
|
||||
if (result.Valid)
|
||||
{
|
||||
Console.WriteLine($"Merkle root verified: {result.MerkleRootVerified}");
|
||||
Console.WriteLine($"Org signature verified: {result.OrgSignatureVerified}");
|
||||
Console.WriteLine($"Attestations verified: {result.AttestationsVerified}");
|
||||
}
|
||||
```
|
||||
|
||||
### Individual Attestation Verification
|
||||
|
||||
```csharp
|
||||
// Extract single attestation for verification
|
||||
var attestation = bundle.Attestations.First(a => a.ArtifactDigest == targetDigest);
|
||||
|
||||
// Verify inclusion proof against Rekor
|
||||
var proofValid = await RekorVerifier.VerifyInclusionAsync(
|
||||
attestation.RekorLogIndex,
|
||||
attestation.InclusionProof);
|
||||
|
||||
// Verify DSSE envelope signature
|
||||
var sigValid = await DsseVerifier.VerifyAsync(
|
||||
attestation.Envelope,
|
||||
attestation.SigningIdentity);
|
||||
```
|
||||
|
||||
## Storage
|
||||
|
||||
### S3/Object Storage
|
||||
|
||||
```yaml
|
||||
attestor:
|
||||
bundling:
|
||||
storage:
|
||||
backend: s3
|
||||
s3:
|
||||
bucket: stellaops-attestor
|
||||
prefix: bundles/
|
||||
objectLock: governance # WORM protection
|
||||
storageClass: STANDARD
|
||||
```
|
||||
|
||||
### Filesystem
|
||||
|
||||
```yaml
|
||||
attestor:
|
||||
bundling:
|
||||
storage:
|
||||
backend: filesystem
|
||||
filesystem:
|
||||
path: /var/lib/stellaops/attestor/bundles
|
||||
directoryPermissions: "0750"
|
||||
filePermissions: "0640"
|
||||
```
|
||||
|
||||
## Retention
|
||||
|
||||
Bundles follow configurable retention policies:
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| `defaultMonths` | 24 | Standard retention period |
|
||||
| `minimumMonths` | 6 | Cannot be reduced below this |
|
||||
| `maximumMonths` | 120 | Maximum allowed retention |
|
||||
|
||||
### Tenant Overrides
|
||||
|
||||
```yaml
|
||||
attestor:
|
||||
bundling:
|
||||
retention:
|
||||
defaultMonths: 24
|
||||
tenantOverrides:
|
||||
tenant-gov: 84 # 7 years
|
||||
tenant-finance: 120 # 10 years
|
||||
```
|
||||
|
||||
## Export Formats
|
||||
|
||||
### JSON (Default)
|
||||
|
||||
Human-readable, suitable for debugging and audit:
|
||||
|
||||
```bash
|
||||
stella attestor bundle export --format json bundle-sha256-abc.json
|
||||
```
|
||||
|
||||
### CBOR
|
||||
|
||||
Compact binary format (~40% smaller):
|
||||
|
||||
```bash
|
||||
stella attestor bundle export --format cbor bundle-sha256-abc.cbor
|
||||
```
|
||||
|
||||
### Compression
|
||||
|
||||
Both formats support compression:
|
||||
|
||||
```yaml
|
||||
attestor:
|
||||
bundling:
|
||||
export:
|
||||
compression: zstd # none | gzip | zstd
|
||||
compressionLevel: 3
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [Sigstore Bundle Specification](https://github.com/sigstore/cosign/blob/main/specs/BUNDLE_SPEC.md)
|
||||
- [Sigstore Protobuf Specs](https://github.com/sigstore/protobuf-specs)
|
||||
- [DSSE Specification](https://github.com/secure-systems-lab/dsse)
|
||||
- [RFC 6962 - Certificate Transparency](https://www.rfc-editor.org/rfc/rfc6962)
|
||||
- [Bundle Rotation Operations](./operations/bundle-rotation.md)
|
||||
|
||||
Reference in New Issue
Block a user