Files
git.stella-ops.org/src/__Libraries/StellaOps.AdvisoryAI.Attestation/AiAttestationService.cs
2026-01-10 11:15:28 +02:00

275 lines
9.0 KiB
C#

// <copyright file="AiAttestationService.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using System.Collections.Concurrent;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using StellaOps.AdvisoryAI.Attestation.Models;
namespace StellaOps.AdvisoryAI.Attestation;
/// <summary>
/// In-memory implementation of AI attestation service.
/// Sprint: SPRINT_20260109_011_001 Task: AIAT-003
/// </summary>
/// <remarks>
/// This implementation stores attestations in memory. For production,
/// use a database-backed implementation with signing integration.
/// </remarks>
public sealed class AiAttestationService : IAiAttestationService
{
private readonly TimeProvider _timeProvider;
private readonly ILogger<AiAttestationService> _logger;
private readonly ConcurrentDictionary<string, StoredAttestation> _runAttestations = new();
private readonly ConcurrentDictionary<string, StoredAttestation> _claimAttestations = new();
public AiAttestationService(
TimeProvider timeProvider,
ILogger<AiAttestationService> logger)
{
_timeProvider = timeProvider;
_logger = logger;
}
/// <inheritdoc/>
public Task<AiAttestationResult> CreateRunAttestationAsync(
AiRunAttestation attestation,
bool sign = true,
CancellationToken ct = default)
{
var now = _timeProvider.GetUtcNow();
var digest = attestation.ComputeDigest();
var json = JsonSerializer.Serialize(attestation, AiAttestationJsonContext.Default.AiRunAttestation);
// In production, this would call the signer service
string? dsseEnvelope = null;
if (sign)
{
// Placeholder - real implementation would use StellaOps.Signer
dsseEnvelope = CreateMockDsseEnvelope(AiRunAttestation.PredicateType, json);
}
var stored = new StoredAttestation(
attestation.RunId,
AiRunAttestation.PredicateType,
json,
digest,
dsseEnvelope,
now);
_runAttestations[attestation.RunId] = stored;
_logger.LogInformation(
"Created run attestation {RunId} with digest {Digest}, signed={Signed}",
attestation.RunId,
digest,
sign);
return Task.FromResult(new AiAttestationResult
{
AttestationId = attestation.RunId,
Digest = digest,
Signed = sign,
DsseEnvelope = dsseEnvelope,
StorageUri = $"stella://ai-attestation/run/{attestation.RunId}",
CreatedAt = now
});
}
/// <inheritdoc/>
public Task<AiAttestationResult> CreateClaimAttestationAsync(
AiClaimAttestation attestation,
bool sign = true,
CancellationToken ct = default)
{
var now = _timeProvider.GetUtcNow();
var digest = attestation.ComputeDigest();
var json = JsonSerializer.Serialize(attestation, AiAttestationJsonContext.Default.AiClaimAttestation);
string? dsseEnvelope = null;
if (sign)
{
dsseEnvelope = CreateMockDsseEnvelope(AiClaimAttestation.PredicateType, json);
}
var stored = new StoredAttestation(
attestation.ClaimId,
AiClaimAttestation.PredicateType,
json,
digest,
dsseEnvelope,
now);
_claimAttestations[attestation.ClaimId] = stored;
_logger.LogDebug(
"Created claim attestation {ClaimId} for run {RunId}",
attestation.ClaimId,
attestation.RunId);
return Task.FromResult(new AiAttestationResult
{
AttestationId = attestation.ClaimId,
Digest = digest,
Signed = sign,
DsseEnvelope = dsseEnvelope,
StorageUri = $"stella://ai-attestation/claim/{attestation.ClaimId}",
CreatedAt = now
});
}
/// <inheritdoc/>
public Task<AiAttestationVerificationResult> VerifyRunAttestationAsync(
string runId,
CancellationToken ct = default)
{
var now = _timeProvider.GetUtcNow();
if (!_runAttestations.TryGetValue(runId, out var stored))
{
return Task.FromResult(AiAttestationVerificationResult.Failure(
now,
$"Run attestation {runId} not found"));
}
// Verify digest
var attestation = JsonSerializer.Deserialize(
stored.Json,
AiAttestationJsonContext.Default.AiRunAttestation);
if (attestation == null)
{
return Task.FromResult(AiAttestationVerificationResult.Failure(
now,
"Failed to deserialize attestation"));
}
var computedDigest = attestation.ComputeDigest();
if (computedDigest != stored.Digest)
{
return Task.FromResult(AiAttestationVerificationResult.Failure(
now,
"Digest mismatch",
digestValid: false));
}
// In production, verify signature via signer service
bool? signatureValid = stored.DsseEnvelope != null ? true : null;
_logger.LogDebug("Verified run attestation {RunId}", runId);
return Task.FromResult(AiAttestationVerificationResult.Success(
now,
stored.DsseEnvelope != null ? "ai-attestation-key" : null));
}
/// <inheritdoc/>
public Task<AiAttestationVerificationResult> VerifyClaimAttestationAsync(
string claimId,
CancellationToken ct = default)
{
var now = _timeProvider.GetUtcNow();
if (!_claimAttestations.TryGetValue(claimId, out var stored))
{
return Task.FromResult(AiAttestationVerificationResult.Failure(
now,
$"Claim attestation {claimId} not found"));
}
var attestation = JsonSerializer.Deserialize(
stored.Json,
AiAttestationJsonContext.Default.AiClaimAttestation);
if (attestation == null)
{
return Task.FromResult(AiAttestationVerificationResult.Failure(
now,
"Failed to deserialize attestation"));
}
var computedDigest = attestation.ComputeDigest();
if (computedDigest != stored.Digest)
{
return Task.FromResult(AiAttestationVerificationResult.Failure(
now,
"Digest mismatch",
digestValid: false));
}
return Task.FromResult(AiAttestationVerificationResult.Success(
now,
stored.DsseEnvelope != null ? "ai-attestation-key" : null));
}
/// <inheritdoc/>
public Task<AiRunAttestation?> GetRunAttestationAsync(
string runId,
CancellationToken ct = default)
{
if (!_runAttestations.TryGetValue(runId, out var stored))
{
return Task.FromResult<AiRunAttestation?>(null);
}
var attestation = JsonSerializer.Deserialize(
stored.Json,
AiAttestationJsonContext.Default.AiRunAttestation);
return Task.FromResult(attestation);
}
/// <inheritdoc/>
public Task<IReadOnlyList<AiClaimAttestation>> GetClaimAttestationsAsync(
string runId,
CancellationToken ct = default)
{
var claims = _claimAttestations.Values
.Select(s => JsonSerializer.Deserialize(s.Json, AiAttestationJsonContext.Default.AiClaimAttestation))
.Where(c => c != null && c.RunId == runId)
.Cast<AiClaimAttestation>()
.ToList();
return Task.FromResult<IReadOnlyList<AiClaimAttestation>>(claims);
}
/// <inheritdoc/>
public Task<IReadOnlyList<AiRunAttestation>> ListRecentAttestationsAsync(
string tenantId,
int limit = 100,
CancellationToken ct = default)
{
var attestations = _runAttestations.Values
.OrderByDescending(s => s.CreatedAt)
.Select(s => JsonSerializer.Deserialize(s.Json, AiAttestationJsonContext.Default.AiRunAttestation))
.Where(a => a != null && a.TenantId == tenantId)
.Cast<AiRunAttestation>()
.Take(limit)
.ToList();
return Task.FromResult<IReadOnlyList<AiRunAttestation>>(attestations);
}
private static string CreateMockDsseEnvelope(string predicateType, string payload)
{
// Mock DSSE envelope - real implementation would use StellaOps.Signer
var payloadBase64 = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(payload));
return $$"""
{
"payloadType": "{{predicateType}}",
"payload": "{{payloadBase64}}",
"signatures": [{"sig": "mock-signature"}]
}
""";
}
private sealed record StoredAttestation(
string Id,
string PredicateType,
string Json,
string Digest,
string? DsseEnvelope,
DateTimeOffset CreatedAt);
}