Files
git.stella-ops.org/src/__Libraries/StellaOps.DeltaVerdict/Signing/DeltaSigningService.cs
StellaOps Bot 5146204f1b feat: add security sink detection patterns for JavaScript/TypeScript
- Introduced `sink-detect.js` with various security sink detection patterns categorized by type (e.g., command injection, SQL injection, file operations).
- Implemented functions to build a lookup map for fast sink detection and to match sink calls against known patterns.
- Added `package-lock.json` for dependency management.
2025-12-22 23:21:21 +02:00

206 lines
7.0 KiB
C#

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 ComputeSignatureCore(pae, options.Algorithm, options.SecretBase64);
}
private static string ComputeSignature(byte[] pae, VerificationOptions options)
{
return ComputeSignatureCore(pae, options.Algorithm, options.SecretBase64);
}
private static string ComputeSignatureCore(byte[] pae, SigningAlgorithm algorithm, string? secretBase64)
{
return algorithm switch
{
SigningAlgorithm.HmacSha256 => ComputeHmac(pae, secretBase64),
SigningAlgorithm.Sha256 => Convert.ToBase64String(SHA256.HashData(pae)),
_ => throw new InvalidOperationException($"Unsupported signing algorithm: {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);