using System.Collections.Generic; using System.Security.Claims; using System.Security.Cryptography.X509Certificates; 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; 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); builder.Services.AddSingleton(TimeProvider.System); builder.Services.AddSingleton(attestorOptions); 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; } }); }); var app = builder.Build(); app.UseSerilogRequestLogging(); app.UseExceptionHandler(static handler => { handler.Run(async context => { var result = Results.Problem(statusCode: StatusCodes.Status500InternalServerError); await result.ExecuteAsync(context); }); }); 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"); 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, 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, 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 }; }