- 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.
233 lines
8.0 KiB
C#
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;
|
|
}
|
|
}
|