- 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.
206 lines
7.0 KiB
C#
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);
|