up
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
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
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user