Resolve Concelier/Excititor merge conflicts
This commit is contained in:
21
src/StellaOps.Signer/AGENTS.md
Normal file
21
src/StellaOps.Signer/AGENTS.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Signer Guild
|
||||
|
||||
## Mission
|
||||
Operate the Stella Ops Signer service: authenticate trusted callers, enforce proof‑of‑entitlement and release integrity policy, and mint verifiable DSSE bundles (keyless or KMS-backed) for downstream attestation.
|
||||
|
||||
## Teams On Call
|
||||
- Team 11 (Signer API)
|
||||
- Team 12 (Signer Reliability & Quotas)
|
||||
|
||||
## Operating Principles
|
||||
- Accept requests only with Authority-issued OpToks plus DPoP or mTLS sender binding; reject unsigned/cross-tenant traffic.
|
||||
- Treat PoE claims as hard gates for quota, version windows, and license validity; cache results deterministically with bounded TTLs.
|
||||
- Verify scanner image release signatures via OCI Referrers before signing; fail closed on ambiguity.
|
||||
- Keep the hot path stateless and deterministic; persist audit trails with structured logging, metrics, and correlation IDs.
|
||||
- Update `TASKS.md`, architecture notes, and tests whenever behaviour or contracts evolve.
|
||||
|
||||
## Key Directories
|
||||
- `src/StellaOps.Signer/StellaOps.Signer.WebService/` — Minimal API host and HTTP surface (to be scaffolded).
|
||||
- `src/StellaOps.Signer/StellaOps.Signer.Core/` — Domain contracts, signing pipeline, quota enforcement (to be scaffolded).
|
||||
- `src/StellaOps.Signer/StellaOps.Signer.Infrastructure/` — External clients (Authority, Licensing, Fulcio/KMS, OCI) and persistence (to be scaffolded).
|
||||
- `src/StellaOps.Signer/StellaOps.Signer.Tests/` — Unit/integration test suites (to be scaffolded).
|
||||
@@ -0,0 +1,55 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StellaOps.Signer.Core;
|
||||
|
||||
public interface IProofOfEntitlementIntrospector
|
||||
{
|
||||
ValueTask<ProofOfEntitlementResult> IntrospectAsync(
|
||||
ProofOfEntitlement proof,
|
||||
CallerContext caller,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public interface IReleaseIntegrityVerifier
|
||||
{
|
||||
ValueTask<ReleaseVerificationResult> VerifyAsync(
|
||||
string scannerImageDigest,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public interface ISignerQuotaService
|
||||
{
|
||||
ValueTask EnsureWithinLimitsAsync(
|
||||
SigningRequest request,
|
||||
ProofOfEntitlementResult entitlement,
|
||||
CallerContext caller,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public interface IDsseSigner
|
||||
{
|
||||
ValueTask<SigningBundle> SignAsync(
|
||||
SigningRequest request,
|
||||
ProofOfEntitlementResult entitlement,
|
||||
CallerContext caller,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public interface ISignerAuditSink
|
||||
{
|
||||
ValueTask<string> WriteAsync(
|
||||
SigningRequest request,
|
||||
SigningBundle bundle,
|
||||
ProofOfEntitlementResult entitlement,
|
||||
CallerContext caller,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public interface ISignerPipeline
|
||||
{
|
||||
ValueTask<SigningOutcome> SignAsync(
|
||||
SigningRequest request,
|
||||
CallerContext caller,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
105
src/StellaOps.Signer/StellaOps.Signer.Core/SignerContracts.cs
Normal file
105
src/StellaOps.Signer/StellaOps.Signer.Core/SignerContracts.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Signer.Core;
|
||||
|
||||
public enum SignerPoEFormat
|
||||
{
|
||||
Jwt,
|
||||
Mtls,
|
||||
}
|
||||
|
||||
public enum SigningMode
|
||||
{
|
||||
Keyless,
|
||||
Kms,
|
||||
}
|
||||
|
||||
public sealed record SigningSubject(
|
||||
string Name,
|
||||
IReadOnlyDictionary<string, string> Digest);
|
||||
|
||||
public sealed record ProofOfEntitlement(
|
||||
SignerPoEFormat Format,
|
||||
string Value);
|
||||
|
||||
public sealed record SigningOptions(
|
||||
SigningMode Mode,
|
||||
int? ExpirySeconds,
|
||||
string ReturnBundle);
|
||||
|
||||
public sealed record SigningRequest(
|
||||
IReadOnlyList<SigningSubject> Subjects,
|
||||
string PredicateType,
|
||||
JsonDocument Predicate,
|
||||
string ScannerImageDigest,
|
||||
ProofOfEntitlement ProofOfEntitlement,
|
||||
SigningOptions Options);
|
||||
|
||||
public sealed record CallerContext(
|
||||
string Subject,
|
||||
string Tenant,
|
||||
IReadOnlyList<string> Scopes,
|
||||
IReadOnlyList<string> Audiences,
|
||||
string? SenderBinding,
|
||||
string? ClientCertificateThumbprint);
|
||||
|
||||
public sealed record ProofOfEntitlementResult(
|
||||
string LicenseId,
|
||||
string CustomerId,
|
||||
string Plan,
|
||||
int MaxArtifactBytes,
|
||||
int QpsLimit,
|
||||
int QpsRemaining,
|
||||
DateTimeOffset ExpiresAtUtc);
|
||||
|
||||
public sealed record ReleaseVerificationResult(
|
||||
bool Trusted,
|
||||
string? ReleaseSigner);
|
||||
|
||||
public sealed record SigningIdentity(
|
||||
string Mode,
|
||||
string Issuer,
|
||||
string Subject,
|
||||
DateTimeOffset? ExpiresAtUtc);
|
||||
|
||||
public sealed record SigningMetadata(
|
||||
SigningIdentity Identity,
|
||||
IReadOnlyList<string> CertificateChain,
|
||||
string ProviderName,
|
||||
string AlgorithmId);
|
||||
|
||||
public sealed record SigningBundle(
|
||||
DsseEnvelope Envelope,
|
||||
SigningMetadata Metadata);
|
||||
|
||||
public sealed record PolicyCounters(
|
||||
string Plan,
|
||||
int MaxArtifactBytes,
|
||||
int QpsRemaining);
|
||||
|
||||
public sealed record SigningOutcome(
|
||||
SigningBundle Bundle,
|
||||
PolicyCounters Policy,
|
||||
string AuditId);
|
||||
|
||||
public sealed record SignerAuditEntry(
|
||||
string AuditId,
|
||||
DateTimeOffset TimestampUtc,
|
||||
string Subject,
|
||||
string Tenant,
|
||||
string Plan,
|
||||
string ScannerImageDigest,
|
||||
string SigningMode,
|
||||
string ProviderName,
|
||||
IReadOnlyList<SigningSubject> Subjects);
|
||||
|
||||
public sealed record DsseEnvelope(
|
||||
string Payload,
|
||||
string PayloadType,
|
||||
IReadOnlyList<DsseSignature> Signatures);
|
||||
|
||||
public sealed record DsseSignature(
|
||||
string Signature,
|
||||
string? KeyId);
|
||||
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
|
||||
namespace StellaOps.Signer.Core;
|
||||
|
||||
public abstract class SignerException : Exception
|
||||
{
|
||||
protected SignerException(string code, string message)
|
||||
: base(message)
|
||||
{
|
||||
Code = code;
|
||||
}
|
||||
|
||||
public string Code { get; }
|
||||
}
|
||||
|
||||
public sealed class SignerValidationException : SignerException
|
||||
{
|
||||
public SignerValidationException(string code, string message)
|
||||
: base(code, message)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SignerAuthorizationException : SignerException
|
||||
{
|
||||
public SignerAuthorizationException(string code, string message)
|
||||
: base(code, message)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SignerReleaseVerificationException : SignerException
|
||||
{
|
||||
public SignerReleaseVerificationException(string code, string message)
|
||||
: base(code, message)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SignerQuotaException : SignerException
|
||||
{
|
||||
public SignerQuotaException(string code, string message)
|
||||
: base(code, message)
|
||||
{
|
||||
}
|
||||
}
|
||||
147
src/StellaOps.Signer/StellaOps.Signer.Core/SignerPipeline.cs
Normal file
147
src/StellaOps.Signer/StellaOps.Signer.Core/SignerPipeline.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StellaOps.Signer.Core;
|
||||
|
||||
public sealed class SignerPipeline : ISignerPipeline
|
||||
{
|
||||
private const string RequiredScope = "signer.sign";
|
||||
private const string RequiredAudience = "signer";
|
||||
|
||||
private readonly IProofOfEntitlementIntrospector _poe;
|
||||
private readonly IReleaseIntegrityVerifier _releaseVerifier;
|
||||
private readonly ISignerQuotaService _quotaService;
|
||||
private readonly IDsseSigner _signer;
|
||||
private readonly ISignerAuditSink _auditSink;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public SignerPipeline(
|
||||
IProofOfEntitlementIntrospector poe,
|
||||
IReleaseIntegrityVerifier releaseVerifier,
|
||||
ISignerQuotaService quotaService,
|
||||
IDsseSigner signer,
|
||||
ISignerAuditSink auditSink,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
_poe = poe ?? throw new ArgumentNullException(nameof(poe));
|
||||
_releaseVerifier = releaseVerifier ?? throw new ArgumentNullException(nameof(releaseVerifier));
|
||||
_quotaService = quotaService ?? throw new ArgumentNullException(nameof(quotaService));
|
||||
_signer = signer ?? throw new ArgumentNullException(nameof(signer));
|
||||
_auditSink = auditSink ?? throw new ArgumentNullException(nameof(auditSink));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public async ValueTask<SigningOutcome> SignAsync(
|
||||
SigningRequest request,
|
||||
CallerContext caller,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
ArgumentNullException.ThrowIfNull(caller);
|
||||
|
||||
ValidateCaller(caller);
|
||||
ValidateRequest(request);
|
||||
|
||||
var entitlement = await _poe
|
||||
.IntrospectAsync(request.ProofOfEntitlement, caller, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (entitlement.ExpiresAtUtc <= _timeProvider.GetUtcNow())
|
||||
{
|
||||
throw new SignerAuthorizationException("entitlement_denied", "Proof of entitlement is expired.");
|
||||
}
|
||||
|
||||
var releaseResult = await _releaseVerifier
|
||||
.VerifyAsync(request.ScannerImageDigest, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (!releaseResult.Trusted)
|
||||
{
|
||||
throw new SignerReleaseVerificationException("release_untrusted", "Scanner image digest failed release verification.");
|
||||
}
|
||||
|
||||
await _quotaService
|
||||
.EnsureWithinLimitsAsync(request, entitlement, caller, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var bundle = await _signer
|
||||
.SignAsync(request, entitlement, caller, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var auditId = await _auditSink
|
||||
.WriteAsync(request, bundle, entitlement, caller, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var outcome = new SigningOutcome(
|
||||
bundle,
|
||||
new PolicyCounters(entitlement.Plan, entitlement.MaxArtifactBytes, entitlement.QpsRemaining),
|
||||
auditId);
|
||||
return outcome;
|
||||
}
|
||||
|
||||
private static void ValidateCaller(CallerContext caller)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(caller.Subject))
|
||||
{
|
||||
throw new SignerAuthorizationException("invalid_caller", "Caller subject is required.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(caller.Tenant))
|
||||
{
|
||||
throw new SignerAuthorizationException("invalid_caller", "Caller tenant is required.");
|
||||
}
|
||||
|
||||
if (!caller.Scopes.Contains(RequiredScope, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new SignerAuthorizationException("insufficient_scope", $"Scope '{RequiredScope}' is required.");
|
||||
}
|
||||
|
||||
if (!caller.Audiences.Contains(RequiredAudience, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new SignerAuthorizationException("invalid_audience", $"Audience '{RequiredAudience}' is required.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateRequest(SigningRequest request)
|
||||
{
|
||||
if (request.Subjects.Count == 0)
|
||||
{
|
||||
throw new SignerValidationException("subject_missing", "At least one subject must be provided.");
|
||||
}
|
||||
|
||||
foreach (var subject in request.Subjects)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(subject.Name))
|
||||
{
|
||||
throw new SignerValidationException("subject_invalid", "Subject name is required.");
|
||||
}
|
||||
|
||||
if (subject.Digest is null || subject.Digest.Count == 0)
|
||||
{
|
||||
throw new SignerValidationException("subject_digest_invalid", "Subject digest is required.");
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.PredicateType))
|
||||
{
|
||||
throw new SignerValidationException("predicate_type_missing", "Predicate type is required.");
|
||||
}
|
||||
|
||||
if (request.Predicate is null || request.Predicate.RootElement.ValueKind == JsonValueKind.Undefined)
|
||||
{
|
||||
throw new SignerValidationException("predicate_missing", "Predicate payload is required.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.ScannerImageDigest))
|
||||
{
|
||||
throw new SignerValidationException("scanner_digest_missing", "Scanner image digest is required.");
|
||||
}
|
||||
|
||||
if (request.ProofOfEntitlement is null)
|
||||
{
|
||||
throw new SignerValidationException("poe_missing", "Proof of entitlement is required.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Signer.Core;
|
||||
|
||||
public static class SignerStatementBuilder
|
||||
{
|
||||
private const string StatementType = "https://in-toto.io/Statement/v0.1";
|
||||
|
||||
public static byte[] BuildStatementPayload(SigningRequest request)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var subjects = new List<object>(request.Subjects.Count);
|
||||
foreach (var subject in request.Subjects)
|
||||
{
|
||||
var digest = new SortedDictionary<string, string>(StringComparer.Ordinal);
|
||||
foreach (var kvp in subject.Digest)
|
||||
{
|
||||
digest[kvp.Key.ToLowerInvariant()] = kvp.Value;
|
||||
}
|
||||
|
||||
subjects.Add(new
|
||||
{
|
||||
name = subject.Name,
|
||||
digest
|
||||
});
|
||||
}
|
||||
|
||||
var statement = new
|
||||
{
|
||||
_type = StatementType,
|
||||
predicateType = request.PredicateType,
|
||||
subject = subjects,
|
||||
predicate = request.Predicate.RootElement.Clone()
|
||||
};
|
||||
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = null,
|
||||
WriteIndented = false,
|
||||
};
|
||||
options.Converters.Add(new JsonElementConverter());
|
||||
return JsonSerializer.SerializeToUtf8Bytes(statement, options);
|
||||
}
|
||||
|
||||
private sealed class JsonElementConverter : System.Text.Json.Serialization.JsonConverter<JsonElement>
|
||||
{
|
||||
public override JsonElement Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
using var document = JsonDocument.ParseValue(ref reader);
|
||||
return document.RootElement.Clone();
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, JsonElement value, JsonSerializerOptions options)
|
||||
{
|
||||
value.WriteTo(writer);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Signer.Core;
|
||||
|
||||
namespace StellaOps.Signer.Infrastructure.Auditing;
|
||||
|
||||
public sealed class InMemorySignerAuditSink : ISignerAuditSink
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, SignerAuditEntry> _entries = new(StringComparer.Ordinal);
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ILogger<InMemorySignerAuditSink> _logger;
|
||||
|
||||
public InMemorySignerAuditSink(TimeProvider timeProvider, ILogger<InMemorySignerAuditSink> logger)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public ValueTask<string> WriteAsync(
|
||||
SigningRequest request,
|
||||
SigningBundle bundle,
|
||||
ProofOfEntitlementResult entitlement,
|
||||
CallerContext caller,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(bundle);
|
||||
ArgumentNullException.ThrowIfNull(entitlement);
|
||||
ArgumentNullException.ThrowIfNull(caller);
|
||||
|
||||
var auditId = Guid.NewGuid().ToString("d");
|
||||
var entry = new SignerAuditEntry(
|
||||
auditId,
|
||||
_timeProvider.GetUtcNow(),
|
||||
caller.Subject,
|
||||
caller.Tenant,
|
||||
entitlement.Plan,
|
||||
request.ScannerImageDigest,
|
||||
bundle.Metadata.Identity.Mode,
|
||||
bundle.Metadata.ProviderName,
|
||||
request.Subjects);
|
||||
|
||||
_entries[auditId] = entry;
|
||||
_logger.LogInformation("Signer audit event {AuditId} recorded for tenant {Tenant}", auditId, caller.Tenant);
|
||||
return ValueTask.FromResult(auditId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
|
||||
namespace StellaOps.Signer.Infrastructure.Options;
|
||||
|
||||
public sealed class SignerCryptoOptions
|
||||
{
|
||||
public string KeyId { get; set; } = "signer-kms-default";
|
||||
|
||||
public string AlgorithmId { get; set; } = "HS256";
|
||||
|
||||
public string Secret { get; set; } = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("stellaops-signer-secret"));
|
||||
|
||||
public string ProviderName { get; set; } = "InMemoryHmacProvider";
|
||||
|
||||
public string Mode { get; set; } = "kms";
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StellaOps.Signer.Infrastructure.Options;
|
||||
|
||||
public sealed class SignerEntitlementOptions
|
||||
{
|
||||
public IDictionary<string, SignerEntitlementDefinition> Tokens { get; } =
|
||||
new Dictionary<string, SignerEntitlementDefinition>(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
public sealed record SignerEntitlementDefinition(
|
||||
string LicenseId,
|
||||
string CustomerId,
|
||||
string Plan,
|
||||
int MaxArtifactBytes,
|
||||
int QpsLimit,
|
||||
int QpsRemaining,
|
||||
DateTimeOffset ExpiresAtUtc);
|
||||
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StellaOps.Signer.Infrastructure.Options;
|
||||
|
||||
public sealed class SignerReleaseVerificationOptions
|
||||
{
|
||||
public ISet<string> TrustedScannerDigests { get; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public string TrustedSigner { get; set; } = "StellaOps Release";
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Signer.Core;
|
||||
using StellaOps.Signer.Infrastructure.Options;
|
||||
|
||||
namespace StellaOps.Signer.Infrastructure.ProofOfEntitlement;
|
||||
|
||||
public sealed class InMemoryProofOfEntitlementIntrospector : IProofOfEntitlementIntrospector
|
||||
{
|
||||
private readonly IOptionsMonitor<SignerEntitlementOptions> _options;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public InMemoryProofOfEntitlementIntrospector(
|
||||
IOptionsMonitor<SignerEntitlementOptions> options,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public ValueTask<ProofOfEntitlementResult> IntrospectAsync(
|
||||
ProofOfEntitlement proof,
|
||||
CallerContext caller,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(proof);
|
||||
ArgumentNullException.ThrowIfNull(caller);
|
||||
|
||||
var token = proof.Value ?? string.Empty;
|
||||
var snapshot = _options.CurrentValue;
|
||||
if (!snapshot.Tokens.TryGetValue(token, out var definition))
|
||||
{
|
||||
throw new SignerAuthorizationException("entitlement_denied", "Proof of entitlement is invalid or revoked.");
|
||||
}
|
||||
|
||||
if (definition.ExpiresAtUtc <= _timeProvider.GetUtcNow())
|
||||
{
|
||||
throw new SignerAuthorizationException("entitlement_denied", "Proof of entitlement has expired.");
|
||||
}
|
||||
|
||||
var result = new ProofOfEntitlementResult(
|
||||
definition.LicenseId,
|
||||
definition.CustomerId,
|
||||
definition.Plan,
|
||||
definition.MaxArtifactBytes,
|
||||
definition.QpsLimit,
|
||||
definition.QpsRemaining,
|
||||
definition.ExpiresAtUtc);
|
||||
|
||||
return ValueTask.FromResult(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Signer.Core;
|
||||
|
||||
namespace StellaOps.Signer.Infrastructure.Quotas;
|
||||
|
||||
public sealed class InMemoryQuotaService : ISignerQuotaService
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, QuotaWindow> _windows = new(StringComparer.Ordinal);
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ILogger<InMemoryQuotaService> _logger;
|
||||
|
||||
public InMemoryQuotaService(TimeProvider timeProvider, ILogger<InMemoryQuotaService> logger)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public ValueTask EnsureWithinLimitsAsync(
|
||||
SigningRequest request,
|
||||
ProofOfEntitlementResult entitlement,
|
||||
CallerContext caller,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
ArgumentNullException.ThrowIfNull(entitlement);
|
||||
ArgumentNullException.ThrowIfNull(caller);
|
||||
|
||||
var payloadSize = EstimatePayloadSize(request);
|
||||
if (payloadSize > entitlement.MaxArtifactBytes)
|
||||
{
|
||||
throw new SignerQuotaException("artifact_too_large", $"Artifact size {payloadSize} exceeds plan cap ({entitlement.MaxArtifactBytes}).");
|
||||
}
|
||||
|
||||
if (entitlement.QpsLimit <= 0)
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
var window = _windows.GetOrAdd(caller.Tenant, static _ => new QuotaWindow());
|
||||
lock (window)
|
||||
{
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
if (window.ResetAt <= now)
|
||||
{
|
||||
window.Reset(now, entitlement.QpsLimit);
|
||||
}
|
||||
|
||||
if (window.Remaining <= 0)
|
||||
{
|
||||
_logger.LogWarning("Quota exceeded for tenant {Tenant}", caller.Tenant);
|
||||
throw new SignerQuotaException("plan_throttled", "Plan QPS limit exceeded.");
|
||||
}
|
||||
|
||||
window.Remaining--;
|
||||
window.LastUpdated = now;
|
||||
}
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
private static int EstimatePayloadSize(SigningRequest request)
|
||||
{
|
||||
var predicateBytes = request.Predicate is null
|
||||
? Array.Empty<byte>()
|
||||
: Encoding.UTF8.GetBytes(request.Predicate.RootElement.GetRawText());
|
||||
|
||||
var subjectBytes = 0;
|
||||
foreach (var subject in request.Subjects)
|
||||
{
|
||||
subjectBytes += subject.Name.Length;
|
||||
foreach (var digest in subject.Digest)
|
||||
{
|
||||
subjectBytes += digest.Key.Length + digest.Value.Length;
|
||||
}
|
||||
}
|
||||
|
||||
return predicateBytes.Length + subjectBytes;
|
||||
}
|
||||
|
||||
private sealed class QuotaWindow
|
||||
{
|
||||
public DateTimeOffset ResetAt { get; private set; } = DateTimeOffset.MinValue;
|
||||
|
||||
public int Remaining { get; set; }
|
||||
|
||||
public DateTimeOffset LastUpdated { get; set; }
|
||||
|
||||
public void Reset(DateTimeOffset now, int limit)
|
||||
{
|
||||
ResetAt = now.AddSeconds(1);
|
||||
Remaining = limit;
|
||||
LastUpdated = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Signer.Core;
|
||||
using StellaOps.Signer.Infrastructure.Options;
|
||||
|
||||
namespace StellaOps.Signer.Infrastructure.ReleaseVerification;
|
||||
|
||||
public sealed class DefaultReleaseIntegrityVerifier : IReleaseIntegrityVerifier
|
||||
{
|
||||
private static readonly Regex DigestPattern = new("^sha256:[a-fA-F0-9]{64}$", RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
||||
|
||||
private readonly IOptionsMonitor<SignerReleaseVerificationOptions> _options;
|
||||
|
||||
public DefaultReleaseIntegrityVerifier(IOptionsMonitor<SignerReleaseVerificationOptions> options)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
public ValueTask<ReleaseVerificationResult> VerifyAsync(string scannerImageDigest, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(scannerImageDigest) || !DigestPattern.IsMatch(scannerImageDigest))
|
||||
{
|
||||
throw new SignerReleaseVerificationException("release_digest_invalid", "Scanner image digest must be a valid sha256 string.");
|
||||
}
|
||||
|
||||
var options = _options.CurrentValue;
|
||||
if (options.TrustedScannerDigests.Count > 0 &&
|
||||
!options.TrustedScannerDigests.Contains(scannerImageDigest))
|
||||
{
|
||||
return ValueTask.FromResult(new ReleaseVerificationResult(false, null));
|
||||
}
|
||||
|
||||
return ValueTask.FromResult(new ReleaseVerificationResult(true, options.TrustedSigner));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Signer.Core;
|
||||
using StellaOps.Signer.Infrastructure.Auditing;
|
||||
using StellaOps.Signer.Infrastructure.ProofOfEntitlement;
|
||||
using StellaOps.Signer.Infrastructure.Quotas;
|
||||
using StellaOps.Signer.Infrastructure.ReleaseVerification;
|
||||
using StellaOps.Signer.Infrastructure.Signing;
|
||||
|
||||
namespace StellaOps.Signer.Infrastructure;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddSignerPipeline(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<ISignerPipeline, SignerPipeline>();
|
||||
services.AddSingleton<IProofOfEntitlementIntrospector, InMemoryProofOfEntitlementIntrospector>();
|
||||
services.AddSingleton<IReleaseIntegrityVerifier, DefaultReleaseIntegrityVerifier>();
|
||||
services.AddSingleton<ISignerQuotaService, InMemoryQuotaService>();
|
||||
services.AddSingleton<IDsseSigner, HmacDsseSigner>();
|
||||
services.AddSingleton<ISignerAuditSink, InMemorySignerAuditSink>();
|
||||
services.AddSingleton(TimeProvider.System);
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Signer.Core;
|
||||
using StellaOps.Signer.Infrastructure.Options;
|
||||
|
||||
namespace StellaOps.Signer.Infrastructure.Signing;
|
||||
|
||||
public sealed class HmacDsseSigner : IDsseSigner
|
||||
{
|
||||
private readonly IOptionsMonitor<SignerCryptoOptions> _options;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public HmacDsseSigner(IOptionsMonitor<SignerCryptoOptions> options, TimeProvider timeProvider)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public ValueTask<SigningBundle> SignAsync(
|
||||
SigningRequest request,
|
||||
ProofOfEntitlementResult entitlement,
|
||||
CallerContext caller,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
ArgumentNullException.ThrowIfNull(entitlement);
|
||||
ArgumentNullException.ThrowIfNull(caller);
|
||||
|
||||
var options = _options.CurrentValue;
|
||||
var payloadBytes = SignerStatementBuilder.BuildStatementPayload(request);
|
||||
|
||||
var secretBytes = Convert.FromBase64String(options.Secret);
|
||||
using var hmac = new HMACSHA256(secretBytes);
|
||||
var signatureBytes = hmac.ComputeHash(payloadBytes);
|
||||
var signature = Convert.ToBase64String(signatureBytes);
|
||||
var payloadBase64 = Convert.ToBase64String(payloadBytes);
|
||||
|
||||
var envelope = new DsseEnvelope(
|
||||
payloadBase64,
|
||||
"application/vnd.in-toto+json",
|
||||
new[]
|
||||
{
|
||||
new DsseSignature(signature, options.KeyId),
|
||||
});
|
||||
|
||||
var metadata = new SigningMetadata(
|
||||
new SigningIdentity(
|
||||
options.Mode,
|
||||
caller.Subject,
|
||||
caller.Subject,
|
||||
_timeProvider.GetUtcNow().AddMinutes(10)),
|
||||
Array.Empty<string>(),
|
||||
options.ProviderName,
|
||||
options.AlgorithmId);
|
||||
|
||||
var bundle = new SigningBundle(envelope, metadata);
|
||||
return ValueTask.FromResult(bundle);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Signer.Core\StellaOps.Signer.Core.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.2" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="3.5.0" />
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.8.24" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<UseConcelierTestInfra>false</UseConcelierTestInfra>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.8" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||
<PackageReference Include="Mongo2Go" Version="3.1.3" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Signer.WebService\StellaOps.Signer.WebService.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Signer.Infrastructure\StellaOps.Signer.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Signer.Core\StellaOps.Signer.Core.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.Configuration\StellaOps.Configuration.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.DependencyInjection\StellaOps.DependencyInjection.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,30 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Signer.WebService.Contracts;
|
||||
|
||||
public sealed record SignDsseSubjectDto(string Name, Dictionary<string, string> Digest);
|
||||
|
||||
public sealed record SignDssePoeDto(string Format, string Value);
|
||||
|
||||
public sealed record SignDsseOptionsDto(string? SigningMode, int? ExpirySeconds, string? ReturnBundle);
|
||||
|
||||
public sealed record SignDsseRequestDto(
|
||||
List<SignDsseSubjectDto> Subject,
|
||||
string PredicateType,
|
||||
JsonElement Predicate,
|
||||
string ScannerImageDigest,
|
||||
SignDssePoeDto Poe,
|
||||
SignDsseOptionsDto? Options);
|
||||
|
||||
public sealed record SignDsseResponseDto(SignDsseBundleDto Bundle, SignDssePolicyDto Policy, string AuditId);
|
||||
|
||||
public sealed record SignDsseBundleDto(SignDsseEnvelopeDto Dsse, IReadOnlyList<string> CertificateChain, string Mode, SignDsseIdentityDto SigningIdentity);
|
||||
|
||||
public sealed record SignDsseEnvelopeDto(string PayloadType, string Payload, IReadOnlyList<SignDsseSignatureDto> Signatures);
|
||||
|
||||
public sealed record SignDsseSignatureDto(string Signature, string? KeyId);
|
||||
|
||||
public sealed record SignDsseIdentityDto(string Issuer, string Subject, string? CertExpiry);
|
||||
|
||||
public sealed record SignDssePolicyDto(string Plan, int MaxArtifactBytes, int QpsRemaining);
|
||||
@@ -0,0 +1,245 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Signer.Core;
|
||||
using StellaOps.Signer.WebService.Contracts;
|
||||
|
||||
namespace StellaOps.Signer.WebService.Endpoints;
|
||||
|
||||
public static class SignerEndpoints
|
||||
{
|
||||
public static IEndpointRouteBuilder MapSignerEndpoints(this IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
var group = endpoints.MapGroup("/api/v1/signer")
|
||||
.WithTags("Signer")
|
||||
.RequireAuthorization();
|
||||
|
||||
group.MapPost("/sign/dsse", SignDsseAsync);
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
private static async Task<IResult> SignDsseAsync(
|
||||
HttpContext httpContext,
|
||||
[FromBody] SignDsseRequestDto requestDto,
|
||||
ISignerPipeline pipeline,
|
||||
ILoggerFactory loggerFactory,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (requestDto is null)
|
||||
{
|
||||
return Results.Problem("Request body is required.", statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
var logger = loggerFactory.CreateLogger("SignerEndpoints.SignDsse");
|
||||
try
|
||||
{
|
||||
var caller = BuildCallerContext(httpContext);
|
||||
ValidateSenderBinding(httpContext, requestDto.Poe, caller);
|
||||
|
||||
using var predicateDocument = JsonDocument.Parse(requestDto.Predicate.GetRawText());
|
||||
var signingRequest = new SigningRequest(
|
||||
ConvertSubjects(requestDto.Subject),
|
||||
requestDto.PredicateType,
|
||||
predicateDocument,
|
||||
requestDto.ScannerImageDigest,
|
||||
new ProofOfEntitlement(
|
||||
ParsePoeFormat(requestDto.Poe.Format),
|
||||
requestDto.Poe.Value),
|
||||
ConvertOptions(requestDto.Options));
|
||||
|
||||
var outcome = await pipeline.SignAsync(signingRequest, caller, cancellationToken).ConfigureAwait(false);
|
||||
var response = ConvertOutcome(outcome);
|
||||
return Results.Ok(response);
|
||||
}
|
||||
catch (SignerValidationException ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Validation failure while signing DSSE.");
|
||||
return Results.Problem(ex.Message, statusCode: StatusCodes.Status400BadRequest, type: ex.Code);
|
||||
}
|
||||
catch (SignerAuthorizationException ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Authorization failure while signing DSSE.");
|
||||
return Results.Problem(ex.Message, statusCode: StatusCodes.Status403Forbidden, type: ex.Code);
|
||||
}
|
||||
catch (SignerReleaseVerificationException ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Release verification failed.");
|
||||
return Results.Problem(ex.Message, statusCode: StatusCodes.Status403Forbidden, type: ex.Code);
|
||||
}
|
||||
catch (SignerQuotaException ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Quota enforcement rejected request.");
|
||||
return Results.Problem(ex.Message, statusCode: StatusCodes.Status429TooManyRequests, type: ex.Code);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Unexpected error while signing DSSE.");
|
||||
return Results.Problem("Internal server error.", statusCode: StatusCodes.Status500InternalServerError, type: "signing_unavailable");
|
||||
}
|
||||
}
|
||||
|
||||
private static CallerContext BuildCallerContext(HttpContext context)
|
||||
{
|
||||
var user = context.User ?? throw new SignerAuthorizationException("invalid_caller", "Caller is not authenticated.");
|
||||
|
||||
string subject = user.FindFirstValue(StellaOpsClaimTypes.Subject) ??
|
||||
throw new SignerAuthorizationException("invalid_caller", "Subject claim is required.");
|
||||
string tenant = user.FindFirstValue(StellaOpsClaimTypes.Tenant) ?? subject;
|
||||
|
||||
var scopes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
if (user.HasClaim(c => c.Type == StellaOpsClaimTypes.Scope))
|
||||
{
|
||||
foreach (var value in user.FindAll(StellaOpsClaimTypes.Scope))
|
||||
{
|
||||
foreach (var scope in value.Value.Split(' ', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
scopes.Add(scope);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var scopeClaim in user.FindAll(StellaOpsClaimTypes.ScopeItem))
|
||||
{
|
||||
scopes.Add(scopeClaim.Value);
|
||||
}
|
||||
|
||||
var audiences = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var audClaim in user.FindAll(StellaOpsClaimTypes.Audience))
|
||||
{
|
||||
if (audClaim.Value.Contains(' '))
|
||||
{
|
||||
foreach (var aud in audClaim.Value.Split(' ', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
audiences.Add(aud);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
audiences.Add(audClaim.Value);
|
||||
}
|
||||
}
|
||||
|
||||
if (audiences.Count == 0)
|
||||
{
|
||||
throw new SignerAuthorizationException("invalid_audience", "Audience claim is required.");
|
||||
}
|
||||
|
||||
var sender = context.Request.Headers.TryGetValue("DPoP", out var dpop)
|
||||
? dpop.ToString()
|
||||
: null;
|
||||
|
||||
var clientCert = context.Connection.ClientCertificate?.Thumbprint;
|
||||
|
||||
return new CallerContext(
|
||||
subject,
|
||||
tenant,
|
||||
scopes.ToArray(),
|
||||
audiences.ToArray(),
|
||||
sender,
|
||||
clientCert);
|
||||
}
|
||||
|
||||
private static void ValidateSenderBinding(HttpContext context, SignDssePoeDto poe, CallerContext caller)
|
||||
{
|
||||
if (poe is null)
|
||||
{
|
||||
throw new SignerValidationException("poe_missing", "Proof of entitlement is required.");
|
||||
}
|
||||
|
||||
var format = ParsePoeFormat(poe.Format);
|
||||
if (format == SignerPoEFormat.Jwt)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(caller.SenderBinding))
|
||||
{
|
||||
throw new SignerAuthorizationException("invalid_token", "DPoP proof is required for JWT PoE.");
|
||||
}
|
||||
}
|
||||
else if (format == SignerPoEFormat.Mtls)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(caller.ClientCertificateThumbprint))
|
||||
{
|
||||
throw new SignerAuthorizationException("invalid_token", "Client certificate is required for mTLS PoE.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyList<SigningSubject> ConvertSubjects(List<SignDsseSubjectDto> subjects)
|
||||
{
|
||||
if (subjects is null || subjects.Count == 0)
|
||||
{
|
||||
throw new SignerValidationException("subject_missing", "At least one subject is required.");
|
||||
}
|
||||
|
||||
return subjects.Select(subject =>
|
||||
{
|
||||
if (subject.Digest is null || subject.Digest.Count == 0)
|
||||
{
|
||||
throw new SignerValidationException("subject_digest_invalid", $"Digest for subject '{subject.Name}' is required.");
|
||||
}
|
||||
|
||||
return new SigningSubject(subject.Name, subject.Digest);
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
private static SigningOptions ConvertOptions(SignDsseOptionsDto? optionsDto)
|
||||
{
|
||||
if (optionsDto is null)
|
||||
{
|
||||
return new SigningOptions(SigningMode.Kms, null, "dsse+cert");
|
||||
}
|
||||
|
||||
var mode = optionsDto.SigningMode switch
|
||||
{
|
||||
null or "" => SigningMode.Kms,
|
||||
"kms" or "KMS" => SigningMode.Kms,
|
||||
"keyless" or "KEYLESS" => SigningMode.Keyless,
|
||||
_ => throw new SignerValidationException("signing_mode_invalid", $"Unsupported signing mode '{optionsDto.SigningMode}'."),
|
||||
};
|
||||
|
||||
return new SigningOptions(mode, optionsDto.ExpirySeconds, optionsDto.ReturnBundle ?? "dsse+cert");
|
||||
}
|
||||
|
||||
private static SignerPoEFormat ParsePoeFormat(string? format)
|
||||
{
|
||||
return format?.ToLowerInvariant() switch
|
||||
{
|
||||
"jwt" => SignerPoEFormat.Jwt,
|
||||
"mtls" => SignerPoEFormat.Mtls,
|
||||
_ => throw new SignerValidationException("poe_invalid", $"Unsupported PoE format '{format}'."),
|
||||
};
|
||||
}
|
||||
|
||||
private static SignDsseResponseDto ConvertOutcome(SigningOutcome outcome)
|
||||
{
|
||||
var signatures = outcome.Bundle.Envelope.Signatures
|
||||
.Select(signature => new SignDsseSignatureDto(signature.Signature, signature.KeyId))
|
||||
.ToArray();
|
||||
|
||||
var bundle = new SignDsseBundleDto(
|
||||
new SignDsseEnvelopeDto(
|
||||
outcome.Bundle.Envelope.PayloadType,
|
||||
outcome.Bundle.Envelope.Payload,
|
||||
signatures),
|
||||
outcome.Bundle.Metadata.CertificateChain,
|
||||
outcome.Bundle.Metadata.Identity.Mode,
|
||||
new SignDsseIdentityDto(
|
||||
outcome.Bundle.Metadata.Identity.Issuer,
|
||||
outcome.Bundle.Metadata.Identity.Subject,
|
||||
outcome.Bundle.Metadata.Identity.ExpiresAtUtc?.ToString("O")));
|
||||
|
||||
var policy = new SignDssePolicyDto(
|
||||
outcome.Policy.Plan,
|
||||
outcome.Policy.MaxArtifactBytes,
|
||||
outcome.Policy.QpsRemaining);
|
||||
|
||||
return new SignDsseResponseDto(bundle, policy, outcome.AuditId);
|
||||
}
|
||||
}
|
||||
43
src/StellaOps.Signer/StellaOps.Signer.WebService/Program.cs
Normal file
43
src/StellaOps.Signer/StellaOps.Signer.WebService/Program.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using StellaOps.Signer.Infrastructure;
|
||||
using StellaOps.Signer.Infrastructure.Options;
|
||||
using StellaOps.Signer.WebService.Endpoints;
|
||||
using StellaOps.Signer.WebService.Security;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddLogging();
|
||||
builder.Services.AddAuthentication(StubBearerAuthenticationDefaults.AuthenticationScheme)
|
||||
.AddScheme<AuthenticationSchemeOptions, StubBearerAuthenticationHandler>(
|
||||
StubBearerAuthenticationDefaults.AuthenticationScheme,
|
||||
_ => { });
|
||||
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
builder.Services.AddSignerPipeline();
|
||||
builder.Services.Configure<SignerEntitlementOptions>(options =>
|
||||
{
|
||||
options.Tokens["valid-poe"] = new SignerEntitlementDefinition(
|
||||
LicenseId: "LIC-TEST",
|
||||
CustomerId: "CUST-TEST",
|
||||
Plan: "pro",
|
||||
MaxArtifactBytes: 128 * 1024,
|
||||
QpsLimit: 5,
|
||||
QpsRemaining: 5,
|
||||
ExpiresAtUtc: DateTimeOffset.UtcNow.AddHours(1));
|
||||
});
|
||||
builder.Services.Configure<SignerReleaseVerificationOptions>(options =>
|
||||
{
|
||||
options.TrustedScannerDigests.Add("sha256:trusted-scanner-digest");
|
||||
});
|
||||
builder.Services.Configure<SignerCryptoOptions>(_ => { });
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapGet("/", () => Results.Ok("StellaOps Signer service ready."));
|
||||
app.MapSignerEndpoints();
|
||||
|
||||
app.Run();
|
||||
@@ -0,0 +1,29 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.8" />
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.8.24" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Signer.Core\StellaOps.Signer.Core.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Signer.Infrastructure\StellaOps.Signer.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.Configuration\StellaOps.Configuration.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.DependencyInjection\StellaOps.DependencyInjection.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.Authority\StellaOps.Auth.Abstractions\StellaOps.Auth.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.Authority\StellaOps.Auth.Client\StellaOps.Auth.Client.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.Authority\StellaOps.Auth.ServerIntegration\StellaOps.Auth.ServerIntegration.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
174
src/StellaOps.Signer/StellaOps.Signer.sln
Normal file
174
src/StellaOps.Signer/StellaOps.Signer.sln
Normal file
@@ -0,0 +1,174 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Signer.Core", "StellaOps.Signer.Core\StellaOps.Signer.Core.csproj", "{81EB20CC-54DE-4450-9370-92B489B64F19}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Signer.Infrastructure", "StellaOps.Signer.Infrastructure\StellaOps.Signer.Infrastructure.csproj", "{AD28F5E8-CF69-4587-B3D2-C2B42935993D}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Signer.WebService", "StellaOps.Signer.WebService\StellaOps.Signer.WebService.csproj", "{104C429B-2122-43B5-BE2A-5FC846FEBDC4}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Configuration", "..\StellaOps.Configuration\StellaOps.Configuration.csproj", "{7A261EB8-60DF-4DD7-83E0-43811B0433B3}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Authority.Plugins.Abstractions", "..\StellaOps.Authority\StellaOps.Authority.Plugins.Abstractions\StellaOps.Authority.Plugins.Abstractions.csproj", "{B0E46302-AAC2-409C-AA2F-526F8328C696}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography", "..\StellaOps.Cryptography\StellaOps.Cryptography.csproj", "{726F764A-EEE9-4910-8149-42F326E37AF0}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.DependencyInjection", "..\StellaOps.DependencyInjection\StellaOps.DependencyInjection.csproj", "{D17E135F-57B9-476A-8ECE-BE081F25E917}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Auth.Abstractions", "..\StellaOps.Authority\StellaOps.Auth.Abstractions\StellaOps.Auth.Abstractions.csproj", "{526A921C-E020-4B7E-A195-29CC6FD1C634}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Auth.Client", "..\StellaOps.Authority\StellaOps.Auth.Client\StellaOps.Auth.Client.csproj", "{BA683E2B-350F-4719-ACF7-1C5C35F5B72F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Auth.ServerIntegration", "..\StellaOps.Authority\StellaOps.Auth.ServerIntegration\StellaOps.Auth.ServerIntegration.csproj", "{EA1037DD-3213-4360-87B8-1129936D89CE}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Signer.Tests", "StellaOps.Signer.Tests\StellaOps.Signer.Tests.csproj", "{B09322C0-6827-46D6-91AD-D2380BD36F21}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{81EB20CC-54DE-4450-9370-92B489B64F19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{81EB20CC-54DE-4450-9370-92B489B64F19}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{81EB20CC-54DE-4450-9370-92B489B64F19}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{81EB20CC-54DE-4450-9370-92B489B64F19}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{81EB20CC-54DE-4450-9370-92B489B64F19}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{81EB20CC-54DE-4450-9370-92B489B64F19}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{81EB20CC-54DE-4450-9370-92B489B64F19}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{81EB20CC-54DE-4450-9370-92B489B64F19}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{81EB20CC-54DE-4450-9370-92B489B64F19}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{81EB20CC-54DE-4450-9370-92B489B64F19}.Release|x64.Build.0 = Release|Any CPU
|
||||
{81EB20CC-54DE-4450-9370-92B489B64F19}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{81EB20CC-54DE-4450-9370-92B489B64F19}.Release|x86.Build.0 = Release|Any CPU
|
||||
{AD28F5E8-CF69-4587-B3D2-C2B42935993D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AD28F5E8-CF69-4587-B3D2-C2B42935993D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AD28F5E8-CF69-4587-B3D2-C2B42935993D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{AD28F5E8-CF69-4587-B3D2-C2B42935993D}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{AD28F5E8-CF69-4587-B3D2-C2B42935993D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{AD28F5E8-CF69-4587-B3D2-C2B42935993D}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{AD28F5E8-CF69-4587-B3D2-C2B42935993D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AD28F5E8-CF69-4587-B3D2-C2B42935993D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AD28F5E8-CF69-4587-B3D2-C2B42935993D}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{AD28F5E8-CF69-4587-B3D2-C2B42935993D}.Release|x64.Build.0 = Release|Any CPU
|
||||
{AD28F5E8-CF69-4587-B3D2-C2B42935993D}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{AD28F5E8-CF69-4587-B3D2-C2B42935993D}.Release|x86.Build.0 = Release|Any CPU
|
||||
{104C429B-2122-43B5-BE2A-5FC846FEBDC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{104C429B-2122-43B5-BE2A-5FC846FEBDC4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{104C429B-2122-43B5-BE2A-5FC846FEBDC4}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{104C429B-2122-43B5-BE2A-5FC846FEBDC4}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{104C429B-2122-43B5-BE2A-5FC846FEBDC4}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{104C429B-2122-43B5-BE2A-5FC846FEBDC4}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{104C429B-2122-43B5-BE2A-5FC846FEBDC4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{104C429B-2122-43B5-BE2A-5FC846FEBDC4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{104C429B-2122-43B5-BE2A-5FC846FEBDC4}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{104C429B-2122-43B5-BE2A-5FC846FEBDC4}.Release|x64.Build.0 = Release|Any CPU
|
||||
{104C429B-2122-43B5-BE2A-5FC846FEBDC4}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{104C429B-2122-43B5-BE2A-5FC846FEBDC4}.Release|x86.Build.0 = Release|Any CPU
|
||||
{7A261EB8-60DF-4DD7-83E0-43811B0433B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7A261EB8-60DF-4DD7-83E0-43811B0433B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7A261EB8-60DF-4DD7-83E0-43811B0433B3}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{7A261EB8-60DF-4DD7-83E0-43811B0433B3}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{7A261EB8-60DF-4DD7-83E0-43811B0433B3}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{7A261EB8-60DF-4DD7-83E0-43811B0433B3}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{7A261EB8-60DF-4DD7-83E0-43811B0433B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7A261EB8-60DF-4DD7-83E0-43811B0433B3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{7A261EB8-60DF-4DD7-83E0-43811B0433B3}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{7A261EB8-60DF-4DD7-83E0-43811B0433B3}.Release|x64.Build.0 = Release|Any CPU
|
||||
{7A261EB8-60DF-4DD7-83E0-43811B0433B3}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{7A261EB8-60DF-4DD7-83E0-43811B0433B3}.Release|x86.Build.0 = Release|Any CPU
|
||||
{B0E46302-AAC2-409C-AA2F-526F8328C696}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B0E46302-AAC2-409C-AA2F-526F8328C696}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B0E46302-AAC2-409C-AA2F-526F8328C696}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{B0E46302-AAC2-409C-AA2F-526F8328C696}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{B0E46302-AAC2-409C-AA2F-526F8328C696}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{B0E46302-AAC2-409C-AA2F-526F8328C696}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{B0E46302-AAC2-409C-AA2F-526F8328C696}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B0E46302-AAC2-409C-AA2F-526F8328C696}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B0E46302-AAC2-409C-AA2F-526F8328C696}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{B0E46302-AAC2-409C-AA2F-526F8328C696}.Release|x64.Build.0 = Release|Any CPU
|
||||
{B0E46302-AAC2-409C-AA2F-526F8328C696}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{B0E46302-AAC2-409C-AA2F-526F8328C696}.Release|x86.Build.0 = Release|Any CPU
|
||||
{726F764A-EEE9-4910-8149-42F326E37AF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{726F764A-EEE9-4910-8149-42F326E37AF0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{726F764A-EEE9-4910-8149-42F326E37AF0}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{726F764A-EEE9-4910-8149-42F326E37AF0}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{726F764A-EEE9-4910-8149-42F326E37AF0}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{726F764A-EEE9-4910-8149-42F326E37AF0}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{726F764A-EEE9-4910-8149-42F326E37AF0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{726F764A-EEE9-4910-8149-42F326E37AF0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{726F764A-EEE9-4910-8149-42F326E37AF0}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{726F764A-EEE9-4910-8149-42F326E37AF0}.Release|x64.Build.0 = Release|Any CPU
|
||||
{726F764A-EEE9-4910-8149-42F326E37AF0}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{726F764A-EEE9-4910-8149-42F326E37AF0}.Release|x86.Build.0 = Release|Any CPU
|
||||
{D17E135F-57B9-476A-8ECE-BE081F25E917}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D17E135F-57B9-476A-8ECE-BE081F25E917}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D17E135F-57B9-476A-8ECE-BE081F25E917}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{D17E135F-57B9-476A-8ECE-BE081F25E917}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{D17E135F-57B9-476A-8ECE-BE081F25E917}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{D17E135F-57B9-476A-8ECE-BE081F25E917}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{D17E135F-57B9-476A-8ECE-BE081F25E917}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D17E135F-57B9-476A-8ECE-BE081F25E917}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D17E135F-57B9-476A-8ECE-BE081F25E917}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{D17E135F-57B9-476A-8ECE-BE081F25E917}.Release|x64.Build.0 = Release|Any CPU
|
||||
{D17E135F-57B9-476A-8ECE-BE081F25E917}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{D17E135F-57B9-476A-8ECE-BE081F25E917}.Release|x86.Build.0 = Release|Any CPU
|
||||
{526A921C-E020-4B7E-A195-29CC6FD1C634}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{526A921C-E020-4B7E-A195-29CC6FD1C634}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{526A921C-E020-4B7E-A195-29CC6FD1C634}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{526A921C-E020-4B7E-A195-29CC6FD1C634}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{526A921C-E020-4B7E-A195-29CC6FD1C634}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{526A921C-E020-4B7E-A195-29CC6FD1C634}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{526A921C-E020-4B7E-A195-29CC6FD1C634}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{526A921C-E020-4B7E-A195-29CC6FD1C634}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{526A921C-E020-4B7E-A195-29CC6FD1C634}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{526A921C-E020-4B7E-A195-29CC6FD1C634}.Release|x64.Build.0 = Release|Any CPU
|
||||
{526A921C-E020-4B7E-A195-29CC6FD1C634}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{526A921C-E020-4B7E-A195-29CC6FD1C634}.Release|x86.Build.0 = Release|Any CPU
|
||||
{BA683E2B-350F-4719-ACF7-1C5C35F5B72F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BA683E2B-350F-4719-ACF7-1C5C35F5B72F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BA683E2B-350F-4719-ACF7-1C5C35F5B72F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{BA683E2B-350F-4719-ACF7-1C5C35F5B72F}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{BA683E2B-350F-4719-ACF7-1C5C35F5B72F}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{BA683E2B-350F-4719-ACF7-1C5C35F5B72F}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{BA683E2B-350F-4719-ACF7-1C5C35F5B72F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BA683E2B-350F-4719-ACF7-1C5C35F5B72F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{BA683E2B-350F-4719-ACF7-1C5C35F5B72F}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{BA683E2B-350F-4719-ACF7-1C5C35F5B72F}.Release|x64.Build.0 = Release|Any CPU
|
||||
{BA683E2B-350F-4719-ACF7-1C5C35F5B72F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{BA683E2B-350F-4719-ACF7-1C5C35F5B72F}.Release|x86.Build.0 = Release|Any CPU
|
||||
{EA1037DD-3213-4360-87B8-1129936D89CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EA1037DD-3213-4360-87B8-1129936D89CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EA1037DD-3213-4360-87B8-1129936D89CE}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{EA1037DD-3213-4360-87B8-1129936D89CE}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{EA1037DD-3213-4360-87B8-1129936D89CE}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{EA1037DD-3213-4360-87B8-1129936D89CE}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{EA1037DD-3213-4360-87B8-1129936D89CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EA1037DD-3213-4360-87B8-1129936D89CE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{EA1037DD-3213-4360-87B8-1129936D89CE}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{EA1037DD-3213-4360-87B8-1129936D89CE}.Release|x64.Build.0 = Release|Any CPU
|
||||
{EA1037DD-3213-4360-87B8-1129936D89CE}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{EA1037DD-3213-4360-87B8-1129936D89CE}.Release|x86.Build.0 = Release|Any CPU
|
||||
{B09322C0-6827-46D6-91AD-D2380BD36F21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B09322C0-6827-46D6-91AD-D2380BD36F21}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B09322C0-6827-46D6-91AD-D2380BD36F21}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{B09322C0-6827-46D6-91AD-D2380BD36F21}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{B09322C0-6827-46D6-91AD-D2380BD36F21}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{B09322C0-6827-46D6-91AD-D2380BD36F21}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{B09322C0-6827-46D6-91AD-D2380BD36F21}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B09322C0-6827-46D6-91AD-D2380BD36F21}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B09322C0-6827-46D6-91AD-D2380BD36F21}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{B09322C0-6827-46D6-91AD-D2380BD36F21}.Release|x64.Build.0 = Release|Any CPU
|
||||
{B09322C0-6827-46D6-91AD-D2380BD36F21}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{B09322C0-6827-46D6-91AD-D2380BD36F21}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
11
src/StellaOps.Signer/TASKS.md
Normal file
11
src/StellaOps.Signer/TASKS.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Signer Guild Task Board (UTC 2025-10-19)
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
| SIGNER-API-11-101 | DOING (2025-10-19) | Signer Guild | — | `/sign/dsse` pipeline with Authority auth, PoE introspection, release verification, DSSE signing. | ✅ `POST /api/v1/signer/sign/dsse` enforces OpTok audience/scope, DPoP/mTLS binding, PoE introspection, and rejects untrusted scanner digests.<br>✅ Signing pipeline supports keyless (Fulcio) plus optional KMS modes, returning DSSE bundles + cert metadata; deterministic audits persisted.<br>✅ Unit/integration tests cover happy path, invalid PoE, untrusted release, Fulcio/KMS failure, and documentation updated in `docs/ARCHITECTURE_SIGNER.md`/API reference. |
|
||||
| SIGNER-REF-11-102 | DOING (2025-10-19) | Signer Guild | — | `/verify/referrers` endpoint with OCI lookup, caching, and policy enforcement. | ✅ `GET /api/v1/signer/verify/referrers` hits OCI Referrers API, validates cosign signatures against Stella release keys, and hard-fails on ambiguity.<br>✅ Deterministic cache with policy-aware TTLs and invalidation guards repeated registry load; metrics/logs expose hit/miss/error counters.<br>✅ Tests simulate trusted/untrusted digests, cache expiry, and registry failures; docs capture usage and quota interplay. |
|
||||
| SIGNER-QUOTA-11-103 | DOING (2025-10-19) | Signer Guild | — | Enforce plan quotas, concurrency/QPS limits, artifact size caps with metrics/audit logs. | ✅ Quota middleware derives plan limits from PoE claims, applies per-tenant concurrency/QPS/size caps, and surfaces remaining capacity in responses.<br>✅ Rate limiter + token bucket state stored in Redis (or equivalent) with deterministic keying and backpressure semantics; overruns emit structured audits.<br>✅ Observability dashboards/counters added; failure modes (throttle, oversize, burst) covered by tests and documented operator runbook. |
|
||||
|
||||
> Remark (2025-10-19): Wave 0 prerequisites reviewed—none outstanding. SIGNER-API-11-101, SIGNER-REF-11-102, and SIGNER-QUOTA-11-103 moved to DOING for kickoff per EXECPLAN.md.
|
||||
|
||||
> Update status columns (TODO / DOING / DONE / BLOCKED) in tandem with code changes and associated tests.
|
||||
Reference in New Issue
Block a user