using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; using StellaOps.Attestor.Envelope; using StellaOps.Canonical.Json; namespace StellaOps.Scanner.Reachability.Witnesses; /// /// Service for creating and verifying DSSE-signed suppression witness envelopes. /// Sprint: SPRINT_20260106_001_002 (SUP-015) /// public sealed class SuppressionDsseSigner : ISuppressionDsseSigner { private readonly EnvelopeSignatureService _signatureService; private static readonly JsonSerializerOptions CanonicalJsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, WriteIndented = false, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, Encoder = JavaScriptEncoder.Default }; /// /// Creates a new SuppressionDsseSigner with the specified signature service. /// public SuppressionDsseSigner(EnvelopeSignatureService signatureService) { _signatureService = signatureService ?? throw new ArgumentNullException(nameof(signatureService)); } /// /// Creates a new SuppressionDsseSigner with a default signature service. /// public SuppressionDsseSigner() : this(new EnvelopeSignatureService()) { } /// public SuppressionDsseResult SignWitness(SuppressionWitness witness, EnvelopeKey signingKey, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(witness); ArgumentNullException.ThrowIfNull(signingKey); cancellationToken.ThrowIfCancellationRequested(); try { // Serialize witness to canonical JSON bytes var payloadBytes = CanonJson.Canonicalize(witness, CanonicalJsonOptions); // Sign DSSE payload using the shared PAE helper var signResult = _signatureService.SignDsse(SuppressionWitnessSchema.DssePayloadType, payloadBytes, signingKey, cancellationToken); if (!signResult.IsSuccess) { return SuppressionDsseResult.Failure($"Signing failed: {signResult.Error?.Message}"); } var signature = signResult.Value; // Create the DSSE envelope var dsseSignature = new DsseSignature( signature: Convert.ToBase64String(signature.Value.Span), keyId: signature.KeyId); var envelope = new DsseEnvelope( payloadType: SuppressionWitnessSchema.DssePayloadType, payload: payloadBytes, signatures: [dsseSignature]); return SuppressionDsseResult.Success(envelope, payloadBytes); } catch (Exception ex) when (ex is JsonException or InvalidOperationException) { return SuppressionDsseResult.Failure($"Failed to create DSSE envelope: {ex.Message}"); } } /// public SuppressionVerifyResult VerifyWitness(DsseEnvelope envelope, EnvelopeKey publicKey, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(envelope); ArgumentNullException.ThrowIfNull(publicKey); cancellationToken.ThrowIfCancellationRequested(); try { // Verify payload type if (!string.Equals(envelope.PayloadType, SuppressionWitnessSchema.DssePayloadType, StringComparison.Ordinal)) { return SuppressionVerifyResult.Failure($"Invalid payload type: expected '{SuppressionWitnessSchema.DssePayloadType}', got '{envelope.PayloadType}'"); } // Deserialize the witness from payload var witness = JsonSerializer.Deserialize(envelope.Payload.Span, CanonicalJsonOptions); if (witness is null) { return SuppressionVerifyResult.Failure("Failed to deserialize witness from payload"); } // Verify schema version if (!string.Equals(witness.WitnessSchema, SuppressionWitnessSchema.Version, StringComparison.Ordinal)) { return SuppressionVerifyResult.Failure($"Unsupported witness schema: {witness.WitnessSchema}"); } // Find signature matching the public key var matchingSignature = envelope.Signatures.FirstOrDefault( s => string.Equals(s.KeyId, publicKey.KeyId, StringComparison.Ordinal)); if (matchingSignature is null) { return SuppressionVerifyResult.Failure($"No signature found for key ID: {publicKey.KeyId}"); } var signatureBytes = Convert.FromBase64String(matchingSignature.Signature); var envelopeSignature = new EnvelopeSignature(publicKey.KeyId, publicKey.AlgorithmId, signatureBytes); var verifyResult = _signatureService.VerifyDsse(envelope.PayloadType, envelope.Payload.Span, envelopeSignature, publicKey, cancellationToken); if (!verifyResult.IsSuccess) { return SuppressionVerifyResult.Failure($"Signature verification failed: {verifyResult.Error?.Message}"); } return SuppressionVerifyResult.Success(witness, matchingSignature.KeyId!); } catch (Exception ex) when (ex is JsonException or FormatException or InvalidOperationException) { return SuppressionVerifyResult.Failure($"Verification failed: {ex.Message}"); } } } /// /// Result of DSSE signing a suppression witness. /// public sealed record SuppressionDsseResult { public bool IsSuccess { get; init; } public DsseEnvelope? Envelope { get; init; } public byte[]? PayloadBytes { get; init; } public string? Error { get; init; } public static SuppressionDsseResult Success(DsseEnvelope envelope, byte[] payloadBytes) => new() { IsSuccess = true, Envelope = envelope, PayloadBytes = payloadBytes }; public static SuppressionDsseResult Failure(string error) => new() { IsSuccess = false, Error = error }; } /// /// Result of verifying a DSSE-signed suppression witness. /// public sealed record SuppressionVerifyResult { public bool IsSuccess { get; init; } public SuppressionWitness? Witness { get; init; } public string? VerifiedKeyId { get; init; } public string? Error { get; init; } public static SuppressionVerifyResult Success(SuppressionWitness witness, string keyId) => new() { IsSuccess = true, Witness = witness, VerifiedKeyId = keyId }; public static SuppressionVerifyResult Failure(string error) => new() { IsSuccess = false, Error = error }; }