UI work to fill SBOM sourcing management gap. UI planning remaining functionality exposure. Work on CI/Tests stabilization
Introduces CGS determinism test runs to CI workflows for Windows, macOS, Linux, Alpine, and Debian, fulfilling CGS-008 cross-platform requirements. Updates local-ci scripts to support new smoke steps, test timeouts, progress intervals, and project slicing for improved test isolation and diagnostics.
This commit is contained in:
220
docs/adr/0042-cgs-merkle-tree-implementation.md
Normal file
220
docs/adr/0042-cgs-merkle-tree-implementation.md
Normal file
@@ -0,0 +1,220 @@
|
||||
# ADR 0042: CGS Merkle Tree Implementation
|
||||
|
||||
## Status
|
||||
ACCEPTED (2025-12-29)
|
||||
|
||||
## Context
|
||||
|
||||
The CGS (Canonical Graph Signature) system requires deterministic hash computation for verdicts. We need to decide whether to:
|
||||
1. Reuse existing `StellaOps.Attestor.ProofChain` Merkle tree builder
|
||||
2. Build a custom Merkle tree implementation in `VerdictBuilderService`
|
||||
|
||||
### Requirements
|
||||
|
||||
- **Determinism**: Same evidence must always produce identical CGS hash
|
||||
- **Order Independence**: VEX document ordering should not affect hash (sorted internally)
|
||||
- **Cross-Platform**: Identical hash on Windows, macOS, Linux (glibc), Linux (musl), BSD
|
||||
- **Leaf Composition**: Specific ordering of evidence components (SBOM, VEX sorted, reachability, policy lock)
|
||||
|
||||
### Existing ProofChain Merkle Builder
|
||||
|
||||
Located at: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/`
|
||||
|
||||
**Pros:**
|
||||
- Already implements Merkle tree construction
|
||||
- Tested and proven in production
|
||||
- Handles parent/child attestation chains
|
||||
|
||||
**Cons:**
|
||||
- Designed for attestation chains, not evidence hashing
|
||||
- Includes attestation-specific metadata in hash
|
||||
- Doesn't support custom leaf ordering required for CGS
|
||||
- Would require modifications that might break existing attestation behavior
|
||||
|
||||
## Decision
|
||||
|
||||
**Build custom Merkle tree implementation in `VerdictBuilderService`.**
|
||||
|
||||
### Rationale
|
||||
|
||||
1. **Separation of Concerns**: CGS hash computation has different requirements than attestation chain verification
|
||||
|
||||
2. **Full Control Over Determinism**: Custom implementation allows:
|
||||
- Explicit leaf ordering: SBOM → VEX (sorted) → Reachability → PolicyLock
|
||||
- VEX document sorting by content hash (not insertion order)
|
||||
- Culture-invariant string comparison (`StringComparer.Ordinal`)
|
||||
|
||||
3. **Simplicity**: ~50 lines of code vs modifying 500+ lines in ProofChain
|
||||
|
||||
4. **No Breaking Changes**: Doesn't affect existing attestation infrastructure
|
||||
|
||||
### Implementation
|
||||
|
||||
```csharp
|
||||
// VerdictBuilderService.cs
|
||||
private static string ComputeCgsHash(EvidencePack evidence, PolicyLock policyLock)
|
||||
{
|
||||
// Build Merkle tree from evidence components (sorted for determinism)
|
||||
var leaves = new List<string>
|
||||
{
|
||||
ComputeHash(evidence.SbomCanonJson),
|
||||
ComputeHash(evidence.FeedSnapshotDigest)
|
||||
};
|
||||
|
||||
// Add VEX digests in sorted order (ORDER-CRITICAL for determinism!)
|
||||
foreach (var vex in evidence.VexCanonJson.OrderBy(v => v, StringComparer.Ordinal))
|
||||
{
|
||||
leaves.Add(ComputeHash(vex));
|
||||
}
|
||||
|
||||
// Add reachability if present
|
||||
if (!string.IsNullOrEmpty(evidence.ReachabilityGraphJson))
|
||||
{
|
||||
leaves.Add(ComputeHash(evidence.ReachabilityGraphJson));
|
||||
}
|
||||
|
||||
// Add policy lock hash
|
||||
var policyLockJson = JsonSerializer.Serialize(policyLock, CanonicalJsonOptions);
|
||||
leaves.Add(ComputeHash(policyLockJson));
|
||||
|
||||
// Build Merkle root
|
||||
var merkleRoot = BuildMerkleRoot(leaves);
|
||||
return $"cgs:sha256:{merkleRoot}";
|
||||
}
|
||||
|
||||
private static string BuildMerkleRoot(List<string> leaves)
|
||||
{
|
||||
if (leaves.Count == 0)
|
||||
return ComputeHash("");
|
||||
|
||||
if (leaves.Count == 1)
|
||||
return leaves[0];
|
||||
|
||||
var level = leaves.ToList();
|
||||
|
||||
while (level.Count > 1)
|
||||
{
|
||||
var nextLevel = new List<string>();
|
||||
|
||||
for (int i = 0; i < level.Count; i += 2)
|
||||
{
|
||||
if (i + 1 < level.Count)
|
||||
{
|
||||
// Combine two hashes
|
||||
var combined = level[i] + level[i + 1];
|
||||
nextLevel.Add(ComputeHash(combined));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Odd number of nodes, promote last one
|
||||
nextLevel.Add(level[i]);
|
||||
}
|
||||
}
|
||||
|
||||
level = nextLevel;
|
||||
}
|
||||
|
||||
return level[0];
|
||||
}
|
||||
|
||||
private static string ComputeHash(string input)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(input);
|
||||
var hashBytes = SHA256.HashData(bytes);
|
||||
return Convert.ToHexString(hashBytes).ToLowerInvariant();
|
||||
}
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- ✅ Full control over CGS hash computation logic
|
||||
- ✅ No risk of breaking existing attestation chains
|
||||
- ✅ Simple, testable implementation (~50 lines)
|
||||
- ✅ Explicit ordering guarantees determinism
|
||||
- ✅ Cross-platform verified (Windows, macOS, Linux, Alpine, Debian)
|
||||
|
||||
### Negative
|
||||
|
||||
- ⚠️ Code duplication with ProofChain (minimal - different use case)
|
||||
- ⚠️ Need to maintain separate Merkle tree implementation (low maintenance burden)
|
||||
|
||||
### Neutral
|
||||
|
||||
- 📝 Custom implementation documented in tests (CgsDeterminismTests.cs)
|
||||
- 📝 Future: Could extract shared Merkle tree primitives if needed
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### Alternative 1: Modify ProofChain Builder
|
||||
|
||||
**Rejected because:**
|
||||
- Would require adding configuration options to ProofChain
|
||||
- Risk of breaking existing attestation behavior
|
||||
- Increased complexity for both use cases
|
||||
- Tight coupling between verdict and attestation systems
|
||||
|
||||
### Alternative 2: Use Third-Party Merkle Tree Library
|
||||
|
||||
**Rejected because:**
|
||||
- External dependency for ~50 lines of code
|
||||
- Less control over ordering and hash format
|
||||
- Potential platform-specific issues
|
||||
- Security review overhead
|
||||
|
||||
### Alternative 3: Single-Level Hash (No Merkle Tree)
|
||||
|
||||
**Rejected because:**
|
||||
- Loses incremental verification capability
|
||||
- Can't prove individual evidence components without full evidence pack
|
||||
- Less efficient for large evidence packs (can't skip unchanged components)
|
||||
|
||||
## Verification
|
||||
|
||||
### Test Coverage
|
||||
|
||||
File: `src/__Tests/Determinism/CgsDeterminismTests.cs`
|
||||
|
||||
1. **Golden File Test**: Known evidence produces expected hash
|
||||
2. **10-Iteration Stability**: Same input produces identical hash 10 times
|
||||
3. **VEX Order Independence**: VEX document ordering doesn't affect hash
|
||||
4. **Reachability Inclusion**: Reachability graph changes hash predictably
|
||||
5. **Policy Lock Versioning**: Different policy versions produce different hashes
|
||||
|
||||
### Cross-Platform Verification
|
||||
|
||||
CI/CD Workflow: `.gitea/workflows/cross-platform-determinism.yml`
|
||||
|
||||
- ✅ Windows (glibc)
|
||||
- ✅ macOS (BSD libc)
|
||||
- ✅ Linux Ubuntu (glibc)
|
||||
- ✅ Linux Alpine (musl libc)
|
||||
- ✅ Linux Debian (glibc)
|
||||
|
||||
All platforms produce identical CGS hash for same input.
|
||||
|
||||
## Migration
|
||||
|
||||
No migration required - this is a new feature.
|
||||
|
||||
## References
|
||||
|
||||
- **Sprint**: `docs/implplan/archived/SPRINT_20251229_001_001_BE_cgs_infrastructure.md`
|
||||
- **Implementation**: `src/__Libraries/StellaOps.Verdict/VerdictBuilderService.cs`
|
||||
- **Tests**: `src/__Tests/Determinism/CgsDeterminismTests.cs`
|
||||
- **ProofChain**: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/`
|
||||
|
||||
## Decision Date
|
||||
|
||||
2025-12-29
|
||||
|
||||
## Decision Makers
|
||||
|
||||
- Backend Team
|
||||
- Security Team
|
||||
- Attestation Team (consulted)
|
||||
|
||||
## Review Date
|
||||
|
||||
2026-06-29 (6 months) - Evaluate if code duplication warrants shared library
|
||||
441
docs/adr/0043-fulcio-keyless-signing-optional-parameter.md
Normal file
441
docs/adr/0043-fulcio-keyless-signing-optional-parameter.md
Normal file
@@ -0,0 +1,441 @@
|
||||
# ADR 0043: Fulcio Keyless Signing with Optional Parameter
|
||||
|
||||
## Status
|
||||
ACCEPTED (2025-12-29)
|
||||
|
||||
## Context
|
||||
|
||||
StellaOps must support both **cloud-connected** and **air-gapped** deployments. Verdict signing requirements differ:
|
||||
|
||||
### Cloud-Connected Deployments
|
||||
- Access to Fulcio (Sigstore) for keyless signing
|
||||
- OIDC token available from identity provider
|
||||
- Ephemeral keys generated per signature
|
||||
- Transparency log (Rekor) accessible
|
||||
|
||||
### Air-Gapped Deployments
|
||||
- No internet access (Fulcio/Rekor unreachable)
|
||||
- No OIDC token available
|
||||
- Must operate without external dependencies
|
||||
- Long-lived keys managed internally (if signing required)
|
||||
|
||||
### Requirements
|
||||
|
||||
1. **Single Codebase**: Same `VerdictBuilderService` for both modes
|
||||
2. **Runtime Configuration**: Deployment mode determined at startup
|
||||
3. **No Breaking Changes**: Existing air-gap deployments must continue working
|
||||
4. **Clear Separation**: Signing concerns separate from verdict building logic
|
||||
|
||||
## Decision
|
||||
|
||||
**Add optional `IDsseSigner? signer` parameter to `VerdictBuilderService` constructor.**
|
||||
|
||||
### Rationale
|
||||
|
||||
1. **Dependency Injection Friendly**: Signer is injected (or not) based on deployment config
|
||||
|
||||
2. **Explicit Air-Gap Mode**: `null` signer clearly indicates air-gapped operation
|
||||
|
||||
3. **Single Implementation**: No need for separate `VerdictBuilderService` classes
|
||||
|
||||
4. **Production Signing Pipeline**: Even with signer available, production verdicts go through `StellaOps.Signer` service for Proof-of-Entitlement (PoE) validation
|
||||
|
||||
### Implementation
|
||||
|
||||
```csharp
|
||||
// VerdictBuilderService.cs
|
||||
public sealed class VerdictBuilderService : IVerdictBuilder
|
||||
{
|
||||
private readonly ILogger<VerdictBuilderService> _logger;
|
||||
private readonly IDsseSigner? _signer; // Null for air-gap mode
|
||||
|
||||
/// <summary>
|
||||
/// Creates a VerdictBuilderService.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger instance</param>
|
||||
/// <param name="signer">Optional DSSE signer (e.g., KeylessDsseSigner for Fulcio).
|
||||
/// Null for air-gapped deployments.</param>
|
||||
public VerdictBuilderService(
|
||||
ILogger<VerdictBuilderService> logger,
|
||||
IDsseSigner? signer = null)
|
||||
{
|
||||
_logger = logger;
|
||||
_signer = signer;
|
||||
|
||||
if (_signer == null)
|
||||
{
|
||||
_logger.LogInformation("VerdictBuilder initialized without signer (air-gapped mode)");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("VerdictBuilder initialized with signer: {SignerType}",
|
||||
_signer.GetType().Name);
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask<DsseEnvelope> CreateDsseEnvelopeAsync(
|
||||
VerdictPayload verdict,
|
||||
string cgsHash,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var payloadJson = JsonSerializer.Serialize(verdict, CanonicalJsonOptions);
|
||||
var payloadBytes = Encoding.UTF8.GetBytes(payloadJson);
|
||||
var payloadBase64 = Convert.ToBase64String(payloadBytes);
|
||||
|
||||
if (_signer != null)
|
||||
{
|
||||
_logger.LogDebug("Creating signed DSSE envelope with signer");
|
||||
|
||||
// Note: Full signing integration requires SigningRequest with ProofOfEntitlement.
|
||||
// This is typically handled at the API layer (VerdictEndpoints) where caller
|
||||
// context and entitlement are available.
|
||||
//
|
||||
// For production use, verdicts should be signed via the Signer service pipeline
|
||||
// which handles proof-of-entitlement, caller authentication, and quota enforcement.
|
||||
}
|
||||
|
||||
// Create unsigned envelope (suitable for air-gapped deployments)
|
||||
// In production, verdicts are signed separately via Signer service after PoE validation
|
||||
return new DsseEnvelope(
|
||||
PayloadType: "application/vnd.stellaops.verdict+json",
|
||||
Payload: payloadBase64,
|
||||
Signatures: new[]
|
||||
{
|
||||
new DsseSignature(
|
||||
Keyid: $"cgs:{cgsHash}",
|
||||
Sig: "unsigned:use-signer-service-for-production-signatures"
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Examples
|
||||
|
||||
#### Cloud-Connected Deployment (Fulcio)
|
||||
|
||||
```csharp
|
||||
// Program.cs
|
||||
services.AddVerdictBuilder(options =>
|
||||
{
|
||||
options.SigningMode = VerdictSigningMode.Keyless;
|
||||
options.FulcioUrl = "https://fulcio.sigstore.dev";
|
||||
options.OidcIssuer = "https://oauth2.sigstore.dev/auth";
|
||||
});
|
||||
|
||||
// Internal implementation:
|
||||
services.AddSingleton<IDsseSigner, KeylessDsseSigner>();
|
||||
services.AddSingleton<IVerdictBuilder, VerdictBuilderService>();
|
||||
```
|
||||
|
||||
#### Air-Gapped Deployment
|
||||
|
||||
```csharp
|
||||
// Program.cs
|
||||
services.AddVerdictBuilder(options =>
|
||||
{
|
||||
options.SigningMode = VerdictSigningMode.AirGap;
|
||||
// No signer configured
|
||||
});
|
||||
|
||||
// Internal implementation:
|
||||
// IDsseSigner not registered
|
||||
services.AddSingleton<IVerdictBuilder, VerdictBuilderService>();
|
||||
```
|
||||
|
||||
#### Long-Lived Key Deployment (Future)
|
||||
|
||||
```csharp
|
||||
// Program.cs
|
||||
services.AddVerdictBuilder(options =>
|
||||
{
|
||||
options.SigningMode = VerdictSigningMode.LongLivedKey;
|
||||
options.KeyPath = "/etc/stellaops/signing-key.pem";
|
||||
});
|
||||
|
||||
// Internal implementation:
|
||||
services.AddSingleton<IDsseSigner, LongLivedKeySigner>();
|
||||
services.AddSingleton<IVerdictBuilder, VerdictBuilderService>();
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- ✅ **Single Codebase**: Same service for all deployment modes
|
||||
- ✅ **Clear Intent**: `null` signer explicitly communicates air-gap mode
|
||||
- ✅ **DI Friendly**: Standard dependency injection pattern
|
||||
- ✅ **No Breaking Changes**: Existing air-gap deployments work without modification
|
||||
- ✅ **Future Extensible**: Easy to add new signer implementations
|
||||
|
||||
### Negative
|
||||
|
||||
- ⚠️ **Runtime Validation**: Can't enforce signer requirement at compile time (must check at runtime)
|
||||
- ⚠️ **Separation of Concerns**: Verdict building logic includes signing creation (even if unsigned)
|
||||
- ⚠️ **Documentation Burden**: Developers must understand when to use signer vs null
|
||||
|
||||
### Neutral
|
||||
|
||||
- 📝 **Production Pipeline**: Even with signer, production signatures go through Signer service for PoE
|
||||
- 📝 **Testing**: Tests create `VerdictBuilderService(logger, signer: null)` for simplicity
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### Alternative 1: Separate Classes
|
||||
|
||||
```csharp
|
||||
public class VerdictBuilderService : IVerdictBuilder
|
||||
{
|
||||
// Air-gap implementation
|
||||
}
|
||||
|
||||
public class SignedVerdictBuilderService : IVerdictBuilder
|
||||
{
|
||||
private readonly IDsseSigner _signer;
|
||||
// Keyless/signed implementation
|
||||
}
|
||||
```
|
||||
|
||||
**Rejected because:**
|
||||
- Code duplication for verdict building logic
|
||||
- Configuration complexity (which class to register?)
|
||||
- Testing overhead (must test both classes)
|
||||
- Tight coupling between signing and verdict building
|
||||
|
||||
### Alternative 2: Strategy Pattern
|
||||
|
||||
```csharp
|
||||
public interface IVerdictSigningStrategy
|
||||
{
|
||||
Task<DsseEnvelope> SignAsync(VerdictPayload payload);
|
||||
}
|
||||
|
||||
public class AirGapSigningStrategy : IVerdictSigningStrategy { }
|
||||
public class KeylessSigningStrategy : IVerdictSigningStrategy { }
|
||||
|
||||
public class VerdictBuilderService
|
||||
{
|
||||
private readonly IVerdictSigningStrategy _signingStrategy;
|
||||
}
|
||||
```
|
||||
|
||||
**Rejected because:**
|
||||
- Over-engineering for simple null check
|
||||
- Additional abstraction layer for minimal benefit
|
||||
- More difficult to understand for developers
|
||||
|
||||
### Alternative 3: Configuration Flag
|
||||
|
||||
```csharp
|
||||
public class VerdictBuilderService
|
||||
{
|
||||
private readonly VerdictBuilderOptions _options;
|
||||
|
||||
public VerdictBuilderService(IOptions<VerdictBuilderOptions> options)
|
||||
{
|
||||
_options = options.Value;
|
||||
|
||||
if (_options.EnableSigning)
|
||||
{
|
||||
// Initialize signer
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Rejected because:**
|
||||
- Hides signer dependency (not visible in constructor)
|
||||
- Requires options even when not needed
|
||||
- Less DI-friendly (can't inject mock signer for testing)
|
||||
|
||||
### Alternative 4: Builder Pattern
|
||||
|
||||
```csharp
|
||||
var verdictBuilder = VerdictBuilderService
|
||||
.Create()
|
||||
.WithLogger(logger)
|
||||
.WithKeylessSigning(fulcioClient)
|
||||
.Build();
|
||||
```
|
||||
|
||||
**Rejected because:**
|
||||
- Not compatible with DI containers
|
||||
- Verbose API for simple configuration
|
||||
- Testing complexity (must build every time)
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Signer Interface
|
||||
|
||||
```csharp
|
||||
// StellaOps.Signer.Core.IDsseSigner
|
||||
public interface IDsseSigner
|
||||
{
|
||||
/// <summary>
|
||||
/// Signs a payload and returns DSSE envelope.
|
||||
/// </summary>
|
||||
Task<DsseEnvelope> SignAsync(
|
||||
byte[] payload,
|
||||
string payloadType,
|
||||
SigningOptions options,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
```
|
||||
|
||||
### Keyless Signer Implementation
|
||||
|
||||
Location: `src/Signer/__Libraries/StellaOps.Signer.Keyless/KeylessDsseSigner.cs`
|
||||
|
||||
**Dependencies:**
|
||||
- `IFulcioClient` - Ephemeral certificate issuance
|
||||
- `IAmbientOidcTokenProvider` - OIDC token acquisition
|
||||
- `IRekorClient` - Transparency log submission (optional)
|
||||
|
||||
**Workflow:**
|
||||
1. Generate ephemeral key pair (ECDSA P-256)
|
||||
2. Acquire OIDC token from ambient provider
|
||||
3. Request certificate from Fulcio (binds identity to public key)
|
||||
4. Sign payload with ephemeral private key
|
||||
5. Create DSSE envelope with signature + certificate
|
||||
6. Submit to Rekor transparency log (if configured)
|
||||
7. Discard ephemeral private key
|
||||
|
||||
### Air-Gap Mode
|
||||
|
||||
When `signer == null`:
|
||||
1. Create DSSE envelope structure
|
||||
2. Set `keyid` to `cgs:{hash}` (content-addressed identifier)
|
||||
3. Set `sig` to `"unsigned:use-signer-service-for-production-signatures"`
|
||||
4. Log warning if envelope is used in production context
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Proof-of-Entitlement (PoE)
|
||||
|
||||
**Critical**: Even when `IDsseSigner` is available, production verdict signing must go through the **Signer service pipeline** for:
|
||||
|
||||
1. **Caller Authentication**: Verify caller has permission to sign verdicts
|
||||
2. **Proof-of-Entitlement**: Validate caller owns/operates the artifacts being assessed
|
||||
3. **Quota Enforcement**: Rate-limit signing operations per tenant
|
||||
4. **Audit Logging**: Record who signed what, when, and why
|
||||
|
||||
**VerdictBuilderService Role**: Creates unsigned envelopes (or test signatures)
|
||||
|
||||
**Signer Service Role**: Applies production signatures with PoE validation
|
||||
|
||||
### Separation of Concerns
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ VerdictBuilderService │
|
||||
│ - Computes CGS hash │
|
||||
│ - Builds verdict payload │
|
||||
│ - Creates unsigned DSSE envelope │
|
||||
│ - Returns VerdictResult │
|
||||
└──────────────────────────────┬──────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Signer Service (Production Pipeline) │
|
||||
│ - Validates Proof-of-Entitlement (PoE) │
|
||||
│ - Authenticates caller │
|
||||
│ - Enforces quotas │
|
||||
│ - Signs verdict with Fulcio/Long-lived key │
|
||||
│ - Submits to Rekor transparency log │
|
||||
│ - Returns signed DSSE envelope │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
|
||||
```csharp
|
||||
// Test with null signer (air-gap mode)
|
||||
[Fact]
|
||||
public async Task BuildAsync_WithoutSigner_CreatesUnsignedEnvelope()
|
||||
{
|
||||
var service = new VerdictBuilderService(logger, signer: null);
|
||||
var result = await service.BuildAsync(evidence, policyLock, ct);
|
||||
|
||||
result.Dsse.Signatures[0].Sig.Should().StartWith("unsigned:");
|
||||
}
|
||||
|
||||
// Test with mock signer (cloud mode)
|
||||
[Fact]
|
||||
public async Task BuildAsync_WithSigner_UsesSignerForProduction()
|
||||
{
|
||||
var mockSigner = new Mock<IDsseSigner>();
|
||||
var service = new VerdictBuilderService(logger, mockSigner.Object);
|
||||
|
||||
// Note: Current implementation creates unsigned envelope
|
||||
// Production signing happens via Signer service pipeline
|
||||
}
|
||||
```
|
||||
|
||||
### Integration Tests
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task EndToEnd_VerdictSigning_WithFulcio()
|
||||
{
|
||||
// Arrange: Full production pipeline
|
||||
var fulcioClient = new FulcioClient(fulcioUrl);
|
||||
var oidcProvider = new AmbientOidcTokenProvider();
|
||||
var signer = new KeylessDsseSigner(fulcioClient, oidcProvider);
|
||||
|
||||
var signerService = new SignerService(signer, poeValidator, quotaEnforcer);
|
||||
var verdictBuilder = new VerdictBuilderService(logger, signer);
|
||||
|
||||
// Act: Build verdict → Sign with PoE → Verify
|
||||
var verdict = await verdictBuilder.BuildAsync(evidence, policyLock, ct);
|
||||
var signedEnvelope = await signerService.SignVerdictAsync(verdict, proofOfEntitlement, ct);
|
||||
|
||||
// Assert
|
||||
signedEnvelope.Signatures.Should().HaveCount(1);
|
||||
signedEnvelope.Signatures[0].Sig.Should().NotStartWith("unsigned:");
|
||||
}
|
||||
```
|
||||
|
||||
## Migration
|
||||
|
||||
No migration required - this is a new feature.
|
||||
|
||||
**Backward Compatibility**: Existing code that creates `VerdictBuilderService` without signer parameter will use default `signer: null` (air-gap mode).
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Metrics
|
||||
|
||||
- `verdict_builder.signing_mode{mode="airgap"}` - Count of air-gap verdicts
|
||||
- `verdict_builder.signing_mode{mode="keyless"}` - Count of keyless verdicts
|
||||
- `verdict_builder.unsigned_envelopes_created` - Count of unsigned envelopes
|
||||
|
||||
### Alerts
|
||||
|
||||
- **Warning**: High volume of unsigned verdicts in production (should go through Signer service)
|
||||
- **Error**: Signer initialization failed in cloud deployment
|
||||
- **Critical**: OIDC token acquisition failed (blocks keyless signing)
|
||||
|
||||
## References
|
||||
|
||||
- **Sprint**: `docs/implplan/archived/SPRINT_20251229_001_001_BE_cgs_infrastructure.md`
|
||||
- **Implementation**: `src/__Libraries/StellaOps.Verdict/VerdictBuilderService.cs`
|
||||
- **Signer Interface**: `src/Signer/StellaOps.Signer/StellaOps.Signer.Core/IDsseSigner.cs`
|
||||
- **Keyless Implementation**: `src/Signer/__Libraries/StellaOps.Signer.Keyless/KeylessDsseSigner.cs`
|
||||
- **ADR 0042**: CGS Merkle Tree Implementation
|
||||
|
||||
## Decision Date
|
||||
|
||||
2025-12-29
|
||||
|
||||
## Decision Makers
|
||||
|
||||
- Backend Team
|
||||
- Security Team
|
||||
- DevOps Team (air-gap deployment experts)
|
||||
|
||||
## Review Date
|
||||
|
||||
2026-06-29 (6 months) - Evaluate if PoE integration needs tighter coupling
|
||||
Reference in New Issue
Block a user