Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
sdk-generator-smoke / sdk-smoke (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
227 lines
8.0 KiB
C#
227 lines
8.0 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// DSSE signer implementation that uses StellaOps.Cryptography providers
|
|
/// for keyless (ephemeral) or KMS-backed signing operations.
|
|
/// Produces cosign-compatible DSSE envelopes.
|
|
/// </summary>
|
|
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<CryptoDsseSigner> _logger;
|
|
private readonly DsseSignerOptions _options;
|
|
|
|
public CryptoDsseSigner(
|
|
ICryptoProviderRegistry cryptoRegistry,
|
|
ISigningKeyResolver keyResolver,
|
|
IOptions<DsseSignerOptions> options,
|
|
ILogger<CryptoDsseSigner> 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<SigningBundle> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<string> BuildCertificateChain(
|
|
ICryptoSigner signer,
|
|
SigningKeyResolution keyResolution)
|
|
{
|
|
var chain = new List<string>();
|
|
|
|
// 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;
|
|
}
|
|
}
|