Refactor code structure for improved readability and maintainability; optimize performance in key functions.
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Attestor.ProofChain.Predicates;
|
||||
using StellaOps.Attestor.ProofChain.Statements;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Replay.Core;
|
||||
using StellaOps.Scanner.Cache.Abstractions;
|
||||
using StellaOps.Scanner.ProofSpine;
|
||||
using StellaOps.Scanner.Reachability.Subgraph;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability.Attestation;
|
||||
|
||||
public sealed class ReachabilitySubgraphPublisher : IReachabilitySubgraphPublisher
|
||||
{
|
||||
private static readonly JsonSerializerOptions DsseJsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
private readonly ReachabilitySubgraphOptions _options;
|
||||
private readonly ICryptoHash _cryptoHash;
|
||||
private readonly ILogger<ReachabilitySubgraphPublisher> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IFileContentAddressableStore? _cas;
|
||||
private readonly IDsseSigningService? _dsseSigningService;
|
||||
private readonly ICryptoProfile? _cryptoProfile;
|
||||
|
||||
public ReachabilitySubgraphPublisher(
|
||||
IOptions<ReachabilitySubgraphOptions> options,
|
||||
ICryptoHash cryptoHash,
|
||||
ILogger<ReachabilitySubgraphPublisher> logger,
|
||||
TimeProvider? timeProvider = null,
|
||||
IFileContentAddressableStore? cas = null,
|
||||
IDsseSigningService? dsseSigningService = null,
|
||||
ICryptoProfile? cryptoProfile = null)
|
||||
{
|
||||
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
_cryptoHash = cryptoHash ?? throw new ArgumentNullException(nameof(cryptoHash));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_cas = cas;
|
||||
_dsseSigningService = dsseSigningService;
|
||||
_cryptoProfile = cryptoProfile;
|
||||
}
|
||||
|
||||
public async Task<ReachabilitySubgraphPublishResult> PublishAsync(
|
||||
ReachabilitySubgraph subgraph,
|
||||
string subjectDigest,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(subgraph);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(subjectDigest);
|
||||
|
||||
if (!_options.Enabled)
|
||||
{
|
||||
_logger.LogDebug("Reachability subgraph attestation disabled");
|
||||
return new ReachabilitySubgraphPublishResult(
|
||||
SubgraphDigest: string.Empty,
|
||||
CasUri: null,
|
||||
AttestationDigest: string.Empty,
|
||||
DsseEnvelopeBytes: Array.Empty<byte>());
|
||||
}
|
||||
|
||||
var normalized = subgraph.Normalize();
|
||||
var subgraphBytes = CanonicalJson.SerializeToUtf8Bytes(normalized);
|
||||
var subgraphDigest = _cryptoHash.ComputePrefixedHashForPurpose(subgraphBytes, HashPurpose.Graph);
|
||||
|
||||
string? casUri = null;
|
||||
if (_options.StoreInCas)
|
||||
{
|
||||
casUri = await StoreSubgraphAsync(subgraphBytes, subgraphDigest, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var statement = BuildStatement(normalized, subgraphDigest, casUri, subjectDigest);
|
||||
var statementBytes = CanonicalJson.SerializeToUtf8Bytes(statement);
|
||||
|
||||
var (envelope, envelopeBytes) = await CreateDsseEnvelopeAsync(statement, statementBytes, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var attestationDigest = _cryptoHash.ComputePrefixedHashForPurpose(envelopeBytes, HashPurpose.Attestation);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Created reachability subgraph attestation: graphDigest={GraphDigest}, attestationDigest={AttestationDigest}",
|
||||
subgraphDigest,
|
||||
attestationDigest);
|
||||
|
||||
return new ReachabilitySubgraphPublishResult(
|
||||
SubgraphDigest: subgraphDigest,
|
||||
CasUri: casUri,
|
||||
AttestationDigest: attestationDigest,
|
||||
DsseEnvelopeBytes: envelopeBytes);
|
||||
}
|
||||
|
||||
private ReachabilitySubgraphStatement BuildStatement(
|
||||
ReachabilitySubgraph subgraph,
|
||||
string subgraphDigest,
|
||||
string? casUri,
|
||||
string subjectDigest)
|
||||
{
|
||||
var analysis = subgraph.AnalysisMetadata;
|
||||
var predicate = new ReachabilitySubgraphPredicate
|
||||
{
|
||||
SchemaVersion = subgraph.Version,
|
||||
GraphDigest = subgraphDigest,
|
||||
GraphCasUri = casUri,
|
||||
FindingKeys = subgraph.FindingKeys,
|
||||
Analysis = new ReachabilitySubgraphAnalysis
|
||||
{
|
||||
Analyzer = analysis?.Analyzer ?? "reachability",
|
||||
AnalyzerVersion = analysis?.AnalyzerVersion ?? "unknown",
|
||||
Confidence = analysis?.Confidence ?? 0.5,
|
||||
Completeness = analysis?.Completeness ?? "partial",
|
||||
GeneratedAt = analysis?.GeneratedAt ?? _timeProvider.GetUtcNow(),
|
||||
HashAlgorithm = _cryptoHash.GetAlgorithmForPurpose(HashPurpose.Graph)
|
||||
}
|
||||
};
|
||||
|
||||
return new ReachabilitySubgraphStatement
|
||||
{
|
||||
Subject =
|
||||
[
|
||||
BuildSubject(subjectDigest)
|
||||
],
|
||||
Predicate = predicate
|
||||
};
|
||||
}
|
||||
|
||||
private static Subject BuildSubject(string digest)
|
||||
{
|
||||
var (algorithm, value) = SplitDigest(digest);
|
||||
return new Subject
|
||||
{
|
||||
Name = digest,
|
||||
Digest = new Dictionary<string, string> { [algorithm] = value }
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<string?> StoreSubgraphAsync(byte[] subgraphBytes, string subgraphDigest, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_cas is null)
|
||||
{
|
||||
_logger.LogWarning("CAS storage requested but no CAS store configured; skipping subgraph storage.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var key = ExtractHashDigest(subgraphDigest);
|
||||
var existing = await _cas.TryGetAsync(key, cancellationToken).ConfigureAwait(false);
|
||||
if (existing is null)
|
||||
{
|
||||
await using var stream = new MemoryStream(subgraphBytes, writable: false);
|
||||
await _cas.PutAsync(new FileCasPutRequest(key, stream, leaveOpen: false), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return $"cas://reachability/subgraphs/{key}";
|
||||
}
|
||||
|
||||
private async Task<(DsseEnvelope Envelope, byte[] EnvelopeBytes)> CreateDsseEnvelopeAsync(
|
||||
ReachabilitySubgraphStatement statement,
|
||||
byte[] statementBytes,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
const string payloadType = "application/vnd.in-toto+json";
|
||||
|
||||
if (_dsseSigningService is not null)
|
||||
{
|
||||
var profile = _cryptoProfile ?? new InlineCryptoProfile(_options.SigningKeyId ?? "scanner-deterministic", "hs256");
|
||||
var signed = await _dsseSigningService.SignAsync(statement, payloadType, profile, cancellationToken).ConfigureAwait(false);
|
||||
return (signed, SerializeDsseEnvelope(signed));
|
||||
}
|
||||
|
||||
var signature = SHA256.HashData(statementBytes);
|
||||
var envelope = new DsseEnvelope(
|
||||
payloadType,
|
||||
Convert.ToBase64String(statementBytes),
|
||||
new[] { new DsseSignature(_options.SigningKeyId ?? "scanner-deterministic", Convert.ToBase64String(signature)) });
|
||||
return (envelope, SerializeDsseEnvelope(envelope));
|
||||
}
|
||||
|
||||
private static byte[] SerializeDsseEnvelope(DsseEnvelope envelope)
|
||||
{
|
||||
var signatures = envelope.Signatures
|
||||
.OrderBy(s => s.KeyId, StringComparer.Ordinal)
|
||||
.ThenBy(s => s.Sig, StringComparer.Ordinal)
|
||||
.Select(s => new { keyid = s.KeyId, sig = s.Sig })
|
||||
.ToArray();
|
||||
|
||||
var dto = new
|
||||
{
|
||||
payloadType = envelope.PayloadType,
|
||||
payload = envelope.Payload,
|
||||
signatures
|
||||
};
|
||||
|
||||
return JsonSerializer.SerializeToUtf8Bytes(dto, DsseJsonOptions);
|
||||
}
|
||||
|
||||
private static string ExtractHashDigest(string prefixedHash)
|
||||
{
|
||||
var colonIndex = prefixedHash.IndexOf(':');
|
||||
return colonIndex >= 0 ? prefixedHash[(colonIndex + 1)..] : prefixedHash;
|
||||
}
|
||||
|
||||
private static (string Algorithm, string Value) SplitDigest(string digest)
|
||||
{
|
||||
var colonIndex = digest.IndexOf(':');
|
||||
if (colonIndex <= 0 || colonIndex == digest.Length - 1)
|
||||
{
|
||||
return ("sha256", digest);
|
||||
}
|
||||
|
||||
return (digest[..colonIndex], digest[(colonIndex + 1)..]);
|
||||
}
|
||||
|
||||
private sealed record InlineCryptoProfile(string KeyId, string Algorithm) : ICryptoProfile;
|
||||
}
|
||||
Reference in New Issue
Block a user