Resolve Concelier/Excititor merge conflicts

This commit is contained in:
master
2025-10-20 14:19:25 +03:00
2687 changed files with 212646 additions and 85913 deletions

View File

@@ -1,21 +1,22 @@
# Team 8 — Security Guild (Authority & Shared Crypto)
## Role
Team 8 owns the end-to-end security posture for StellaOps Authority and its consumers. That includes password hashing policy, audit/event hygiene, rate-limit & lockout rules, revocation distribution, and sovereign cryptography abstractions that allow alternative algorithm suites (e.g., GOST) without touching feature code.
## Operational Boundaries
- Primary workspace: `src/StellaOps.Cryptography`, `src/StellaOps.Authority.Plugin.Standard`, `src/StellaOps.Authority.Storage.Mongo`, and Authority host (`src/StellaOps.Authority/StellaOps.Authority`).
- Coordinate cross-module changes via TASKS.md updates and PR descriptions.
- Never bypass deterministic behaviour (sorted keys, stable timestamps).
- Tests live alongside owning projects (`*.Tests`). Extend goldens instead of rewriting.
## Expectations
- Default to Argon2id (Konscious) for password hashing; PBKDF2 only for legacy verification with transparent rehash on success.
- Emit structured security events with minimal PII and clear correlation IDs.
- Rate-limit `/token` and bootstrap endpoints once CORE8 hooks are available.
- Deliver offline revocation bundles signed with detached JWS and provide a verification script.
- Maintain `docs/security/authority-threat-model.md` and ensure mitigations are tracked.
- All crypto consumption flows through `StellaOps.Cryptography` abstractions to enable sovereign crypto providers.
# Team 8 — Security Guild (Authority & Shared Crypto)
## Role
Team 8 owns the end-to-end security posture for StellaOps Authority and its consumers. That includes password hashing policy, audit/event hygiene, rate-limit & lockout rules, revocation distribution, and sovereign cryptography abstractions that allow alternative algorithm suites (e.g., GOST) without touching feature code.
## Operational Boundaries
- Primary workspace: `src/StellaOps.Cryptography`, `src/StellaOps.Authority.Plugin.Standard`, `src/StellaOps.Authority.Storage.Mongo`, and Authority host (`src/StellaOps.Authority/StellaOps.Authority`).
- Coordinate cross-module changes via TASKS.md updates and PR descriptions.
- Never bypass deterministic behaviour (sorted keys, stable timestamps).
- Tests live alongside owning projects (`*.Tests`). Extend goldens instead of rewriting.
## Expectations
- Default to Argon2id (Konscious) for password hashing; PBKDF2 only for legacy verification with transparent rehash on success.
- Emit structured security events with minimal PII and clear correlation IDs.
- Rate-limit `/token` and bootstrap endpoints once CORE8 hooks are available.
- Deliver offline revocation bundles signed with detached JWS and provide a verification script.
- Maintain `docs/security/authority-threat-model.md` and ensure mitigations are tracked.
- All crypto consumption flows through `StellaOps.Cryptography` abstractions to enable sovereign crypto providers.
- Every new cryptographic algorithm, dependency, or acceleration path ships as an `ICryptoProvider` plug-in under `StellaOps.Cryptography.*`; feature code must never bind directly to third-party crypto libraries.

View File

@@ -1,105 +1,176 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Security.Cryptography;
namespace StellaOps.Cryptography;
/// <summary>
/// Represents asymmetric signing key material managed by a crypto provider.
/// </summary>
public sealed class CryptoSigningKey
{
private static readonly ReadOnlyDictionary<string, string?> EmptyMetadata =
new(new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase));
public CryptoSigningKey(
CryptoKeyReference reference,
string algorithmId,
in ECParameters privateParameters,
DateTimeOffset createdAt,
DateTimeOffset? expiresAt = null,
IReadOnlyDictionary<string, string?>? metadata = null)
{
Reference = reference ?? throw new ArgumentNullException(nameof(reference));
if (string.IsNullOrWhiteSpace(algorithmId))
{
throw new ArgumentException("Algorithm identifier is required.", nameof(algorithmId));
}
if (privateParameters.D is null || privateParameters.D.Length == 0)
{
throw new ArgumentException("Private key parameters must include the scalar component.", nameof(privateParameters));
}
AlgorithmId = algorithmId;
CreatedAt = createdAt;
ExpiresAt = expiresAt;
PrivateParameters = CloneParameters(privateParameters, includePrivate: true);
PublicParameters = CloneParameters(privateParameters, includePrivate: false);
Metadata = metadata is null
? EmptyMetadata
: new ReadOnlyDictionary<string, string?>(metadata.ToDictionary(
static pair => pair.Key,
static pair => pair.Value,
StringComparer.OrdinalIgnoreCase));
}
/// <summary>
/// Gets the key reference (id + provider hint).
/// </summary>
public CryptoKeyReference Reference { get; }
/// <summary>
/// Gets the algorithm identifier (e.g., ES256).
/// </summary>
public string AlgorithmId { get; }
/// <summary>
/// Gets the private EC parameters (cloned).
/// </summary>
public ECParameters PrivateParameters { get; }
/// <summary>
/// Gets the public EC parameters (cloned, no private scalar).
/// </summary>
public ECParameters PublicParameters { get; }
/// <summary>
/// Gets the timestamp when the key was created/imported.
/// </summary>
public DateTimeOffset CreatedAt { get; }
/// <summary>
/// Gets the optional expiry timestamp for the key.
/// </summary>
public DateTimeOffset? ExpiresAt { get; }
/// <summary>
/// Gets arbitrary metadata entries associated with the key.
/// </summary>
public IReadOnlyDictionary<string, string?> Metadata { get; }
private static ECParameters CloneParameters(ECParameters source, bool includePrivate)
{
var clone = new ECParameters
{
Curve = source.Curve,
Q = new ECPoint
{
X = source.Q.X is null ? null : (byte[])source.Q.X.Clone(),
Y = source.Q.Y is null ? null : (byte[])source.Q.Y.Clone()
}
};
if (includePrivate && source.D is not null)
{
clone.D = (byte[])source.D.Clone();
}
return clone;
}
}
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Security.Cryptography;
namespace StellaOps.Cryptography;
/// <summary>
/// Describes the underlying key material for a <see cref="CryptoSigningKey"/>.
/// </summary>
public enum CryptoSigningKeyKind
{
Ec,
Raw
}
/// <summary>
/// Represents asymmetric signing key material managed by a crypto provider.
/// </summary>
public sealed class CryptoSigningKey
{
private static readonly ReadOnlyDictionary<string, string?> EmptyMetadata =
new(new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase));
private static readonly byte[] EmptyKey = Array.Empty<byte>();
private readonly byte[] privateKeyBytes;
private readonly byte[] publicKeyBytes;
public CryptoSigningKey(
CryptoKeyReference reference,
string algorithmId,
in ECParameters privateParameters,
DateTimeOffset createdAt,
DateTimeOffset? expiresAt = null,
IReadOnlyDictionary<string, string?>? metadata = null)
{
Reference = reference ?? throw new ArgumentNullException(nameof(reference));
if (string.IsNullOrWhiteSpace(algorithmId))
{
throw new ArgumentException("Algorithm identifier is required.", nameof(algorithmId));
}
if (privateParameters.D is null || privateParameters.D.Length == 0)
{
throw new ArgumentException("Private key parameters must include the scalar component.", nameof(privateParameters));
}
AlgorithmId = algorithmId;
CreatedAt = createdAt;
ExpiresAt = expiresAt;
Kind = CryptoSigningKeyKind.Ec;
privateKeyBytes = EmptyKey;
publicKeyBytes = EmptyKey;
PrivateParameters = CloneParameters(privateParameters, includePrivate: true);
PublicParameters = CloneParameters(privateParameters, includePrivate: false);
Metadata = metadata is null
? EmptyMetadata
: new ReadOnlyDictionary<string, string?>(metadata.ToDictionary(
static pair => pair.Key,
static pair => pair.Value,
StringComparer.OrdinalIgnoreCase));
}
public CryptoSigningKey(
CryptoKeyReference reference,
string algorithmId,
ReadOnlyMemory<byte> privateKey,
DateTimeOffset createdAt,
DateTimeOffset? expiresAt = null,
ReadOnlyMemory<byte> publicKey = default,
IReadOnlyDictionary<string, string?>? metadata = null)
{
Reference = reference ?? throw new ArgumentNullException(nameof(reference));
if (string.IsNullOrWhiteSpace(algorithmId))
{
throw new ArgumentException("Algorithm identifier is required.", nameof(algorithmId));
}
if (privateKey.IsEmpty)
{
throw new ArgumentException("Private key material must be provided.", nameof(privateKey));
}
AlgorithmId = algorithmId;
CreatedAt = createdAt;
ExpiresAt = expiresAt;
Kind = CryptoSigningKeyKind.Raw;
privateKeyBytes = privateKey.ToArray();
publicKeyBytes = publicKey.IsEmpty ? EmptyKey : publicKey.ToArray();
PrivateParameters = default;
PublicParameters = default;
Metadata = metadata is null
? EmptyMetadata
: new ReadOnlyDictionary<string, string?>(metadata.ToDictionary(
static pair => pair.Key,
static pair => pair.Value,
StringComparer.OrdinalIgnoreCase));
}
/// <summary>
/// Gets the key reference (id + provider hint).
/// </summary>
public CryptoKeyReference Reference { get; }
/// <summary>
/// Gets the algorithm identifier (e.g., ES256).
/// </summary>
public string AlgorithmId { get; }
/// <summary>
/// Gets the private EC parameters (cloned).
/// </summary>
public ECParameters PrivateParameters { get; }
/// <summary>
/// Gets the public EC parameters (cloned, no private scalar).
/// </summary>
public ECParameters PublicParameters { get; }
/// <summary>
/// Indicates the underlying key material representation.
/// </summary>
public CryptoSigningKeyKind Kind { get; }
/// <summary>
/// Gets the raw private key bytes when available (empty for EC-backed keys).
/// </summary>
public ReadOnlyMemory<byte> PrivateKey => privateKeyBytes;
/// <summary>
/// Gets the raw public key bytes when available (empty for EC-backed keys or when not supplied).
/// </summary>
public ReadOnlyMemory<byte> PublicKey => publicKeyBytes;
/// <summary>
/// Gets the timestamp when the key was created/imported.
/// </summary>
public DateTimeOffset CreatedAt { get; }
/// <summary>
/// Gets the optional expiry timestamp for the key.
/// </summary>
public DateTimeOffset? ExpiresAt { get; }
/// <summary>
/// Gets arbitrary metadata entries associated with the key.
/// </summary>
public IReadOnlyDictionary<string, string?> Metadata { get; }
private static ECParameters CloneParameters(ECParameters source, bool includePrivate)
{
var clone = new ECParameters
{
Curve = source.Curve,
Q = new ECPoint
{
X = source.Q.X is null ? null : (byte[])source.Q.X.Clone(),
Y = source.Q.Y is null ? null : (byte[])source.Q.Y.Clone()
}
};
if (includePrivate && source.D is not null)
{
clone.D = (byte[])source.D.Clone();
}
return clone;
}
}

View File

@@ -1,129 +1,133 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
namespace StellaOps.Cryptography;
/// <summary>
/// Default in-process crypto provider exposing password hashing capabilities.
/// </summary>
public sealed class DefaultCryptoProvider : ICryptoProvider
{
private readonly ConcurrentDictionary<string, IPasswordHasher> passwordHashers;
private readonly ConcurrentDictionary<string, CryptoSigningKey> signingKeys;
private static readonly HashSet<string> SupportedSigningAlgorithms = new(StringComparer.OrdinalIgnoreCase)
{
SignatureAlgorithms.Es256
};
public DefaultCryptoProvider()
{
passwordHashers = new ConcurrentDictionary<string, IPasswordHasher>(StringComparer.OrdinalIgnoreCase);
signingKeys = new ConcurrentDictionary<string, CryptoSigningKey>(StringComparer.Ordinal);
var argon = new Argon2idPasswordHasher();
var pbkdf2 = new Pbkdf2PasswordHasher();
passwordHashers.TryAdd(PasswordHashAlgorithm.Argon2id.ToString(), argon);
passwordHashers.TryAdd(PasswordHashAlgorithms.Argon2id, argon);
passwordHashers.TryAdd(PasswordHashAlgorithm.Pbkdf2.ToString(), pbkdf2);
passwordHashers.TryAdd(PasswordHashAlgorithms.Pbkdf2Sha256, pbkdf2);
}
public string Name => "default";
public bool Supports(CryptoCapability capability, string algorithmId)
{
if (string.IsNullOrWhiteSpace(algorithmId))
{
return false;
}
return capability switch
{
CryptoCapability.PasswordHashing => passwordHashers.ContainsKey(algorithmId),
CryptoCapability.Signing or CryptoCapability.Verification => SupportedSigningAlgorithms.Contains(algorithmId),
_ => false
};
}
public IPasswordHasher GetPasswordHasher(string algorithmId)
{
if (!Supports(CryptoCapability.PasswordHashing, algorithmId))
{
throw new InvalidOperationException($"Password hashing algorithm '{algorithmId}' is not supported by provider '{Name}'.");
}
return passwordHashers[algorithmId];
}
public ICryptoSigner GetSigner(string algorithmId, CryptoKeyReference keyReference)
{
ArgumentNullException.ThrowIfNull(keyReference);
if (!Supports(CryptoCapability.Signing, algorithmId))
{
throw new InvalidOperationException($"Signing algorithm '{algorithmId}' is not supported by provider '{Name}'.");
}
if (!signingKeys.TryGetValue(keyReference.KeyId, out var signingKey))
{
throw new KeyNotFoundException($"Signing key '{keyReference.KeyId}' is not registered with provider '{Name}'.");
}
if (!string.Equals(signingKey.AlgorithmId, algorithmId, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(
$"Signing key '{keyReference.KeyId}' is registered for algorithm '{signingKey.AlgorithmId}', not '{algorithmId}'.");
}
return EcdsaSigner.Create(signingKey);
}
public void UpsertSigningKey(CryptoSigningKey signingKey)
{
ArgumentNullException.ThrowIfNull(signingKey);
EnsureSigningSupported(signingKey.AlgorithmId);
ValidateSigningKey(signingKey);
signingKeys.AddOrUpdate(signingKey.Reference.KeyId, signingKey, (_, _) => signingKey);
}
public bool RemoveSigningKey(string keyId)
{
if (string.IsNullOrWhiteSpace(keyId))
{
return false;
}
return signingKeys.TryRemove(keyId, out _);
}
public IReadOnlyCollection<CryptoSigningKey> GetSigningKeys()
=> signingKeys.Values.ToArray();
private static void EnsureSigningSupported(string algorithmId)
{
if (!SupportedSigningAlgorithms.Contains(algorithmId))
{
throw new InvalidOperationException($"Signing algorithm '{algorithmId}' is not supported by provider 'default'.");
}
}
private static void ValidateSigningKey(CryptoSigningKey signingKey)
{
if (!string.Equals(signingKey.AlgorithmId, SignatureAlgorithms.Es256, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException($"Only ES256 signing keys are currently supported by provider 'default'.");
}
var expected = ECCurve.NamedCurves.nistP256;
var curve = signingKey.PrivateParameters.Curve;
if (!curve.IsNamed || !string.Equals(curve.Oid.Value, expected.Oid.Value, StringComparison.Ordinal))
{
throw new InvalidOperationException("ES256 signing keys must use the NIST P-256 curve.");
}
}
}
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
namespace StellaOps.Cryptography;
/// <summary>
/// Default in-process crypto provider exposing password hashing capabilities.
/// </summary>
public sealed class DefaultCryptoProvider : ICryptoProvider
{
private readonly ConcurrentDictionary<string, IPasswordHasher> passwordHashers;
private readonly ConcurrentDictionary<string, CryptoSigningKey> signingKeys;
private static readonly HashSet<string> SupportedSigningAlgorithms = new(StringComparer.OrdinalIgnoreCase)
{
SignatureAlgorithms.Es256
};
public DefaultCryptoProvider()
{
passwordHashers = new ConcurrentDictionary<string, IPasswordHasher>(StringComparer.OrdinalIgnoreCase);
signingKeys = new ConcurrentDictionary<string, CryptoSigningKey>(StringComparer.Ordinal);
var argon = new Argon2idPasswordHasher();
var pbkdf2 = new Pbkdf2PasswordHasher();
passwordHashers.TryAdd(PasswordHashAlgorithm.Argon2id.ToString(), argon);
passwordHashers.TryAdd(PasswordHashAlgorithms.Argon2id, argon);
passwordHashers.TryAdd(PasswordHashAlgorithm.Pbkdf2.ToString(), pbkdf2);
passwordHashers.TryAdd(PasswordHashAlgorithms.Pbkdf2Sha256, pbkdf2);
}
public string Name => "default";
public bool Supports(CryptoCapability capability, string algorithmId)
{
if (string.IsNullOrWhiteSpace(algorithmId))
{
return false;
}
return capability switch
{
CryptoCapability.PasswordHashing => passwordHashers.ContainsKey(algorithmId),
CryptoCapability.Signing or CryptoCapability.Verification => SupportedSigningAlgorithms.Contains(algorithmId),
_ => false
};
}
public IPasswordHasher GetPasswordHasher(string algorithmId)
{
if (!Supports(CryptoCapability.PasswordHashing, algorithmId))
{
throw new InvalidOperationException($"Password hashing algorithm '{algorithmId}' is not supported by provider '{Name}'.");
}
return passwordHashers[algorithmId];
}
public ICryptoSigner GetSigner(string algorithmId, CryptoKeyReference keyReference)
{
ArgumentNullException.ThrowIfNull(keyReference);
if (!Supports(CryptoCapability.Signing, algorithmId))
{
throw new InvalidOperationException($"Signing algorithm '{algorithmId}' is not supported by provider '{Name}'.");
}
if (!signingKeys.TryGetValue(keyReference.KeyId, out var signingKey))
{
throw new KeyNotFoundException($"Signing key '{keyReference.KeyId}' is not registered with provider '{Name}'.");
}
if (!string.Equals(signingKey.AlgorithmId, algorithmId, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(
$"Signing key '{keyReference.KeyId}' is registered for algorithm '{signingKey.AlgorithmId}', not '{algorithmId}'.");
}
return EcdsaSigner.Create(signingKey);
}
public void UpsertSigningKey(CryptoSigningKey signingKey)
{
ArgumentNullException.ThrowIfNull(signingKey);
EnsureSigningSupported(signingKey.AlgorithmId);
if (signingKey.Kind != CryptoSigningKeyKind.Ec)
{
throw new InvalidOperationException($"Provider '{Name}' only accepts EC signing keys.");
}
ValidateSigningKey(signingKey);
signingKeys.AddOrUpdate(signingKey.Reference.KeyId, signingKey, (_, _) => signingKey);
}
public bool RemoveSigningKey(string keyId)
{
if (string.IsNullOrWhiteSpace(keyId))
{
return false;
}
return signingKeys.TryRemove(keyId, out _);
}
public IReadOnlyCollection<CryptoSigningKey> GetSigningKeys()
=> signingKeys.Values.ToArray();
private static void EnsureSigningSupported(string algorithmId)
{
if (!SupportedSigningAlgorithms.Contains(algorithmId))
{
throw new InvalidOperationException($"Signing algorithm '{algorithmId}' is not supported by provider 'default'.");
}
}
private static void ValidateSigningKey(CryptoSigningKey signingKey)
{
if (!string.Equals(signingKey.AlgorithmId, SignatureAlgorithms.Es256, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException($"Only ES256 signing keys are currently supported by provider 'default'.");
}
var expected = ECCurve.NamedCurves.nistP256;
var curve = signingKey.PrivateParameters.Curve;
if (!curve.IsNamed || !string.Equals(curve.Oid.Value, expected.Oid.Value, StringComparison.Ordinal))
{
throw new InvalidOperationException("ES256 signing keys must use the NIST P-256 curve.");
}
}
}

View File

@@ -64,6 +64,10 @@ public sealed class LibsodiumCryptoProvider : ICryptoProvider
{
ArgumentNullException.ThrowIfNull(signingKey);
EnsureAlgorithmSupported(signingKey.AlgorithmId);
if (signingKey.Kind != CryptoSigningKeyKind.Ec)
{
throw new InvalidOperationException($"Provider '{Name}' only accepts EC signing keys.");
}
signingKeys.AddOrUpdate(signingKey.Reference.KeyId, signingKey, (_, _) => signingKey);
}

View File

@@ -1,11 +1,13 @@
namespace StellaOps.Cryptography;
/// <summary>
/// Known signature algorithm identifiers.
/// </summary>
public static class SignatureAlgorithms
{
public const string Es256 = "ES256";
public const string Es384 = "ES384";
public const string Es512 = "ES512";
}
namespace StellaOps.Cryptography;
/// <summary>
/// Known signature algorithm identifiers.
/// </summary>
public static class SignatureAlgorithms
{
public const string Es256 = "ES256";
public const string Es384 = "ES384";
public const string Es512 = "ES512";
public const string Ed25519 = "ED25519";
public const string EdDsa = "EdDSA";
}

View File

@@ -1,44 +1,51 @@
# Team 8 — Security Guild Task Board (UTC 2025-10-10)
| ID | Status | Owner | Description | Dependencies | Exit Criteria |
|----|--------|-------|-------------|--------------|---------------|
| SEC1.A | DONE (2025-10-11) | Security Guild | Introduce `Argon2idPasswordHasher` backed by Konscious defaults. Wire options into `StandardPluginOptions` (`PasswordHashOptions`) and `StellaOpsAuthorityOptions.Security.PasswordHashing`. | PLG3, CORE3 | ✅ Hashes emit PHC string `$argon2id$v=19$m=19456,t=2,p=1$...`; ✅ `NeedsRehash` promotes PBKDF2 → Argon2; ✅ Integration tests cover tamper, legacy rehash, perf p95 &lt; 250ms. |
| SEC1.B | DONE (2025-10-12) | Security Guild | Add compile-time switch to enable libsodium/Core variants later (`STELLAOPS_CRYPTO_SODIUM`). Document build variable. | SEC1.A | ✅ Conditional compilation path compiles; ✅ README snippet in `docs/security/password-hashing.md`. |
| SEC2.A | DONE (2025-10-13) | Security Guild + Core | Define audit event contract (`AuthEventRecord`) with subject/client/scope/IP/outcome/correlationId and PII tags. | CORE5CORE7 | ✅ Contract shipped in `StellaOps.Cryptography` (or shared abstractions); ✅ Docs in `docs/security/audit-events.md`. |
| SEC2.B | DONE (2025-10-13) | Security Guild | Emit audit records from OpenIddict handlers (password + client creds) and bootstrap APIs. Persist via `IAuthorityLoginAttemptStore`. | SEC2.A | ✅ Tests assert three flows (success/failure/lockout); ✅ Serilog output contains correlationId + PII tagging; ✅ Mongo store holds summary rows. |
| SEC3.A | DONE (2025-10-12) | Security Guild + Core | Configure ASP.NET rate limiter (`AddRateLimiter`) with fixed-window policy keyed by IP + `client_id`. Apply to `/token` and `/internal/*`. | CORE8 completion | ✅ Middleware active; ✅ Configurable limits via options; ✅ Integration test hits 429. |
| SEC3.B | DONE (2025-10-13) | Security Guild | Document lockout + rate-limit tuning guidance and escalation thresholds. | SEC3.A | ✅ Section in `docs/security/rate-limits.md`; ✅ Includes SOC alert recommendations. |
| SEC4.A | DONE (2025-10-12) | Security Guild + DevOps | Define revocation JSON schema (`revocation_bundle.schema.json`) and detached JWS workflow. | CORE9, OPS3 | ✅ Schema + sample committed; ✅ CLI command `stellaops auth revoke export` scaffolded with acceptance tests; ✅ Verification script + docs. |
| SEC4.B | DONE (2025-10-12) | Security Guild | Integrate signing keys with crypto provider abstraction (initially ES256 via BCL). | SEC4.A, D5 | ✅ `ICryptoProvider.GetSigner` stub + default BCL signer; ✅ Unit tests verifying signature roundtrip. |
| SEC5.A | DONE (2025-10-12) | Security Guild | Author STRIDE threat model (`docs/security/authority-threat-model.md`) covering token, bootstrap, revocation, CLI, plugin surfaces. | All SEC1SEC4 in progress | ✅ DFDs + trust boundaries drawn; ✅ Risk table with owners/actions; ✅ Follow-up backlog issues created. |
| SEC5.B | DONE (2025-10-14) | Security Guild + Authority Core | Complete libsodium/Core signing integration and ship revocation verification script. | SEC4.A, SEC4.B, SEC4.HOST | ✅ libsodium/Core signing provider wired; ✅ `stellaops auth revoke verify` script published; ✅ Revocation docs updated with verification workflow. |
| SEC5.B1 | DONE (2025-10-14) | Security Guild + Authority Core | Introduce `LibsodiumCryptoProvider` implementing ECDSA signing/verification via libsodium, register under feature flag, and validate against existing ES256 fixtures. | SEC5.B | ✅ Provider resolves via `ICryptoProviderRegistry`; ✅ Integration tests cover sign/verify parity with default provider; ✅ Fallback to managed provider documented. |
| SEC5.B2 | DONE (2025-10-14) | Security Guild + DevEx/CLI | Extend `stellaops auth revoke verify` to detect provider metadata, reuse registry for verification, and document CLI workflow. | SEC5.B | ✅ CLI uses registry signers for verification; ✅ End-to-end test invokes verify against sample bundle; ✅ docs/11_AUTHORITY.md references CLI procedure. |
| SEC5.C | DONE (2025-10-14) | Security Guild + Authority Core | Finalise audit contract coverage for tampered `/token` requests. | SEC2.A, SEC2.B | ✅ Tamper attempts logged with correlationId/PII tags; ✅ SOC runbook updated; ✅ Threat model status reviewed. |
| SEC5.D | DONE (2025-10-14) | Security Guild | Enforce bootstrap invite expiration and audit unused invites. | SEC5.A | ✅ Bootstrap tokens auto-expire; ✅ Audit entries emitted for expiration/reuse attempts; ✅ Operator docs updated. |
> Remark (2025-10-14): Cleanup service wired to store; background sweep + invite audit tests added.
| SEC5.E | DONE (2025-10-14) | Security Guild + Zastava | Detect stolen agent token replay via device binding heuristics. | SEC4.A | ✅ Device binding guidance published; ✅ Alerting pipeline raises stale revocation acknowledgements; ✅ Tests cover replay detection. |
> Remark (2025-10-14): Token usage metadata persisted with replay audits + handler/unit coverage.
| SEC5.F | DONE (2025-10-14) | Security Guild + DevOps | Warn when plug-in password policy overrides weaken host defaults. | SEC1.A, PLG3 | ✅ Static analyser flags weaker overrides; ✅ Runtime warning surfaced; ✅ Docs call out mitigation. |
> Remark (2025-10-14): Analyzer surfaces warnings during CLI load; docs updated with mitigation steps.
| SEC5.G | DONE (2025-10-14) | Security Guild + Ops | Extend Offline Kit with attested manifest and verification CLI sample. | OPS3 | ✅ Offline Kit build signs manifest with detached JWS; ✅ Verification CLI documented; ✅ Supply-chain attestation recorded. |
> Remark (2025-10-14): Offline kit docs include manifest verification workflow; attestation artifacts referenced.
| SEC5.H | DONE (2025-10-13) | Security Guild + Authority Core | Ensure `/token` denials persist audit records with correlation IDs. | SEC2.A, SEC2.B | ✅ Audit store captures denials; ✅ Tests cover success/failure/lockout; ✅ Threat model review updated. |
| D5.A | DONE (2025-10-12) | Security Guild | Flesh out `StellaOps.Cryptography` provider registry, policy, and DI helpers enabling sovereign crypto selection. | SEC1.A, SEC4.B | ✅ `ICryptoProviderRegistry` implementation with provider selection rules; ✅ `StellaOps.Cryptography.DependencyInjection` extensions; ✅ Tests covering fallback ordering. |
> Remark (2025-10-13, SEC2.B): Coordinated with Authority Core — audit sinks now receive `/token` success/failure events; awaiting host test suite once signing fixture lands.
>
> Remark (2025-10-13, SEC3.B): Pinged Docs & Plugin guilds — rate limit guidance published in `docs/security/rate-limits.md` and flagged for PLG6.DOC copy lift.
>
> Remark (2025-10-13, SEC5.B): Split follow-up into SEC5.B1 (libsodium provider) and SEC5.B2 (CLI verification) after scoping registry integration; work not yet started.
## Notes
- Target Argon2 parameters follow OWASP Cheat Sheet (memory ≈ 19MiB, iterations 2, parallelism 1). Allow overrides via configuration.
- When CORE8 lands, pair with Team 2 to expose request context information required by the rate limiter (client_id enrichment).
- Revocation bundle must be consumable offline; include issue timestamp, signing key metadata, and reasons.
- All crypto usage in Authority code should funnel through the new abstractions (`ICryptoProvider`), enabling future CryptoPro/OpenSSL providers.
## Done Definition
- Code merges include unit/integration tests and documentation updates.
- `TASKS.md` status transitions (TODO → DOING → DONE/BLOCKED) must happen in the same PR as the work.
- Prior to marking DONE: run `dotnet test` for touched solutions and attach excerpt to PR description.
# Team 8 — Security Guild Task Board (UTC 2025-10-10)
| ID | Status | Owner | Description | Dependencies | Exit Criteria |
|----|--------|-------|-------------|--------------|---------------|
| SEC1.A | DONE (2025-10-11) | Security Guild | Introduce `Argon2idPasswordHasher` backed by Konscious defaults. Wire options into `StandardPluginOptions` (`PasswordHashOptions`) and `StellaOpsAuthorityOptions.Security.PasswordHashing`. | PLG3, CORE3 | ✅ Hashes emit PHC string `$argon2id$v=19$m=19456,t=2,p=1$...`; ✅ `NeedsRehash` promotes PBKDF2 → Argon2; ✅ Integration tests cover tamper, legacy rehash, perf p95 &lt; 250ms. |
| SEC1.B | DONE (2025-10-12) | Security Guild | Add compile-time switch to enable libsodium/Core variants later (`STELLAOPS_CRYPTO_SODIUM`). Document build variable. | SEC1.A | ✅ Conditional compilation path compiles; ✅ README snippet in `docs/security/password-hashing.md`. |
| SEC2.A | DONE (2025-10-13) | Security Guild + Core | Define audit event contract (`AuthEventRecord`) with subject/client/scope/IP/outcome/correlationId and PII tags. | CORE5CORE7 | ✅ Contract shipped in `StellaOps.Cryptography` (or shared abstractions); ✅ Docs in `docs/security/audit-events.md`. |
| SEC2.B | DONE (2025-10-13) | Security Guild | Emit audit records from OpenIddict handlers (password + client creds) and bootstrap APIs. Persist via `IAuthorityLoginAttemptStore`. | SEC2.A | ✅ Tests assert three flows (success/failure/lockout); ✅ Serilog output contains correlationId + PII tagging; ✅ Mongo store holds summary rows. |
| SEC3.A | DONE (2025-10-12) | Security Guild + Core | Configure ASP.NET rate limiter (`AddRateLimiter`) with fixed-window policy keyed by IP + `client_id`. Apply to `/token` and `/internal/*`. | CORE8 completion | ✅ Middleware active; ✅ Configurable limits via options; ✅ Integration test hits 429. |
| SEC3.B | DONE (2025-10-13) | Security Guild | Document lockout + rate-limit tuning guidance and escalation thresholds. | SEC3.A | ✅ Section in `docs/security/rate-limits.md`; ✅ Includes SOC alert recommendations. |
| SEC4.A | DONE (2025-10-12) | Security Guild + DevOps | Define revocation JSON schema (`revocation_bundle.schema.json`) and detached JWS workflow. | CORE9, OPS3 | ✅ Schema + sample committed; ✅ CLI command `stellaops auth revoke export` scaffolded with acceptance tests; ✅ Verification script + docs. |
| SEC4.B | DONE (2025-10-12) | Security Guild | Integrate signing keys with crypto provider abstraction (initially ES256 via BCL). | SEC4.A, D5 | ✅ `ICryptoProvider.GetSigner` stub + default BCL signer; ✅ Unit tests verifying signature roundtrip. |
| SEC5.A | DONE (2025-10-12) | Security Guild | Author STRIDE threat model (`docs/security/authority-threat-model.md`) covering token, bootstrap, revocation, CLI, plugin surfaces. | All SEC1SEC4 in progress | ✅ DFDs + trust boundaries drawn; ✅ Risk table with owners/actions; ✅ Follow-up backlog issues created. |
| SEC5.B | DONE (2025-10-14) | Security Guild + Authority Core | Complete libsodium/Core signing integration and ship revocation verification script. | SEC4.A, SEC4.B, SEC4.HOST | ✅ libsodium/Core signing provider wired; ✅ `stellaops auth revoke verify` script published; ✅ Revocation docs updated with verification workflow. |
| SEC5.B1 | DONE (2025-10-14) | Security Guild + Authority Core | Introduce `LibsodiumCryptoProvider` implementing ECDSA signing/verification via libsodium, register under feature flag, and validate against existing ES256 fixtures. | SEC5.B | ✅ Provider resolves via `ICryptoProviderRegistry`; ✅ Integration tests cover sign/verify parity with default provider; ✅ Fallback to managed provider documented. |
| SEC5.B2 | DONE (2025-10-14) | Security Guild + DevEx/CLI | Extend `stellaops auth revoke verify` to detect provider metadata, reuse registry for verification, and document CLI workflow. | SEC5.B | ✅ CLI uses registry signers for verification; ✅ End-to-end test invokes verify against sample bundle; ✅ docs/11_AUTHORITY.md references CLI procedure. |
| SEC5.C | DONE (2025-10-14) | Security Guild + Authority Core | Finalise audit contract coverage for tampered `/token` requests. | SEC2.A, SEC2.B | ✅ Tamper attempts logged with correlationId/PII tags; ✅ SOC runbook updated; ✅ Threat model status reviewed. |
| SEC5.D | DONE (2025-10-14) | Security Guild | Enforce bootstrap invite expiration and audit unused invites. | SEC5.A | ✅ Bootstrap tokens auto-expire; ✅ Audit entries emitted for expiration/reuse attempts; ✅ Operator docs updated. |
> Remark (2025-10-14): Cleanup service wired to store; background sweep + invite audit tests added.
| SEC5.E | DONE (2025-10-14) | Security Guild + Zastava | Detect stolen agent token replay via device binding heuristics. | SEC4.A | ✅ Device binding guidance published; ✅ Alerting pipeline raises stale revocation acknowledgements; ✅ Tests cover replay detection. |
> Remark (2025-10-14): Token usage metadata persisted with replay audits + handler/unit coverage.
| SEC5.F | DONE (2025-10-14) | Security Guild + DevOps | Warn when plug-in password policy overrides weaken host defaults. | SEC1.A, PLG3 | ✅ Static analyser flags weaker overrides; ✅ Runtime warning surfaced; ✅ Docs call out mitigation. |
> Remark (2025-10-14): Analyzer surfaces warnings during CLI load; docs updated with mitigation steps.
| SEC5.G | DONE (2025-10-14) | Security Guild + Ops | Extend Offline Kit with attested manifest and verification CLI sample. | OPS3 | ✅ Offline Kit build signs manifest with detached JWS; ✅ Verification CLI documented; ✅ Supply-chain attestation recorded. |
> Remark (2025-10-14): Offline kit docs include manifest verification workflow; attestation artifacts referenced.
| SEC5.H | DONE (2025-10-13) | Security Guild + Authority Core | Ensure `/token` denials persist audit records with correlation IDs. | SEC2.A, SEC2.B | ✅ Audit store captures denials; ✅ Tests cover success/failure/lockout; ✅ Threat model review updated. |
| D5.A | DONE (2025-10-12) | Security Guild | Flesh out `StellaOps.Cryptography` provider registry, policy, and DI helpers enabling sovereign crypto selection. | SEC1.A, SEC4.B | ✅ `ICryptoProviderRegistry` implementation with provider selection rules; ✅ `StellaOps.Cryptography.DependencyInjection` extensions; ✅ Tests covering fallback ordering. |
| SEC6.A | DONE (2025-10-19) | Security Guild | Ship BouncyCastle-backed Ed25519 signing as a `StellaOps.Cryptography` plug-in and migrate Scanner WebService signing to consume the provider registry; codify the plug-in rule in AGENTS.<br>2025-10-19: Added `StellaOps.Cryptography.Plugin.BouncyCastle`, updated DI and ReportSigner, captured provider tests (`BouncyCastleEd25519CryptoProviderTests`). | D5.A | ✅ Plug-in registered via DI (`AddStellaOpsCrypto` + `AddBouncyCastleEd25519Provider`); ✅ Report signer resolves keys through registry; ✅ Unit tests cover Ed25519 sign/verify via provider. |
> Remark (2025-10-13, SEC2.B): Coordinated with Authority Core — audit sinks now receive `/token` success/failure events; awaiting host test suite once signing fixture lands.
>
> Remark (2025-10-13, SEC3.B): Pinged Docs & Plugin guilds — rate limit guidance published in `docs/security/rate-limits.md` and flagged for PLG6.DOC copy lift.
>
> Remark (2025-10-13, SEC5.B): Split follow-up into SEC5.B1 (libsodium provider) and SEC5.B2 (CLI verification) after scoping registry integration; work not yet started.
> Remark (2025-10-13, SEC2.B): Coordinated with Authority Core — audit sinks now receive `/token` success/failure events; awaiting host test suite once signing fixture lands.
>
> Remark (2025-10-13, SEC3.B): Pinged Docs & Plugin guilds — rate limit guidance published in `docs/security/rate-limits.md` and flagged for PLG6.DOC copy lift.
>
> Remark (2025-10-13, SEC5.B): Split follow-up into SEC5.B1 (libsodium provider) and SEC5.B2 (CLI verification) after scoping registry integration; work not yet started.
## Notes
- Target Argon2 parameters follow OWASP Cheat Sheet (memory ≈ 19MiB, iterations 2, parallelism 1). Allow overrides via configuration.
- When CORE8 lands, pair with Team 2 to expose request context information required by the rate limiter (client_id enrichment).
- Revocation bundle must be consumable offline; include issue timestamp, signing key metadata, and reasons.
- All crypto usage in Authority code should funnel through the new abstractions (`ICryptoProvider`), enabling future CryptoPro/OpenSSL providers.
## Done Definition
- Code merges include unit/integration tests and documentation updates.
- `TASKS.md` status transitions (TODO → DOING → DONE/BLOCKED) must happen in the same PR as the work.
- Prior to marking DONE: run `dotnet test` for touched solutions and attach excerpt to PR description.