Refactor code structure for improved readability and maintainability; optimize performance in key functions.
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.DeltaVerdict.Models;
|
||||
using StellaOps.DeltaVerdict.Serialization;
|
||||
|
||||
namespace StellaOps.DeltaVerdict.Signing;
|
||||
|
||||
public interface IDeltaSigningService
|
||||
{
|
||||
Task<DeltaVerdict.Models.DeltaVerdict> SignAsync(
|
||||
DeltaVerdict.Models.DeltaVerdict delta,
|
||||
SigningOptions options,
|
||||
CancellationToken ct = default);
|
||||
|
||||
Task<VerificationResult> VerifyAsync(
|
||||
DeltaVerdict.Models.DeltaVerdict delta,
|
||||
VerificationOptions options,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public sealed class DeltaSigningService : IDeltaSigningService
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
public Task<DeltaVerdict.Models.DeltaVerdict> SignAsync(
|
||||
DeltaVerdict.Models.DeltaVerdict delta,
|
||||
SigningOptions options,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(delta);
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
var withDigest = DeltaVerdictSerializer.WithDigest(delta);
|
||||
var payloadJson = DeltaVerdictSerializer.Serialize(withDigest with { Signature = null });
|
||||
var payloadBytes = Encoding.UTF8.GetBytes(payloadJson);
|
||||
var envelope = BuildEnvelope(payloadBytes, options);
|
||||
var envelopeJson = JsonSerializer.Serialize(envelope, JsonOptions);
|
||||
|
||||
return Task.FromResult(withDigest with { Signature = envelopeJson });
|
||||
}
|
||||
|
||||
public Task<VerificationResult> VerifyAsync(
|
||||
DeltaVerdict.Models.DeltaVerdict delta,
|
||||
VerificationOptions options,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(delta);
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
if (string.IsNullOrEmpty(delta.Signature))
|
||||
{
|
||||
return Task.FromResult(VerificationResult.Fail("Delta is not signed"));
|
||||
}
|
||||
|
||||
DsseEnvelope? envelope;
|
||||
try
|
||||
{
|
||||
envelope = JsonSerializer.Deserialize<DsseEnvelope>(delta.Signature, JsonOptions);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
return Task.FromResult(VerificationResult.Fail($"Invalid signature envelope: {ex.Message}"));
|
||||
}
|
||||
|
||||
if (envelope is null)
|
||||
{
|
||||
return Task.FromResult(VerificationResult.Fail("Signature envelope is empty"));
|
||||
}
|
||||
|
||||
var payloadBytes = Convert.FromBase64String(envelope.Payload);
|
||||
var pae = BuildPae(envelope.PayloadType, payloadBytes);
|
||||
var expectedSig = ComputeSignature(pae, options);
|
||||
|
||||
var matched = envelope.Signatures.Any(sig =>
|
||||
string.Equals(sig.KeyId, options.KeyId, StringComparison.Ordinal)
|
||||
&& string.Equals(sig.Sig, expectedSig, StringComparison.Ordinal));
|
||||
|
||||
if (!matched)
|
||||
{
|
||||
return Task.FromResult(VerificationResult.Fail("Signature verification failed"));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(delta.DeltaDigest))
|
||||
{
|
||||
var computed = DeltaVerdictSerializer.ComputeDigest(delta);
|
||||
if (!string.Equals(computed, delta.DeltaDigest, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Task.FromResult(VerificationResult.Fail("Delta digest mismatch"));
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult(VerificationResult.Success());
|
||||
}
|
||||
|
||||
private static DsseEnvelope BuildEnvelope(byte[] payload, SigningOptions options)
|
||||
{
|
||||
var pae = BuildPae(options.PayloadType, payload);
|
||||
var signature = ComputeSignature(pae, options);
|
||||
|
||||
return new DsseEnvelope(
|
||||
options.PayloadType,
|
||||
Convert.ToBase64String(payload),
|
||||
[new DsseSignature(options.KeyId, signature)]);
|
||||
}
|
||||
|
||||
private static string ComputeSignature(byte[] pae, SigningOptions options)
|
||||
{
|
||||
return options.Algorithm switch
|
||||
{
|
||||
SigningAlgorithm.HmacSha256 => ComputeHmac(pae, options.SecretBase64),
|
||||
SigningAlgorithm.Sha256 => Convert.ToBase64String(SHA256.HashData(pae)),
|
||||
_ => throw new InvalidOperationException($"Unsupported signing algorithm: {options.Algorithm}")
|
||||
};
|
||||
}
|
||||
|
||||
private static string ComputeHmac(byte[] data, string? secretBase64)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(secretBase64))
|
||||
{
|
||||
throw new InvalidOperationException("HMAC signing requires a base64 secret.");
|
||||
}
|
||||
|
||||
var secret = Convert.FromBase64String(secretBase64);
|
||||
using var hmac = new HMACSHA256(secret);
|
||||
var sig = hmac.ComputeHash(data);
|
||||
return Convert.ToBase64String(sig);
|
||||
}
|
||||
|
||||
private static byte[] BuildPae(string payloadType, byte[] payload)
|
||||
{
|
||||
var prefix = "DSSEv1";
|
||||
var typeBytes = Encoding.UTF8.GetBytes(payloadType);
|
||||
var prefixBytes = Encoding.UTF8.GetBytes(prefix);
|
||||
var lengthType = Encoding.UTF8.GetBytes(typeBytes.Length.ToString());
|
||||
var lengthPayload = Encoding.UTF8.GetBytes(payload.Length.ToString());
|
||||
|
||||
using var stream = new MemoryStream();
|
||||
stream.Write(prefixBytes);
|
||||
stream.WriteByte((byte)' ');
|
||||
stream.Write(lengthType);
|
||||
stream.WriteByte((byte)' ');
|
||||
stream.Write(typeBytes);
|
||||
stream.WriteByte((byte)' ');
|
||||
stream.Write(lengthPayload);
|
||||
stream.WriteByte((byte)' ');
|
||||
stream.Write(payload);
|
||||
return stream.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record SigningOptions
|
||||
{
|
||||
public required string KeyId { get; init; }
|
||||
public SigningAlgorithm Algorithm { get; init; } = SigningAlgorithm.HmacSha256;
|
||||
public string? SecretBase64 { get; init; }
|
||||
public string PayloadType { get; init; } = "application/vnd.stellaops.delta-verdict+json";
|
||||
}
|
||||
|
||||
public sealed record VerificationOptions
|
||||
{
|
||||
public required string KeyId { get; init; }
|
||||
public SigningAlgorithm Algorithm { get; init; } = SigningAlgorithm.HmacSha256;
|
||||
public string? SecretBase64 { get; init; }
|
||||
}
|
||||
|
||||
public enum SigningAlgorithm
|
||||
{
|
||||
HmacSha256,
|
||||
Sha256
|
||||
}
|
||||
|
||||
public sealed record VerificationResult
|
||||
{
|
||||
public required bool IsValid { get; init; }
|
||||
public string? Error { get; init; }
|
||||
|
||||
public static VerificationResult Success() => new() { IsValid = true };
|
||||
public static VerificationResult Fail(string error) => new() { IsValid = false, Error = error };
|
||||
}
|
||||
|
||||
public sealed record DsseEnvelope(
|
||||
string PayloadType,
|
||||
string Payload,
|
||||
IReadOnlyList<DsseSignature> Signatures);
|
||||
|
||||
public sealed record DsseSignature(string KeyId, string Sig);
|
||||
Reference in New Issue
Block a user