// // Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later. // using System.Collections.Concurrent; using System.Text.Json; using Microsoft.Extensions.Logging; using StellaOps.AdvisoryAI.Attestation.Models; namespace StellaOps.AdvisoryAI.Attestation; /// /// In-memory implementation of AI attestation service. /// Sprint: SPRINT_20260109_011_001 Task: AIAT-003 /// /// /// This implementation stores attestations in memory. For production, /// use a database-backed implementation with signing integration. /// public sealed class AiAttestationService : IAiAttestationService { private readonly TimeProvider _timeProvider; private readonly ILogger _logger; private readonly ConcurrentDictionary _runAttestations = new(); private readonly ConcurrentDictionary _claimAttestations = new(); public AiAttestationService( TimeProvider timeProvider, ILogger logger) { _timeProvider = timeProvider; _logger = logger; } /// public Task 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 }); } /// public Task 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 }); } /// public Task 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)); } /// public Task 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)); } /// public Task GetRunAttestationAsync( string runId, CancellationToken ct = default) { if (!_runAttestations.TryGetValue(runId, out var stored)) { return Task.FromResult(null); } var attestation = JsonSerializer.Deserialize( stored.Json, AiAttestationJsonContext.Default.AiRunAttestation); return Task.FromResult(attestation); } /// public Task> 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() .ToList(); return Task.FromResult>(claims); } /// public Task> 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() .Take(limit) .ToList(); return Task.FromResult>(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); }