up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
sm-remote-ci / build-and-test (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
devportal-offline / build-offline (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
oas-ci / oas-validate (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
sm-remote-ci / build-and-test (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Audit.ReplayToken;
|
||||
|
||||
/// <summary>
|
||||
/// Generates replay tokens using SHA-256 hashing with deterministic canonicalization.
|
||||
/// </summary>
|
||||
public sealed class Sha256ReplayTokenGenerator : IReplayTokenGenerator
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = false,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
private readonly ICryptoHash _cryptoHash;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public Sha256ReplayTokenGenerator(ICryptoHash cryptoHash, TimeProvider timeProvider)
|
||||
{
|
||||
_cryptoHash = cryptoHash ?? throw new ArgumentNullException(nameof(cryptoHash));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
}
|
||||
|
||||
public ReplayToken Generate(ReplayTokenRequest request)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var canonical = Canonicalize(request);
|
||||
var hashHex = ComputeHash(canonical);
|
||||
|
||||
return new ReplayToken(hashHex, _timeProvider.GetUtcNow());
|
||||
}
|
||||
|
||||
public bool Verify(ReplayToken token, ReplayTokenRequest request)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(token);
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var computed = Generate(request);
|
||||
return string.Equals(token.Value, computed.Value, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private string ComputeHash(string input)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(input);
|
||||
return _cryptoHash.ComputeHashHex(bytes, HashAlgorithms.Sha256);
|
||||
}
|
||||
|
||||
private static string? NormalizeValue(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return value.Trim();
|
||||
}
|
||||
|
||||
private static List<string> NormalizeSortedList(IReadOnlyList<string>? values)
|
||||
{
|
||||
if (values is null || values.Count == 0)
|
||||
{
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
var normalized = values
|
||||
.Where(static x => !string.IsNullOrWhiteSpace(x))
|
||||
.Select(static x => x.Trim())
|
||||
.OrderBy(static x => x, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> NormalizeSortedDictionary(IReadOnlyDictionary<string, string>? values)
|
||||
{
|
||||
if (values is null || values.Count == 0)
|
||||
{
|
||||
return new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
var normalized = values
|
||||
.Where(static kvp => !string.IsNullOrWhiteSpace(kvp.Key))
|
||||
.Select(static kvp => new KeyValuePair<string, string>(kvp.Key.Trim(), kvp.Value?.Trim() ?? string.Empty))
|
||||
.OrderBy(static kvp => kvp.Key, StringComparer.Ordinal)
|
||||
.ToDictionary(static kvp => kvp.Key, static kvp => kvp.Value, StringComparer.Ordinal);
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Produces deterministic canonical representation of inputs.
|
||||
/// </summary>
|
||||
private static string Canonicalize(ReplayTokenRequest request)
|
||||
{
|
||||
var canonical = new CanonicalReplayInput
|
||||
{
|
||||
Version = ReplayToken.DefaultVersion,
|
||||
FeedManifests = NormalizeSortedList(request.FeedManifests),
|
||||
RulesVersion = NormalizeValue(request.RulesVersion),
|
||||
RulesHash = NormalizeValue(request.RulesHash),
|
||||
LatticePolicyVersion = NormalizeValue(request.LatticePolicyVersion),
|
||||
LatticePolicyHash = NormalizeValue(request.LatticePolicyHash),
|
||||
InputHashes = NormalizeSortedList(request.InputHashes),
|
||||
ScoringConfigVersion = NormalizeValue(request.ScoringConfigVersion),
|
||||
EvidenceHashes = NormalizeSortedList(request.EvidenceHashes),
|
||||
AdditionalContext = NormalizeSortedDictionary(request.AdditionalContext)
|
||||
};
|
||||
|
||||
return JsonSerializer.Serialize(canonical, JsonOptions);
|
||||
}
|
||||
|
||||
private sealed class CanonicalReplayInput
|
||||
{
|
||||
public required string Version { get; init; }
|
||||
public required List<string> FeedManifests { get; init; }
|
||||
public string? RulesVersion { get; init; }
|
||||
public string? RulesHash { get; init; }
|
||||
public string? LatticePolicyVersion { get; init; }
|
||||
public string? LatticePolicyHash { get; init; }
|
||||
public required List<string> InputHashes { get; init; }
|
||||
public string? ScoringConfigVersion { get; init; }
|
||||
public required List<string> EvidenceHashes { get; init; }
|
||||
public required Dictionary<string, string> AdditionalContext { get; init; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user