audit work, fixed StellaOps.sln warnings/errors, fixed tests, sprints work, new advisories
This commit is contained in:
220
docs/technical/adr/0042-cgs-merkle-tree-implementation.md
Normal file
220
docs/technical/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
|
||||
@@ -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
|
||||
205
docs/technical/adr/0044-binary-delta-signatures.md
Normal file
205
docs/technical/adr/0044-binary-delta-signatures.md
Normal file
@@ -0,0 +1,205 @@
|
||||
# ADR 0044: Binary Delta Signatures for Backport Detection
|
||||
|
||||
## Status
|
||||
ACCEPTED (2026-01-03)
|
||||
|
||||
## Context
|
||||
|
||||
Vulnerability scanners today rely on version string comparison to determine if a package is vulnerable. However, Linux distributions (RHEL, Debian, Ubuntu, SUSE, Alpine) routinely **backport** security fixes into older versions without bumping the upstream version number.
|
||||
|
||||
### The Problem
|
||||
|
||||
**Example:** OpenSSL 1.0.1e on RHEL 6 has Heartbleed (CVE-2014-0160) patched, but upstream says `1.0.1e < 1.0.1g` (the fix version), so scanners flag it as vulnerable. This creates:
|
||||
|
||||
1. **False positives** - Patched systems flagged as vulnerable
|
||||
2. **Alert fatigue** - Security teams waste time investigating non-issues
|
||||
3. **Compliance failures** - Audit reports show phantom vulnerabilities
|
||||
4. **Trust erosion** - Users distrust scanner results
|
||||
|
||||
### Current Mitigations
|
||||
|
||||
1. **Distro-specific advisory feeds** (DSA, RHSA, USN) - Incomplete coverage
|
||||
2. **VEX statements from vendors** - Requires vendor participation, often delayed
|
||||
3. **Manual triage** - Doesn't scale
|
||||
4. **OVAL feeds** - OS packages only, not application binaries
|
||||
|
||||
### Requirements
|
||||
|
||||
- **Binary-level detection**: Examine compiled code, not version strings
|
||||
- **Cryptographic proof**: Hash-based evidence that fix is present
|
||||
- **Offline operation**: Work in air-gapped environments
|
||||
- **Multi-architecture**: Support x86-64, ARM64, and other ISAs
|
||||
- **Deterministic**: Same binary → same signature across platforms
|
||||
- **LTO resilience**: Handle Link-Time Optimization changes
|
||||
|
||||
## Decision
|
||||
|
||||
**Implement binary delta signature matching using normalized code comparison.**
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Delta Signature Pipeline │
|
||||
├────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Binary Disassembly Normalization Signature │
|
||||
│ ───────────► ───────────────► ──────────────► ─────────────► │
|
||||
│ ELF/PE/MachO Iced (x86) or Zero addresses SHA-256 + │
|
||||
│ B2R2 (ARM/MIPS) Canonicalize NOPs CFG hash + │
|
||||
│ Normalize PLT/GOT Chunk hashes │
|
||||
│ │
|
||||
└────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Disassembly Engine Selection
|
||||
|
||||
**Chosen: Plugin-based architecture with Iced (primary) + B2R2 (fallback)**
|
||||
|
||||
| Engine | Strengths | Weaknesses |
|
||||
|--------|-----------|------------|
|
||||
| **Iced** | Fastest x86/x86-64, MIT license, pure C# | x86 only |
|
||||
| **B2R2** | Multi-arch (ARM, MIPS, RISC-V), IR lifting, MIT license | F# (requires wrapper) |
|
||||
|
||||
**Rationale:**
|
||||
- Iced for performance-critical x86/x86-64 path (90%+ of scanned binaries)
|
||||
- B2R2 for ARM64, MIPS, RISC-V when needed
|
||||
- Plugin architecture allows adding engines without core changes
|
||||
|
||||
### Normalization Strategy
|
||||
|
||||
To compare binaries compiled by different toolchains/versions, we normalize:
|
||||
|
||||
1. **Zero absolute addresses** - Remove PC-relative and RIP-relative variance
|
||||
2. **Canonicalize NOPs** - Collapse multi-byte NOPs (0x90, 0x0F1F, etc.) to single NOP
|
||||
3. **Normalize PLT/GOT** - Replace dynamic linking stubs with symbolic tokens
|
||||
4. **Zero relocations** - Remove relocation target variance
|
||||
5. **Normalize jump tables** - Convert absolute offsets to relative
|
||||
|
||||
**Recipe versioning**: Every signature includes the normalization recipe ID and version. Changing normalization behavior requires a version bump.
|
||||
|
||||
### Signature Components
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": "stellaops.deltasig.v1",
|
||||
"cve": "CVE-2014-0160",
|
||||
"package": { "name": "openssl", "soname": "libssl.so.1.0.0" },
|
||||
"target": { "arch": "x86_64", "abi": "gnu" },
|
||||
"normalization": { "recipeId": "stellaops.normalize.x64.v1", "version": "1.0.0" },
|
||||
"signatureState": "patched",
|
||||
"symbols": [
|
||||
{
|
||||
"name": "tls1_process_heartbeat",
|
||||
"hashAlg": "sha256",
|
||||
"hashHex": "abc123...",
|
||||
"sizeBytes": 1234,
|
||||
"cfgBbCount": 15,
|
||||
"cfgEdgeHash": "def456...",
|
||||
"chunks": [
|
||||
{ "offset": 0, "size": 2048, "hashHex": "..." },
|
||||
{ "offset": 2048, "size": 2048, "hashHex": "..." }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Matching Strategy
|
||||
|
||||
1. **Exact match** - Full normalized hash matches patched or vulnerable signature
|
||||
2. **Chunk match** - ≥70% of chunks match (handles LTO modifications)
|
||||
3. **CFG match** - Control flow graph structure matches (catches recompilations)
|
||||
|
||||
### VEX Evidence Emission
|
||||
|
||||
When a binary is confirmed patched via delta signature:
|
||||
|
||||
```json
|
||||
{
|
||||
"result": "patched",
|
||||
"cveIds": ["CVE-2014-0160"],
|
||||
"confidence": 0.95,
|
||||
"symbolMatches": [
|
||||
{ "symbolName": "tls1_process_heartbeat", "state": "patched", "exactMatch": true }
|
||||
],
|
||||
"justification": "vulnerable_code_not_present",
|
||||
"summary": "Binary confirmed PATCHED with 95% confidence. 1 symbol(s) matched patched signatures exactly."
|
||||
}
|
||||
```
|
||||
|
||||
This evidence feeds into VEX candidate generation with full audit trail.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### 1. Source Code Comparison
|
||||
**Rejected**: Requires source access, doesn't work for closed-source binaries, compile options affect behavior.
|
||||
|
||||
### 2. Debug Symbol Matching
|
||||
**Rejected**: Symbols often stripped in production, doesn't prove code content.
|
||||
|
||||
### 3. File Hash Matching
|
||||
**Rejected**: Entire binary must match exactly; any rebuild invalidates signature.
|
||||
|
||||
### 4. YARA Rules
|
||||
**Rejected**: Pattern-based, high false positive rate, doesn't provide cryptographic proof.
|
||||
|
||||
### 5. Single Disassembly Engine (B2R2 only)
|
||||
**Rejected**: Performance critical; Iced is 3-5x faster for x86/x86-64 which is 90%+ of scanned binaries.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
1. **Eliminate false positives** for backported security fixes
|
||||
2. **Cryptographic proof** of patch status (auditable, reproducible)
|
||||
3. **Offline operation** with signature packs
|
||||
4. **Multi-architecture** support for modern infrastructure
|
||||
5. **VEX integration** for automated triage
|
||||
|
||||
### Negative
|
||||
|
||||
1. **Signature authoring required** - Must create signatures for each CVE/package
|
||||
2. **Normalization limits** - Extreme compiler optimizations may defeat matching
|
||||
3. **Storage overhead** - Signature database growth
|
||||
4. **Compute cost** - Disassembly + normalization per binary
|
||||
|
||||
### Mitigations
|
||||
|
||||
- **Signature federation** - Share signatures across organizations
|
||||
- **Chunk matching** - Resilient to LTO and PGO changes
|
||||
- **Priority authoring** - Focus on high-severity CVEs first
|
||||
- **Incremental scanning** - Cache analysis results
|
||||
|
||||
## Implementation
|
||||
|
||||
### Sprint: SPRINT_20260102_001_BE
|
||||
|
||||
| Component | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| Disassembly.Abstractions | DONE | Plugin interface, models |
|
||||
| Disassembly.Iced | DONE | x86/x86-64 support |
|
||||
| Disassembly.B2R2 | DONE | Multi-arch support |
|
||||
| Normalization | DONE | X64 + ARM64 pipelines |
|
||||
| DeltaSig | DONE | Generator + matcher |
|
||||
| Persistence | DONE | PostgreSQL schema |
|
||||
| CLI | DONE | extract, author, sign, verify, match, pack, inspect |
|
||||
| Scanner integration | DONE | DeltaSigAnalyzer, IBinaryVulnerabilityService |
|
||||
| VEX emission | DONE | DeltaSignatureEvidence, DeltaSigVexEmitter |
|
||||
|
||||
### Test Coverage
|
||||
|
||||
- 74 unit tests for DeltaSig library
|
||||
- 45 unit tests for Normalization
|
||||
- 24 unit tests for Disassembly
|
||||
- 11 property tests (FsCheck) for normalization idempotency
|
||||
- 14 golden tests for known CVEs (Heartbleed, Log4Shell, POODLE)
|
||||
- 25 unit tests for VEX evidence emission
|
||||
|
||||
## References
|
||||
|
||||
- [Binary Diff Signatures Advisory](../product-advisories/30-Dec-2025%20-%20Binary%20Diff%20Signatures%20for%20Patch%20Detection.md)
|
||||
- [B2R2 GitHub](https://github.com/B2R2-org/B2R2)
|
||||
- [Iced GitHub](https://github.com/icedland/iced)
|
||||
- [OpenVEX Specification](https://github.com/openvex/spec)
|
||||
- [CVE-2014-0160 (Heartbleed)](https://nvd.nist.gov/vuln/detail/CVE-2014-0160)
|
||||
165
docs/technical/adr/authority-plugin-ldap.md
Normal file
165
docs/technical/adr/authority-plugin-ldap.md
Normal file
@@ -0,0 +1,165 @@
|
||||
# RFC: StellaOps.Authority.Plugin.Ldap
|
||||
|
||||
**Status:** Accepted – Auth Guild, Security Guild, DevEx Docs sign-off (2025-11-03)
|
||||
**Authors:** Plugin Team 4 (Auth Libraries & Identity Providers)
|
||||
**Related initiatives:** PLG7 backlog, CORE5 event handlers, DOC4 developer guide
|
||||
|
||||
> Review log captured in `docs/notes/2025-11-03-authority-plugin-ldap-review.md`. Decisions below reflect consensus from Auth Guild, Security Guild, and DevEx Docs walkthroughs held on 2025-11-03.
|
||||
|
||||
## 0. Review Summary (2025-11-03)
|
||||
|
||||
- Confirmed plugin charter: deliver offline-friendly LDAP credential verification with deterministic caching and audit parity with the Standard plugin.
|
||||
- Resolved open questions:
|
||||
- **Client provisioning audit trail:** plugin always mirrors credential lifecycle metadata (actor, timestamp, hashed secret reference) into Authority’s Mongo store even when LDAP writes succeed; directory writes remain optional and secrets never persist locally.
|
||||
- **Mutual TLS support:** LDAPS client certificates are required for regulated installations; configuration gains optional client certificate bindings with secret-provider integration and deterministic trust-store selection.
|
||||
- **Group-to-role mapping:** mappings accept static DN dictionaries and deterministic `regex` matchers; regex captures project to canonical roles via substitution (documented contract for policy automation).
|
||||
- Follow-up implementation issues filed in `StellaOps.Authority.Plugin.Standard/TASKS.md` (see Section 11) to track scaffolding, mutual TLS enablement, audit mirror, and mapping enhancements.
|
||||
|
||||
## 1. Problem Statement
|
||||
Many on-prem StellaOps deployments rely on existing LDAP/Active Directory domains for workforce identity. The current Standard Mongo-backed plugin requires duplicating users and secrets, which increases operational overhead and violates corporate policy in some regulated environments. We need a sovereign, offline-friendly LDAP plugin that:
|
||||
|
||||
- Supports password grant and bootstrap provisioning flows without storing credentials in Mongo.
|
||||
- Enforces StellaOps security policies (lockout, password policy hints, audit logging) while delegating credential validation to LDAP.
|
||||
- Operates deterministically in offline or partially connected environments by caching directory metadata when necessary.
|
||||
|
||||
## 2. Goals
|
||||
- Provide a first-party `StellaOps.Authority.Plugin.Ldap` plugin advertising `password` and optional `clientProvisioning` capabilities at launch.
|
||||
- Support username/password authentication against LDAP bind operations with configurable DN templates.
|
||||
- Allow optional bootstrap seeding of service accounts by writing into LDAP (guarded behind explicit configuration) or by mapping to pre-existing entries.
|
||||
- Surface directory-derived claims (groups, attributes) for downstream authorization via `IClaimsEnricher`.
|
||||
- Integrate with Authority lockout telemetry and structured logging without persisting secrets locally.
|
||||
|
||||
## 3. Non-Goals
|
||||
- Implement multi-factor authentication out of the box (future enhancement once TOTP/WebAuthn strategy is finalised).
|
||||
- Provide write-heavy directory management (e.g., user creation workflows) beyond optional bootstrap service account seeding.
|
||||
- Replace the Standard plugin; both must remain supported and selectable per environment.
|
||||
|
||||
## 4. Key Constraints & Assumptions
|
||||
- Offline-first posture: deployments may operate without outbound internet and with intermittent directory connectivity (e.g., read-only replicas). The plugin must tolerate transient LDAP connectivity failures and degrade gracefully.
|
||||
- Deterministic behaviour: identical configuration and directory state must yield identical token issuance results. Cached metadata (e.g., group lookups) must have defined expiration.
|
||||
- Security: No plaintext credential storage; TLS must be enforced for LDAP connections unless explicitly overridden for air-gapped lab environments.
|
||||
|
||||
## 5. High-Level Architecture
|
||||
1. **Configuration binding** (`ldap.yaml`): defines server endpoints, bind strategy, claim mapping, and optional bootstrap overrides.
|
||||
2. **Connection factory**: pooled LDAP connections using a resilient client (preferred dependency: `Novell.Directory.Ldap.NETStandard`).
|
||||
3. **Credential validator** (`IUserCredentialStore`): performs bind-as-user flow with optional fallback bind using service account when directories disallow anonymous search.
|
||||
4. **Claims enricher** (`IClaimsEnricher`): queries group membership/attributes and projects them into canonical roles/claims.
|
||||
5. **Optional client provisioning / bootstrap** (`IClientProvisioningStore` + `IUserCredentialStore.UpsertUserAsync`): maintains machine/service principals either in Mongo (metadata) or via LDAP entries based on configuration. Capabilities are only advertised when the manifest requests them, configuration enables them, **and** the plug-in proves the bind identity can add/delete entries beneath the configured containers; otherwise the feature is automatically downgraded so read-only deployments remain safe.
|
||||
6. **Health checks**: periodic LDAP `whoami` or `search` probes surfaced through `AuthorityPluginHealthResult`.
|
||||
|
||||
```
|
||||
Authority Host
|
||||
├── Plugin Manifest (ldap)
|
||||
├── Registrar → registers ConnectionFactory, LdapCredentialStore, LdapClaimsEnricher
|
||||
├── Password Grant Handler → CredentialStore.VerifyPasswordAsync → LDAP Bind
|
||||
└── Claims Pipeline → ClaimsEnricher.EnrichAsync → LDAP group lookup
|
||||
```
|
||||
|
||||
## 6. Configuration Schema (Draft)
|
||||
```yaml
|
||||
connection:
|
||||
host: "ldaps://ldap.example.internal"
|
||||
port: 636
|
||||
useStartTls: false
|
||||
validateCertificates: true
|
||||
clientCertificate:
|
||||
pfxPath: "file:/etc/stellaops/certs/ldap-client.pfx"
|
||||
passwordSecret: "file:/etc/stellaops/secrets/ldap-client-pfx.txt"
|
||||
sendChain: true
|
||||
trustStore:
|
||||
mode: "system" # system | bundle
|
||||
bundlePath: "file:/etc/stellaops/trust/ldap-root.pem"
|
||||
bindDn: "cn=stellaops-bind,ou=service,dc=example,dc=internal"
|
||||
bindPasswordSecret: "file:/etc/stellaops/secrets/ldap-bind.txt"
|
||||
searchBase: "dc=example,dc=internal"
|
||||
usernameAttribute: "uid"
|
||||
userDnFormat: "uid={username},ou=people,dc=example,dc=internal" # optional template
|
||||
security:
|
||||
requireTls: true
|
||||
allowInsecureWithEnvToggle: false # requires STELLAOPS_LDAP_ALLOW_INSECURE=true
|
||||
allowedCipherSuites: [] # optional allow-list
|
||||
referralChasing: false
|
||||
lockout:
|
||||
useAuthorityPolicies: true # reuse Authority lockout counters
|
||||
directoryLockoutAttribute: "pwdAccountLockedTime"
|
||||
claims:
|
||||
groupAttribute: "memberOf"
|
||||
groupToRoleMap:
|
||||
"cn=stellaops-admins,ou=groups,dc=example,dc=internal": "operators"
|
||||
"cn=stellaops-read,ou=groups,dc=example,dc=internal": "auditors"
|
||||
regexMappings:
|
||||
- pattern: "^cn=stellaops-(?P<role>[a-z-]+),ou=groups,dc=example,dc=internal$"
|
||||
roleFormat: "{role}" # yields operators/investigate/etc.
|
||||
extraAttributes:
|
||||
displayName: "displayName"
|
||||
email: "mail"
|
||||
clientProvisioning:
|
||||
enabled: false
|
||||
containerDn: "ou=service,dc=example,dc=internal"
|
||||
secretAttribute: "userPassword"
|
||||
auditMirror:
|
||||
enabled: true
|
||||
collectionName: "ldap_client_provisioning"
|
||||
health:
|
||||
probeIntervalSeconds: 60
|
||||
timeoutSeconds: 5
|
||||
```
|
||||
|
||||
## 7. Capability Mapping
|
||||
| Capability | Implementation Notes |
|
||||
|------------|---------------------|
|
||||
| `password` | Bind-as-user validation with Authority lockout integration. Mandatory. Requires TLS and optionally client certificate binding per regulated install posture. |
|
||||
| `clientProvisioning` | Optional; when enabled, creates/updates LDAP entries for machine clients **and** mirrors lifecycle metadata into Mongo for audit parity. |
|
||||
| `bootstrap` | Exposed only when bootstrap manifest provides service account credentials AND directory write permissions are confirmed during startup; always records audit mirror entries. |
|
||||
| `mfa` | Not supported in MVP. Future iteration may integrate TOTP attributes or external MFA providers. |
|
||||
|
||||
## 8. Operational Considerations
|
||||
- **Offline cache:** provide optional Mongo cache for group membership to keep `/ready` responsive if LDAP is temporarily unreachable. Cache entries must include TTL and invalidation hooks.
|
||||
- **Secrets management:** accept `file:` and environment variable references; integrate with existing `StellaOps.Configuration` secret providers.
|
||||
- **Mutual TLS & trust anchors:** support client certificate authentication with deterministic trust-store selection (`system` vs bundled file) to satisfy regulated deployments; surface validation outcomes via health endpoints.
|
||||
- **Audit mirror:** write deterministic Mongo records capturing provisioning operations (actor, LDAP DN, operation type, hashed secret reference) to align with Authority audit policy even when LDAP is authoritative.
|
||||
- **Observability:** emit structured logs with event IDs (`LDAP_BIND_START`, `LDAP_BIND_FAILURE`, `LDAP_GROUP_LOOKUP`), counters for success/failure, and latency histograms.
|
||||
- **Throttling:** reuse Authority rate-limiting middleware; add per-connection throttles to avoid saturating directory servers during brute-force attacks.
|
||||
|
||||
## 9. Security & Compliance
|
||||
- Enforce TLS (`ldaps://` or STARTTLS) by default. Provide explicit `allowInsecure` flag gated behind environment variable for lab/testing only.
|
||||
- Support optional mutual TLS (client cert authentication) with secret-backed PFX loader and deterministic trust bundle selection.
|
||||
- Support password hash migration by detecting directory lockout attributes and surfacing `RequiresPasswordReset` when policies demand changes.
|
||||
- Log distinguished names only at `Debug` level to avoid leaking sensitive structure in default logs.
|
||||
- Coordinate with Security Guild for penetration testing before GA; incorporate audit log entries for bind attempts and provisioning changes.
|
||||
|
||||
## 10. Testing Strategy
|
||||
- **Unit tests:** mock LDAP connections to validate DN formatting, error mapping, and capability negotiation.
|
||||
- **Integration tests:** run against an ephemeral OpenLDAP container (seeded via LDIF fixtures) within CI. Include mutual TLS handshake verification (valid/expired certs) and offline cache regression (disconnect LDAP mid-test).
|
||||
- **Determinism tests:** feed identical LDIF snapshots and configuration to ensure output tokens/claims remain stable across runs.
|
||||
- **Smoke tests:** `dotnet test` harness plus manual `dotnet run` scenario verifying `/token` password grants, `/internal/users` bootstrap with LDAP-backed store, and Mongo audit mirror entries.
|
||||
|
||||
## 11. Implementation Plan
|
||||
1. Scaffold `StellaOps.Authority.Plugin.Ldap` project + companion test project (net10.0, `<IsAuthorityPlugin>` true).
|
||||
2. Implement configuration binding/validation, including secret-backed client certificate + trust-store options and `allowInsecureWithEnvToggle`.
|
||||
3. Build connection factory + credential store with bind logic, TLS enforcement, and deterministic retry policies.
|
||||
4. Implement claims enricher with regex mapping support and optional Mongo-backed cache layer.
|
||||
5. Add client provisioning store with LDAP write toggles and Mongo audit mirror (`ldap_client_provisioning` collection).
|
||||
6. Wire health checks, telemetry, and structured audit events (bind attempts, provisioning, cache fallbacks).
|
||||
7. Deliver bootstrap validation that inspects directory permissions and logs deterministic capability summary.
|
||||
8. Extend developer guide and samples with LDAP configuration guidance; include mutual TLS and regex mapping examples.
|
||||
9. Update docs/TASKS trackers and release notes entry; ensure CI coverage (unit, integration with OpenLDAP, determinism, smoke tests).
|
||||
|
||||
## 12. Resolved Questions
|
||||
- **Audit mirror:** Client provisioning always persists lifecycle metadata in Mongo for audit parity; LDAP remains the credential source of truth.
|
||||
- **Mutual TLS:** Plugin must support optional client certificate authentication with secret-backed key material and deterministic trust-store selection.
|
||||
- **Group mapping:** Provide deterministic regex mapping support to translate directory DNs into Authority roles/scopes without custom scripts.
|
||||
|
||||
## 13. Timeline (Tentative)
|
||||
- **Week 1:** RFC review & sign-off.
|
||||
- **Week 2-3:** Implementation & unit tests.
|
||||
- **Week 4:** Integration tests + documentation updates.
|
||||
- **Week 5:** Security review, release candidate packaging.
|
||||
|
||||
## 14. Approval
|
||||
- **Auth Guild Lead:** ✅ Approved 2025-11-03 (see review log).
|
||||
- **Security Guild Representative:** ✅ Approved 2025-11-03 (see review log).
|
||||
- **DevEx Docs:** ✅ Approved 2025-11-03 (see review log).
|
||||
|
||||
---
|
||||
Please add comments inline or via PR review. Once approved, track execution under PLG7.
|
||||
Reference in New Issue
Block a user