using System; using System.Collections.Generic; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using StellaOps.Cryptography; using StellaOps.Signer.Core; namespace StellaOps.Signer.Infrastructure.Signing; /// /// DSSE signer implementation that uses StellaOps.Cryptography providers /// for keyless (ephemeral) or KMS-backed signing operations. /// Produces cosign-compatible DSSE envelopes. /// public sealed class CryptoDsseSigner : IDsseSigner { private const string DssePayloadType = "application/vnd.in-toto+json"; private const string PreAuthenticationEncodingPrefix = "DSSEv1"; private readonly ICryptoProviderRegistry _cryptoRegistry; private readonly ISigningKeyResolver _keyResolver; private readonly ILogger _logger; private readonly DsseSignerOptions _options; public CryptoDsseSigner( ICryptoProviderRegistry cryptoRegistry, ISigningKeyResolver keyResolver, IOptions options, ILogger logger) { _cryptoRegistry = cryptoRegistry ?? throw new ArgumentNullException(nameof(cryptoRegistry)); _keyResolver = keyResolver ?? throw new ArgumentNullException(nameof(keyResolver)); _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public async ValueTask SignAsync( SigningRequest request, ProofOfEntitlementResult entitlement, CallerContext caller, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request); ArgumentNullException.ThrowIfNull(entitlement); ArgumentNullException.ThrowIfNull(caller); var signingMode = request.Options.Mode; var algorithmId = ResolveAlgorithm(signingMode); _logger.LogDebug( "Starting DSSE signing for tenant {Tenant} with mode {Mode} and algorithm {Algorithm}", caller.Tenant, signingMode, algorithmId); // Build the in-toto statement payload var statementPayload = SignerStatementBuilder.BuildStatementPayload(request); // Encode payload as base64url for DSSE var payloadBase64 = Convert.ToBase64String(statementPayload) .TrimEnd('=') .Replace('+', '-') .Replace('/', '_'); // Build PAE (Pre-Authentication Encoding) for signing var paeBytes = BuildPae(DssePayloadType, statementPayload); // Resolve signing key and provider var keyResolution = await _keyResolver .ResolveKeyAsync(signingMode, caller.Tenant, cancellationToken) .ConfigureAwait(false); var keyReference = new CryptoKeyReference(keyResolution.KeyId, keyResolution.ProviderHint); // Get signer from crypto registry var signerResolution = _cryptoRegistry.ResolveSigner( CryptoCapability.Signing, algorithmId, keyReference, keyResolution.ProviderHint); var signer = signerResolution.Signer; // Sign the PAE var signatureBytes = await signer .SignAsync(paeBytes, cancellationToken) .ConfigureAwait(false); // Encode signature as base64url (cosign-compatible) var signatureBase64 = Convert.ToBase64String(signatureBytes) .TrimEnd('=') .Replace('+', '-') .Replace('/', '_'); _logger.LogInformation( "DSSE signing completed for tenant {Tenant} using provider {Provider} with key {KeyId}", caller.Tenant, signerResolution.ProviderName, signer.KeyId); // Build certificate chain if available var certChain = BuildCertificateChain(signer, keyResolution); // Build DSSE envelope var envelope = new DsseEnvelope( Payload: payloadBase64, PayloadType: DssePayloadType, Signatures: [ new DsseSignature( Signature: signatureBase64, KeyId: signer.KeyId) ]); // Build signing metadata var identity = new SigningIdentity( Mode: signingMode.ToString().ToLowerInvariant(), Issuer: keyResolution.Issuer ?? _options.DefaultIssuer, Subject: keyResolution.Subject ?? caller.Subject, ExpiresAtUtc: keyResolution.ExpiresAtUtc); var metadata = new SigningMetadata( Identity: identity, CertificateChain: certChain, ProviderName: signerResolution.ProviderName, AlgorithmId: algorithmId); return new SigningBundle(envelope, metadata); } /// /// Builds the PAE (Pre-Authentication Encoding) as per DSSE specification. /// PAE = "DSSEv1" || SP || LEN(type) || SP || type || SP || LEN(payload) || SP || payload /// where SP is space (0x20) and LEN is decimal ASCII length. /// private static byte[] BuildPae(string payloadType, byte[] payload) { var typeBytes = Encoding.UTF8.GetBytes(payloadType); // Calculate total length var prefixBytes = Encoding.UTF8.GetBytes(PreAuthenticationEncodingPrefix); var typeLenStr = typeBytes.Length.ToString(); var payloadLenStr = payload.Length.ToString(); var totalLen = prefixBytes.Length + 1 + typeLenStr.Length + 1 + typeBytes.Length + 1 + payloadLenStr.Length + 1 + payload.Length; var pae = new byte[totalLen]; var offset = 0; // DSSEv1 Buffer.BlockCopy(prefixBytes, 0, pae, offset, prefixBytes.Length); offset += prefixBytes.Length; pae[offset++] = 0x20; // space // LEN(type) var typeLenBytes = Encoding.UTF8.GetBytes(typeLenStr); Buffer.BlockCopy(typeLenBytes, 0, pae, offset, typeLenBytes.Length); offset += typeLenBytes.Length; pae[offset++] = 0x20; // space // type Buffer.BlockCopy(typeBytes, 0, pae, offset, typeBytes.Length); offset += typeBytes.Length; pae[offset++] = 0x20; // space // LEN(payload) var payloadLenBytes = Encoding.UTF8.GetBytes(payloadLenStr); Buffer.BlockCopy(payloadLenBytes, 0, pae, offset, payloadLenBytes.Length); offset += payloadLenBytes.Length; pae[offset++] = 0x20; // space // payload Buffer.BlockCopy(payload, 0, pae, offset, payload.Length); return pae; } private string ResolveAlgorithm(SigningMode mode) { return mode switch { SigningMode.Keyless => _options.KeylessAlgorithm ?? SignatureAlgorithms.Es256, SigningMode.Kms => _options.KmsAlgorithm ?? SignatureAlgorithms.Es256, _ => SignatureAlgorithms.Es256 }; } private static IReadOnlyList BuildCertificateChain( ICryptoSigner signer, SigningKeyResolution keyResolution) { var chain = new List(); // Export public key as JWK for verification try { var jwk = signer.ExportPublicJsonWebKey(); if (jwk is not null) { // Convert JWK to PEM-like representation for certificate chain // In keyless mode, this represents the ephemeral signing certificate var jwkJson = System.Text.Json.JsonSerializer.Serialize(jwk); chain.Add(Convert.ToBase64String(Encoding.UTF8.GetBytes(jwkJson))); } } catch { // Some signers may not support JWK export } // Add any additional certificates from key resolution if (keyResolution.CertificateChain is { Count: > 0 }) { chain.AddRange(keyResolution.CertificateChain); } return chain; } }