Resolve Concelier/Excititor merge conflicts
This commit is contained in:
		
							
								
								
									
										405
									
								
								src/StellaOps.Attestor/StellaOps.Attestor.WebService/Program.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										405
									
								
								src/StellaOps.Attestor/StellaOps.Attestor.WebService/Program.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,405 @@ | ||||
| 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<AttestorOptions>(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<AttestorOptions>() | ||||
|     .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<string, object?> | ||||
|         { | ||||
|             ["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<string, object?> | ||||
|         { | ||||
|             ["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<X509Certificate2> LoadClientCertificateAuthorities(string? path) | ||||
| { | ||||
|     var certificates = new List<X509Certificate2>(); | ||||
|  | ||||
|     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<X509Certificate2>()); | ||||
|     } | ||||
|     catch (Exception ex) when (ex is IOException or CryptographicException) | ||||
|     { | ||||
|         Log.Warning(ex, "Failed to load client CA bundle from {Path}", path); | ||||
|     } | ||||
|  | ||||
|     return certificates; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user