up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-13 00:20:26 +02:00
parent e1f1bef4c1
commit 564df71bfb
2376 changed files with 334389 additions and 328032 deletions

View File

@@ -1,30 +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);
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

@@ -1,11 +1,11 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
@@ -14,15 +14,15 @@ 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();
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);
group.MapGet("/verify/referrers", VerifyReferrersAsync);
return endpoints;
@@ -30,40 +30,40 @@ public static class SignerEndpoints
private static async Task<IResult> SignDsseAsync(
HttpContext httpContext,
[FromBody] SignDsseRequestDto requestDto,
ISignerPipeline pipeline,
ILoggerFactory loggerFactory,
CancellationToken cancellationToken)
{
if (requestDto is null)
{
[FromBody] SignDsseRequestDto requestDto,
ISignerPipeline pipeline,
ILoggerFactory loggerFactory,
CancellationToken cancellationToken)
{
if (requestDto is null)
{
return CreateProblem("invalid_request", "Request body is required.", 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 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 Json(response);
}
catch (SignerValidationException ex)
{
logger.LogWarning(ex, "Validation failure while signing DSSE.");
}
catch (SignerValidationException ex)
{
logger.LogWarning(ex, "Validation failure while signing DSSE.");
return CreateProblem(ex.Code, ex.Message, StatusCodes.Status400BadRequest);
}
catch (SignerAuthorizationException ex)
@@ -135,155 +135,155 @@ public static class SignerEndpoints
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);
}
}
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

@@ -1,6 +1,6 @@
namespace StellaOps.Signer.WebService.Security;
public static class StubBearerAuthenticationDefaults
{
public const string AuthenticationScheme = "StubBearer";
}
namespace StellaOps.Signer.WebService.Security;
public static class StubBearerAuthenticationDefaults
{
public const string AuthenticationScheme = "StubBearer";
}

View File

@@ -1,55 +1,55 @@
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Auth.Abstractions;
namespace StellaOps.Signer.WebService.Security;
public sealed class StubBearerAuthenticationHandler
: AuthenticationHandler<AuthenticationSchemeOptions>
{
public StubBearerAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder)
: base(options, logger, encoder)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var authorization = Request.Headers.Authorization.ToString();
if (string.IsNullOrWhiteSpace(authorization) ||
!authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
return Task.FromResult(AuthenticateResult.Fail("Missing bearer token."));
}
var token = authorization.Substring("Bearer ".Length).Trim();
if (token.Length == 0)
{
return Task.FromResult(AuthenticateResult.Fail("Bearer token is empty."));
}
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, "stub-subject"),
new(StellaOpsClaimTypes.Subject, "stub-subject"),
new(StellaOpsClaimTypes.Tenant, "stub-tenant"),
new(StellaOpsClaimTypes.Scope, "signer.sign"),
new(StellaOpsClaimTypes.ScopeItem, "signer.sign"),
new(StellaOpsClaimTypes.Audience, "signer"),
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
}
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Auth.Abstractions;
namespace StellaOps.Signer.WebService.Security;
public sealed class StubBearerAuthenticationHandler
: AuthenticationHandler<AuthenticationSchemeOptions>
{
public StubBearerAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder)
: base(options, logger, encoder)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var authorization = Request.Headers.Authorization.ToString();
if (string.IsNullOrWhiteSpace(authorization) ||
!authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
return Task.FromResult(AuthenticateResult.Fail("Missing bearer token."));
}
var token = authorization.Substring("Bearer ".Length).Trim();
if (token.Length == 0)
{
return Task.FromResult(AuthenticateResult.Fail("Bearer token is empty."));
}
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, "stub-subject"),
new(StellaOpsClaimTypes.Subject, "stub-subject"),
new(StellaOpsClaimTypes.Tenant, "stub-tenant"),
new(StellaOpsClaimTypes.Scope, "signer.sign"),
new(StellaOpsClaimTypes.ScopeItem, "signer.sign"),
new(StellaOpsClaimTypes.Audience, "signer"),
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
}