// 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;
}
}