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:
master
2025-12-29 19:12:38 +02:00
parent 41552d26ec
commit a4badc275e
286 changed files with 50918 additions and 992 deletions

View 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