// SPDX-License-Identifier: AGPL-3.0-or-later // Sprint: SPRINT_20251226_007_BE_determinism_gaps // Task: DET-GAP-08 - CLI command `stella sign --keyless --rekor` for CI pipelines using System.CommandLine; using Microsoft.Extensions.DependencyInjection; namespace StellaOps.Cli.Commands; /// /// CLI commands for Sigstore keyless signing operations. /// Supports self-hosted Sigstore (Fulcio + Rekor) for on-premise deployments. /// internal static class SignCommandGroup { /// /// Build the sign command with keyless/traditional subcommands. /// public static Command BuildSignCommand( IServiceProvider serviceProvider, Option verboseOption, CancellationToken cancellationToken) { var command = new Command("sign", "Sign artifacts (keyless via Sigstore or traditional key-based)"); command.Add(BuildKeylessCommand(serviceProvider, verboseOption, cancellationToken)); command.Add(BuildVerifyKeylessCommand(serviceProvider, verboseOption, cancellationToken)); return command; } private static Command BuildKeylessCommand( IServiceProvider serviceProvider, Option verboseOption, CancellationToken cancellationToken) { var command = new Command("keyless", "Sign artifact using Sigstore keyless signing (Fulcio + Rekor)"); var inputOption = new Option("--input") { Description = "Path to file or artifact to sign", Required = true }; command.Add(inputOption); var outputOption = new Option("--output") { Description = "Output path for signature bundle (defaults to .sigstore)" }; command.Add(outputOption); var identityTokenOption = new Option("--identity-token") { Description = "OIDC identity token (JWT). If not provided, attempts ambient credential detection." }; command.Add(identityTokenOption); var rekorOption = new Option("--rekor") { Description = "Upload signature to Rekor transparency log (default: true)", DefaultValue = true }; command.Add(rekorOption); var fulcioUrlOption = new Option("--fulcio-url") { Description = "Override Fulcio URL (for self-hosted Sigstore)" }; command.Add(fulcioUrlOption); var rekorUrlOption = new Option("--rekor-url") { Description = "Override Rekor URL (for self-hosted Sigstore)" }; command.Add(rekorUrlOption); var oidcIssuerOption = new Option("--oidc-issuer") { Description = "OIDC issuer URL for identity verification" }; command.Add(oidcIssuerOption); var bundleFormatOption = new Option("--bundle-format") { Description = "Output bundle format: sigstore, cosign-bundle, dsse (default: sigstore)", DefaultValue = "sigstore" }; command.Add(bundleFormatOption); var caBundleOption = new Option("--ca-bundle") { Description = "Path to custom CA certificate bundle for self-hosted TLS" }; command.Add(caBundleOption); var insecureOption = new Option("--insecure-skip-verify") { Description = "Skip TLS verification (NOT for production)", DefaultValue = false }; command.Add(insecureOption); command.Add(verboseOption); command.SetAction(async (parseResult, ct) => { var input = parseResult.GetValue(inputOption) ?? string.Empty; var output = parseResult.GetValue(outputOption); var identityToken = parseResult.GetValue(identityTokenOption); var useRekor = parseResult.GetValue(rekorOption); var fulcioUrl = parseResult.GetValue(fulcioUrlOption); var rekorUrl = parseResult.GetValue(rekorUrlOption); var oidcIssuer = parseResult.GetValue(oidcIssuerOption); var bundleFormat = parseResult.GetValue(bundleFormatOption) ?? "sigstore"; var caBundle = parseResult.GetValue(caBundleOption); var insecure = parseResult.GetValue(insecureOption); var verbose = parseResult.GetValue(verboseOption); return await CommandHandlers.HandleSignKeylessAsync( serviceProvider, input, output, identityToken, useRekor, fulcioUrl, rekorUrl, oidcIssuer, bundleFormat, caBundle, insecure, verbose, ct); }); return command; } private static Command BuildVerifyKeylessCommand( IServiceProvider serviceProvider, Option verboseOption, CancellationToken cancellationToken) { var command = new Command("verify-keyless", "Verify a keyless signature against Sigstore"); var inputOption = new Option("--input") { Description = "Path to file or artifact to verify", Required = true }; command.Add(inputOption); var bundleOption = new Option("--bundle") { Description = "Path to Sigstore bundle file (defaults to .sigstore)" }; command.Add(bundleOption); var certificateOption = new Option("--certificate") { Description = "Path to signing certificate (PEM format)" }; command.Add(certificateOption); var signatureOption = new Option("--signature") { Description = "Path to detached signature" }; command.Add(signatureOption); var rekorUuidOption = new Option("--rekor-uuid") { Description = "Rekor entry UUID for transparency verification" }; command.Add(rekorUuidOption); var rekorUrlOption = new Option("--rekor-url") { Description = "Override Rekor URL (for self-hosted Sigstore)" }; command.Add(rekorUrlOption); var issuerOption = new Option("--certificate-issuer") { Description = "Expected OIDC issuer in certificate" }; command.Add(issuerOption); var subjectOption = new Option("--certificate-subject") { Description = "Expected subject (email/identity) in certificate" }; command.Add(subjectOption); var caBundleOption = new Option("--ca-bundle") { Description = "Path to custom CA certificate bundle for self-hosted TLS" }; command.Add(caBundleOption); command.Add(verboseOption); command.SetAction(async (parseResult, ct) => { var input = parseResult.GetValue(inputOption) ?? string.Empty; var bundle = parseResult.GetValue(bundleOption); var certificate = parseResult.GetValue(certificateOption); var signature = parseResult.GetValue(signatureOption); var rekorUuid = parseResult.GetValue(rekorUuidOption); var rekorUrl = parseResult.GetValue(rekorUrlOption); var issuer = parseResult.GetValue(issuerOption); var subject = parseResult.GetValue(subjectOption); var caBundle = parseResult.GetValue(caBundleOption); var verbose = parseResult.GetValue(verboseOption); return await CommandHandlers.HandleVerifyKeylessAsync( serviceProvider, input, bundle, certificate, signature, rekorUuid, rekorUrl, issuer, subject, caBundle, verbose, ct); }); return command; } }