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

@@ -0,0 +1,21 @@
# Signer Guild
## Mission
Operate the StellaOps Signer service: authenticate trusted callers, enforce proofofentitlement 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).

View File

@@ -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);
}

View 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);

View File

@@ -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)
{
}
}

View 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.");
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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";
}

View File

@@ -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);

View File

@@ -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";
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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));
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);

View File

@@ -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);
}
}

View 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();

View File

@@ -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>

View 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

View 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.