Files
git.stella-ops.org/src/Cli/StellaOps.Cli/Commands/SignCommandGroup.cs
StellaOps Bot 907783f625 Add property-based tests for SBOM/VEX document ordering and Unicode normalization determinism
- Implement `SbomVexOrderingDeterminismProperties` for testing component list and vulnerability metadata hash consistency.
- Create `UnicodeNormalizationDeterminismProperties` to validate NFC normalization and Unicode string handling.
- Add project file for `StellaOps.Testing.Determinism.Properties` with necessary dependencies.
- Introduce CI/CD template validation tests including YAML syntax checks and documentation content verification.
- Create validation script for CI/CD templates ensuring all required files and structures are present.
2025-12-26 15:17:58 +02:00

233 lines
8.0 KiB
C#

// 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;
/// <summary>
/// CLI commands for Sigstore keyless signing operations.
/// Supports self-hosted Sigstore (Fulcio + Rekor) for on-premise deployments.
/// </summary>
internal static class SignCommandGroup
{
/// <summary>
/// Build the sign command with keyless/traditional subcommands.
/// </summary>
public static Command BuildSignCommand(
IServiceProvider serviceProvider,
Option<bool> 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<bool> verboseOption,
CancellationToken cancellationToken)
{
var command = new Command("keyless", "Sign artifact using Sigstore keyless signing (Fulcio + Rekor)");
var inputOption = new Option<string>("--input")
{
Description = "Path to file or artifact to sign",
Required = true
};
command.Add(inputOption);
var outputOption = new Option<string?>("--output")
{
Description = "Output path for signature bundle (defaults to <input>.sigstore)"
};
command.Add(outputOption);
var identityTokenOption = new Option<string?>("--identity-token")
{
Description = "OIDC identity token (JWT). If not provided, attempts ambient credential detection."
};
command.Add(identityTokenOption);
var rekorOption = new Option<bool>("--rekor")
{
Description = "Upload signature to Rekor transparency log (default: true)",
DefaultValue = true
};
command.Add(rekorOption);
var fulcioUrlOption = new Option<string?>("--fulcio-url")
{
Description = "Override Fulcio URL (for self-hosted Sigstore)"
};
command.Add(fulcioUrlOption);
var rekorUrlOption = new Option<string?>("--rekor-url")
{
Description = "Override Rekor URL (for self-hosted Sigstore)"
};
command.Add(rekorUrlOption);
var oidcIssuerOption = new Option<string?>("--oidc-issuer")
{
Description = "OIDC issuer URL for identity verification"
};
command.Add(oidcIssuerOption);
var bundleFormatOption = new Option<string>("--bundle-format")
{
Description = "Output bundle format: sigstore, cosign-bundle, dsse (default: sigstore)",
DefaultValue = "sigstore"
};
command.Add(bundleFormatOption);
var caBundleOption = new Option<string?>("--ca-bundle")
{
Description = "Path to custom CA certificate bundle for self-hosted TLS"
};
command.Add(caBundleOption);
var insecureOption = new Option<bool>("--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<bool> verboseOption,
CancellationToken cancellationToken)
{
var command = new Command("verify-keyless", "Verify a keyless signature against Sigstore");
var inputOption = new Option<string>("--input")
{
Description = "Path to file or artifact to verify",
Required = true
};
command.Add(inputOption);
var bundleOption = new Option<string?>("--bundle")
{
Description = "Path to Sigstore bundle file (defaults to <input>.sigstore)"
};
command.Add(bundleOption);
var certificateOption = new Option<string?>("--certificate")
{
Description = "Path to signing certificate (PEM format)"
};
command.Add(certificateOption);
var signatureOption = new Option<string?>("--signature")
{
Description = "Path to detached signature"
};
command.Add(signatureOption);
var rekorUuidOption = new Option<string?>("--rekor-uuid")
{
Description = "Rekor entry UUID for transparency verification"
};
command.Add(rekorUuidOption);
var rekorUrlOption = new Option<string?>("--rekor-url")
{
Description = "Override Rekor URL (for self-hosted Sigstore)"
};
command.Add(rekorUrlOption);
var issuerOption = new Option<string?>("--certificate-issuer")
{
Description = "Expected OIDC issuer in certificate"
};
command.Add(issuerOption);
var subjectOption = new Option<string?>("--certificate-subject")
{
Description = "Expected subject (email/identity) in certificate"
};
command.Add(subjectOption);
var caBundleOption = new Option<string?>("--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;
}
}