using System.Collections.Generic; using System.IO; using System.Linq; using System.Security.Authentication; using System.Security.Cryptography; using System.Security.Claims; using System.Security.Cryptography.X509Certificates; using System.Threading.RateLimiting; using Serilog; using Serilog.Events; using StellaOps.Attestor.Core.Options; using StellaOps.Attestor.Core.Submission; using StellaOps.Attestor.Infrastructure; using StellaOps.Configuration; using StellaOps.Auth.ServerIntegration; using Microsoft.Extensions.Diagnostics.HealthChecks; using OpenTelemetry.Metrics; using StellaOps.Attestor.Core.Observability; using StellaOps.Attestor.Core.Verification; using Microsoft.AspNetCore.Server.Kestrel.Https; using Serilog.Context; const string ConfigurationSection = "attestor"; var builder = WebApplication.CreateBuilder(args); builder.Configuration.AddStellaOpsDefaults(options => { options.BasePath = builder.Environment.ContentRootPath; options.EnvironmentPrefix = "ATTESTOR_"; options.BindingSection = ConfigurationSection; }); builder.Host.UseSerilog((context, services, loggerConfiguration) => { loggerConfiguration .MinimumLevel.Information() .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) .Enrich.FromLogContext() .WriteTo.Console(); }); var attestorOptions = builder.Configuration.BindOptions(ConfigurationSection); var clientCertificateAuthorities = LoadClientCertificateAuthorities(attestorOptions.Security.Mtls.CaBundle); builder.Services.AddSingleton(TimeProvider.System); builder.Services.AddSingleton(attestorOptions); builder.Services.AddRateLimiter(options => { options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; options.OnRejected = static (context, _) => { context.HttpContext.Response.Headers.TryAdd("Retry-After", "1"); return ValueTask.CompletedTask; }; options.AddPolicy("attestor-submissions", httpContext => { var identity = httpContext.Connection.ClientCertificate?.Thumbprint ?? httpContext.User.FindFirst("sub")?.Value ?? httpContext.User.FindFirst("client_id")?.Value ?? httpContext.Connection.RemoteIpAddress?.ToString() ?? "anonymous"; var quota = attestorOptions.Quotas.PerCaller; var tokensPerPeriod = Math.Max(1, quota.Qps); var tokenLimit = Math.Max(tokensPerPeriod, quota.Burst); var queueLimit = Math.Max(quota.Burst, tokensPerPeriod); return RateLimitPartition.GetTokenBucketLimiter(identity, _ => new TokenBucketRateLimiterOptions { TokenLimit = tokenLimit, TokensPerPeriod = tokensPerPeriod, ReplenishmentPeriod = TimeSpan.FromSeconds(1), QueueLimit = queueLimit, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, AutoReplenishment = true }); }); }); builder.Services.AddOptions() .Bind(builder.Configuration.GetSection(ConfigurationSection)) .ValidateOnStart(); builder.Services.AddProblemDetails(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddAttestorInfrastructure(); builder.Services.AddHttpContextAccessor(); builder.Services.AddHealthChecks() .AddCheck("self", () => HealthCheckResult.Healthy()); builder.Services.AddOpenTelemetry() .WithMetrics(metricsBuilder => { metricsBuilder.AddMeter(AttestorMetrics.MeterName); metricsBuilder.AddAspNetCoreInstrumentation(); metricsBuilder.AddRuntimeInstrumentation(); }); if (attestorOptions.Security.Authority is { Issuer: not null } authority) { builder.Services.AddStellaOpsResourceServerAuthentication( builder.Configuration, configurationSection: null, configure: resourceOptions => { resourceOptions.Authority = authority.Issuer!; resourceOptions.RequireHttpsMetadata = authority.RequireHttpsMetadata; if (!string.IsNullOrWhiteSpace(authority.JwksUrl)) { resourceOptions.MetadataAddress = authority.JwksUrl; } foreach (var audience in authority.Audiences) { resourceOptions.Audiences.Add(audience); } foreach (var scope in authority.RequiredScopes) { resourceOptions.RequiredScopes.Add(scope); } }); builder.Services.AddAuthorization(options => { options.AddPolicy("attestor:write", policy => { policy.RequireAuthenticatedUser(); policy.RequireClaim("scope", authority.RequiredScopes); }); }); } else { builder.Services.AddAuthorization(); } builder.WebHost.ConfigureKestrel(kestrel => { kestrel.ConfigureHttpsDefaults(https => { if (attestorOptions.Security.Mtls.RequireClientCertificate) { https.ClientCertificateMode = ClientCertificateMode.RequireCertificate; } https.SslProtocols = SslProtocols.Tls13 | SslProtocols.Tls12; https.ClientCertificateValidation = (certificate, _, _) => { if (!attestorOptions.Security.Mtls.RequireClientCertificate) { return true; } if (certificate is null) { Log.Warning("Client certificate missing"); return false; } if (clientCertificateAuthorities.Count > 0) { using var chain = new X509Chain { ChainPolicy = { RevocationMode = X509RevocationMode.NoCheck, TrustMode = X509ChainTrustMode.CustomRootTrust } }; foreach (var authority in clientCertificateAuthorities) { chain.ChainPolicy.CustomTrustStore.Add(authority); } if (!chain.Build(certificate)) { Log.Warning("Client certificate chain validation failed for {Subject}", certificate.Subject); return false; } } if (attestorOptions.Security.Mtls.AllowedThumbprints.Count > 0 && !attestorOptions.Security.Mtls.AllowedThumbprints.Contains(certificate.Thumbprint ?? string.Empty, StringComparer.OrdinalIgnoreCase)) { Log.Warning("Client certificate thumbprint {Thumbprint} rejected", certificate.Thumbprint); return false; } if (attestorOptions.Security.Mtls.AllowedSubjects.Count > 0 && !attestorOptions.Security.Mtls.AllowedSubjects.Contains(certificate.Subject, StringComparer.OrdinalIgnoreCase)) { Log.Warning("Client certificate subject {Subject} rejected", certificate.Subject); return false; } return true; }; }); }); var app = builder.Build(); app.UseSerilogRequestLogging(); app.Use(async (context, next) => { var correlationId = context.Request.Headers["X-Correlation-Id"].FirstOrDefault(); if (string.IsNullOrWhiteSpace(correlationId)) { correlationId = Guid.NewGuid().ToString("N"); } context.Response.Headers["X-Correlation-Id"] = correlationId; using (LogContext.PushProperty("CorrelationId", correlationId)) { await next().ConfigureAwait(false); } }); app.UseExceptionHandler(static handler => { handler.Run(async context => { var result = Results.Problem(statusCode: StatusCodes.Status500InternalServerError); await result.ExecuteAsync(context); }); }); app.UseRateLimiter(); app.UseAuthentication(); app.UseAuthorization(); app.MapHealthChecks("/health/ready"); app.MapHealthChecks("/health/live"); app.MapPost("/api/v1/rekor/entries", async (AttestorSubmissionRequest request, HttpContext httpContext, IAttestorSubmissionService submissionService, CancellationToken cancellationToken) => { var certificate = httpContext.Connection.ClientCertificate; if (certificate is null) { return Results.Problem(statusCode: StatusCodes.Status403Forbidden, title: "Client certificate required"); } var user = httpContext.User; if (user?.Identity is not { IsAuthenticated: true }) { return Results.Problem(statusCode: StatusCodes.Status401Unauthorized, title: "Authentication required"); } var submissionContext = BuildSubmissionContext(user, certificate); try { var result = await submissionService.SubmitAsync(request, submissionContext, cancellationToken).ConfigureAwait(false); return Results.Ok(result); } catch (AttestorValidationException validationEx) { return Results.Problem(statusCode: StatusCodes.Status400BadRequest, title: validationEx.Message, extensions: new Dictionary { ["code"] = validationEx.Code }); } }) .RequireAuthorization("attestor:write") .RequireRateLimiting("attestor-submissions"); app.MapGet("/api/v1/rekor/entries/{uuid}", async (string uuid, bool? refresh, IAttestorVerificationService verificationService, CancellationToken cancellationToken) => { var entry = await verificationService.GetEntryAsync(uuid, refresh is true, cancellationToken).ConfigureAwait(false); if (entry is null) { return Results.NotFound(); } return Results.Ok(new { uuid = entry.RekorUuid, index = entry.Index, backend = entry.Log.Backend, proof = entry.Proof is null ? null : new { checkpoint = entry.Proof.Checkpoint is null ? null : new { origin = entry.Proof.Checkpoint.Origin, size = entry.Proof.Checkpoint.Size, rootHash = entry.Proof.Checkpoint.RootHash, timestamp = entry.Proof.Checkpoint.Timestamp?.ToString("O") }, inclusion = entry.Proof.Inclusion is null ? null : new { leafHash = entry.Proof.Inclusion.LeafHash, path = entry.Proof.Inclusion.Path } }, logURL = entry.Log.Url, status = entry.Status, mirror = entry.Mirror is null ? null : new { backend = entry.Mirror.Backend, uuid = entry.Mirror.Uuid, index = entry.Mirror.Index, logURL = entry.Mirror.Url, status = entry.Mirror.Status, proof = entry.Mirror.Proof is null ? null : new { checkpoint = entry.Mirror.Proof.Checkpoint is null ? null : new { origin = entry.Mirror.Proof.Checkpoint.Origin, size = entry.Mirror.Proof.Checkpoint.Size, rootHash = entry.Mirror.Proof.Checkpoint.RootHash, timestamp = entry.Mirror.Proof.Checkpoint.Timestamp?.ToString("O") }, inclusion = entry.Mirror.Proof.Inclusion is null ? null : new { leafHash = entry.Mirror.Proof.Inclusion.LeafHash, path = entry.Mirror.Proof.Inclusion.Path } }, error = entry.Mirror.Error }, artifact = new { sha256 = entry.Artifact.Sha256, kind = entry.Artifact.Kind, imageDigest = entry.Artifact.ImageDigest, subjectUri = entry.Artifact.SubjectUri } }); }).RequireAuthorization("attestor:write"); app.MapPost("/api/v1/rekor/verify", async (AttestorVerificationRequest request, IAttestorVerificationService verificationService, CancellationToken cancellationToken) => { try { var result = await verificationService.VerifyAsync(request, cancellationToken).ConfigureAwait(false); return Results.Ok(result); } catch (AttestorVerificationException ex) { return Results.Problem(statusCode: StatusCodes.Status400BadRequest, title: ex.Message, extensions: new Dictionary { ["code"] = ex.Code }); } }).RequireAuthorization("attestor:write"); app.Run(); static SubmissionContext BuildSubmissionContext(ClaimsPrincipal user, X509Certificate2 certificate) { var subject = user.FindFirst("sub")?.Value ?? certificate.Subject; var audience = user.FindFirst("aud")?.Value ?? string.Empty; var clientId = user.FindFirst("client_id")?.Value; var tenant = user.FindFirst("tenant")?.Value; return new SubmissionContext { CallerSubject = subject, CallerAudience = audience, CallerClientId = clientId, CallerTenant = tenant, ClientCertificate = certificate, MtlsThumbprint = certificate.Thumbprint }; } static List LoadClientCertificateAuthorities(string? path) { var certificates = new List(); if (string.IsNullOrWhiteSpace(path)) { return certificates; } try { if (!File.Exists(path)) { Log.Warning("Client CA bundle '{Path}' not found", path); return certificates; } var collection = new X509Certificate2Collection(); collection.ImportFromPemFile(path); certificates.AddRange(collection.Cast()); } catch (Exception ex) when (ex is IOException or CryptographicException) { Log.Warning(ex, "Failed to load client CA bundle from {Path}", path); } return certificates; }