up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("StellaOps.Attestor.Tests")]
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("StellaOps.Attestor.Tests")]
|
||||
|
||||
@@ -1,157 +1,157 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Attestor.Core.Rekor;
|
||||
using StellaOps.Attestor.Core.Submission;
|
||||
|
||||
namespace StellaOps.Attestor.Infrastructure.Rekor;
|
||||
|
||||
internal sealed class HttpRekorClient : IRekorClient
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<HttpRekorClient> _logger;
|
||||
|
||||
public HttpRekorClient(HttpClient httpClient, ILogger<HttpRekorClient> logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<RekorSubmissionResponse> SubmitAsync(AttestorSubmissionRequest request, RekorBackend backend, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var submissionUri = BuildUri(backend.Url, "api/v2/log/entries");
|
||||
|
||||
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, submissionUri)
|
||||
{
|
||||
Content = JsonContent.Create(BuildSubmissionPayload(request), options: SerializerOptions)
|
||||
};
|
||||
|
||||
using var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.Conflict)
|
||||
{
|
||||
var message = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
throw new InvalidOperationException($"Rekor reported a conflict: {message}");
|
||||
}
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
using var document = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var root = document.RootElement;
|
||||
|
||||
long? index = null;
|
||||
if (root.TryGetProperty("index", out var indexElement) && indexElement.TryGetInt64(out var indexValue))
|
||||
{
|
||||
index = indexValue;
|
||||
}
|
||||
|
||||
return new RekorSubmissionResponse
|
||||
{
|
||||
Uuid = root.TryGetProperty("uuid", out var uuidElement) ? uuidElement.GetString() ?? string.Empty : string.Empty,
|
||||
Index = index,
|
||||
LogUrl = root.TryGetProperty("logURL", out var urlElement) ? urlElement.GetString() ?? backend.Url.ToString() : backend.Url.ToString(),
|
||||
Status = root.TryGetProperty("status", out var statusElement) ? statusElement.GetString() ?? "included" : "included",
|
||||
Proof = TryParseProof(root.TryGetProperty("proof", out var proofElement) ? proofElement : default)
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<RekorProofResponse?> GetProofAsync(string rekorUuid, RekorBackend backend, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var proofUri = BuildUri(backend.Url, $"api/v2/log/entries/{rekorUuid}/proof");
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, proofUri);
|
||||
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
_logger.LogDebug("Rekor proof for {Uuid} not found", rekorUuid);
|
||||
return null;
|
||||
}
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
using var document = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return TryParseProof(document.RootElement);
|
||||
}
|
||||
|
||||
private static object BuildSubmissionPayload(AttestorSubmissionRequest request)
|
||||
{
|
||||
var signatures = new List<object>();
|
||||
foreach (var sig in request.Bundle.Dsse.Signatures)
|
||||
{
|
||||
signatures.Add(new { keyid = sig.KeyId, sig = sig.Signature });
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
entries = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
dsseEnvelope = new
|
||||
{
|
||||
payload = request.Bundle.Dsse.PayloadBase64,
|
||||
payloadType = request.Bundle.Dsse.PayloadType,
|
||||
signatures
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static RekorProofResponse? TryParseProof(JsonElement proofElement)
|
||||
{
|
||||
if (proofElement.ValueKind == JsonValueKind.Undefined || proofElement.ValueKind == JsonValueKind.Null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var checkpointElement = proofElement.TryGetProperty("checkpoint", out var cp) ? cp : default;
|
||||
var inclusionElement = proofElement.TryGetProperty("inclusion", out var inc) ? inc : default;
|
||||
|
||||
return new RekorProofResponse
|
||||
{
|
||||
Checkpoint = checkpointElement.ValueKind == JsonValueKind.Object
|
||||
? new RekorProofResponse.RekorCheckpoint
|
||||
{
|
||||
Origin = checkpointElement.TryGetProperty("origin", out var origin) ? origin.GetString() : null,
|
||||
Size = checkpointElement.TryGetProperty("size", out var size) && size.TryGetInt64(out var sizeValue) ? sizeValue : 0,
|
||||
RootHash = checkpointElement.TryGetProperty("rootHash", out var rootHash) ? rootHash.GetString() : null,
|
||||
Timestamp = checkpointElement.TryGetProperty("timestamp", out var ts) && ts.ValueKind == JsonValueKind.String && DateTimeOffset.TryParse(ts.GetString(), out var dto) ? dto : null
|
||||
}
|
||||
: null,
|
||||
Inclusion = inclusionElement.ValueKind == JsonValueKind.Object
|
||||
? new RekorProofResponse.RekorInclusionProof
|
||||
{
|
||||
LeafHash = inclusionElement.TryGetProperty("leafHash", out var leaf) ? leaf.GetString() : null,
|
||||
Path = inclusionElement.TryGetProperty("path", out var pathElement) && pathElement.ValueKind == JsonValueKind.Array
|
||||
? pathElement.EnumerateArray().Select(p => p.GetString() ?? string.Empty).ToArray()
|
||||
: Array.Empty<string>()
|
||||
}
|
||||
: null
|
||||
};
|
||||
}
|
||||
|
||||
private static Uri BuildUri(Uri baseUri, string relative)
|
||||
{
|
||||
if (!relative.StartsWith("/", StringComparison.Ordinal))
|
||||
{
|
||||
relative = "/" + relative;
|
||||
}
|
||||
|
||||
return new Uri(baseUri, relative);
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Attestor.Core.Rekor;
|
||||
using StellaOps.Attestor.Core.Submission;
|
||||
|
||||
namespace StellaOps.Attestor.Infrastructure.Rekor;
|
||||
|
||||
internal sealed class HttpRekorClient : IRekorClient
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<HttpRekorClient> _logger;
|
||||
|
||||
public HttpRekorClient(HttpClient httpClient, ILogger<HttpRekorClient> logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<RekorSubmissionResponse> SubmitAsync(AttestorSubmissionRequest request, RekorBackend backend, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var submissionUri = BuildUri(backend.Url, "api/v2/log/entries");
|
||||
|
||||
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, submissionUri)
|
||||
{
|
||||
Content = JsonContent.Create(BuildSubmissionPayload(request), options: SerializerOptions)
|
||||
};
|
||||
|
||||
using var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.Conflict)
|
||||
{
|
||||
var message = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
throw new InvalidOperationException($"Rekor reported a conflict: {message}");
|
||||
}
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
using var document = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var root = document.RootElement;
|
||||
|
||||
long? index = null;
|
||||
if (root.TryGetProperty("index", out var indexElement) && indexElement.TryGetInt64(out var indexValue))
|
||||
{
|
||||
index = indexValue;
|
||||
}
|
||||
|
||||
return new RekorSubmissionResponse
|
||||
{
|
||||
Uuid = root.TryGetProperty("uuid", out var uuidElement) ? uuidElement.GetString() ?? string.Empty : string.Empty,
|
||||
Index = index,
|
||||
LogUrl = root.TryGetProperty("logURL", out var urlElement) ? urlElement.GetString() ?? backend.Url.ToString() : backend.Url.ToString(),
|
||||
Status = root.TryGetProperty("status", out var statusElement) ? statusElement.GetString() ?? "included" : "included",
|
||||
Proof = TryParseProof(root.TryGetProperty("proof", out var proofElement) ? proofElement : default)
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<RekorProofResponse?> GetProofAsync(string rekorUuid, RekorBackend backend, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var proofUri = BuildUri(backend.Url, $"api/v2/log/entries/{rekorUuid}/proof");
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, proofUri);
|
||||
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
_logger.LogDebug("Rekor proof for {Uuid} not found", rekorUuid);
|
||||
return null;
|
||||
}
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
using var document = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return TryParseProof(document.RootElement);
|
||||
}
|
||||
|
||||
private static object BuildSubmissionPayload(AttestorSubmissionRequest request)
|
||||
{
|
||||
var signatures = new List<object>();
|
||||
foreach (var sig in request.Bundle.Dsse.Signatures)
|
||||
{
|
||||
signatures.Add(new { keyid = sig.KeyId, sig = sig.Signature });
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
entries = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
dsseEnvelope = new
|
||||
{
|
||||
payload = request.Bundle.Dsse.PayloadBase64,
|
||||
payloadType = request.Bundle.Dsse.PayloadType,
|
||||
signatures
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static RekorProofResponse? TryParseProof(JsonElement proofElement)
|
||||
{
|
||||
if (proofElement.ValueKind == JsonValueKind.Undefined || proofElement.ValueKind == JsonValueKind.Null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var checkpointElement = proofElement.TryGetProperty("checkpoint", out var cp) ? cp : default;
|
||||
var inclusionElement = proofElement.TryGetProperty("inclusion", out var inc) ? inc : default;
|
||||
|
||||
return new RekorProofResponse
|
||||
{
|
||||
Checkpoint = checkpointElement.ValueKind == JsonValueKind.Object
|
||||
? new RekorProofResponse.RekorCheckpoint
|
||||
{
|
||||
Origin = checkpointElement.TryGetProperty("origin", out var origin) ? origin.GetString() : null,
|
||||
Size = checkpointElement.TryGetProperty("size", out var size) && size.TryGetInt64(out var sizeValue) ? sizeValue : 0,
|
||||
RootHash = checkpointElement.TryGetProperty("rootHash", out var rootHash) ? rootHash.GetString() : null,
|
||||
Timestamp = checkpointElement.TryGetProperty("timestamp", out var ts) && ts.ValueKind == JsonValueKind.String && DateTimeOffset.TryParse(ts.GetString(), out var dto) ? dto : null
|
||||
}
|
||||
: null,
|
||||
Inclusion = inclusionElement.ValueKind == JsonValueKind.Object
|
||||
? new RekorProofResponse.RekorInclusionProof
|
||||
{
|
||||
LeafHash = inclusionElement.TryGetProperty("leafHash", out var leaf) ? leaf.GetString() : null,
|
||||
Path = inclusionElement.TryGetProperty("path", out var pathElement) && pathElement.ValueKind == JsonValueKind.Array
|
||||
? pathElement.EnumerateArray().Select(p => p.GetString() ?? string.Empty).ToArray()
|
||||
: Array.Empty<string>()
|
||||
}
|
||||
: null
|
||||
};
|
||||
}
|
||||
|
||||
private static Uri BuildUri(Uri baseUri, string relative)
|
||||
{
|
||||
if (!relative.StartsWith("/", StringComparison.Ordinal))
|
||||
{
|
||||
relative = "/" + relative;
|
||||
}
|
||||
|
||||
return new Uri(baseUri, relative);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,71 +1,71 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Attestor.Core.Rekor;
|
||||
using StellaOps.Attestor.Core.Submission;
|
||||
|
||||
namespace StellaOps.Attestor.Infrastructure.Rekor;
|
||||
|
||||
internal sealed class StubRekorClient : IRekorClient
|
||||
{
|
||||
private readonly ILogger<StubRekorClient> _logger;
|
||||
|
||||
public StubRekorClient(ILogger<StubRekorClient> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task<RekorSubmissionResponse> SubmitAsync(AttestorSubmissionRequest request, RekorBackend backend, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var uuid = Guid.NewGuid().ToString();
|
||||
_logger.LogInformation("Stub Rekor submission for bundle {BundleSha} -> {Uuid}", request.Meta.BundleSha256, uuid);
|
||||
|
||||
var proof = new RekorProofResponse
|
||||
{
|
||||
Checkpoint = new RekorProofResponse.RekorCheckpoint
|
||||
{
|
||||
Origin = backend.Url.Host,
|
||||
Size = 1,
|
||||
RootHash = request.Meta.BundleSha256,
|
||||
Timestamp = DateTimeOffset.UtcNow
|
||||
},
|
||||
Inclusion = new RekorProofResponse.RekorInclusionProof
|
||||
{
|
||||
LeafHash = request.Meta.BundleSha256,
|
||||
Path = Array.Empty<string>()
|
||||
}
|
||||
};
|
||||
|
||||
var response = new RekorSubmissionResponse
|
||||
{
|
||||
Uuid = uuid,
|
||||
Index = Random.Shared.NextInt64(1, long.MaxValue),
|
||||
LogUrl = new Uri(backend.Url, $"/api/v2/log/entries/{uuid}").ToString(),
|
||||
Status = "included",
|
||||
Proof = proof
|
||||
};
|
||||
|
||||
return Task.FromResult(response);
|
||||
}
|
||||
|
||||
public Task<RekorProofResponse?> GetProofAsync(string rekorUuid, RekorBackend backend, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogInformation("Stub Rekor proof fetch for {Uuid}", rekorUuid);
|
||||
return Task.FromResult<RekorProofResponse?>(new RekorProofResponse
|
||||
{
|
||||
Checkpoint = new RekorProofResponse.RekorCheckpoint
|
||||
{
|
||||
Origin = backend.Url.Host,
|
||||
Size = 1,
|
||||
RootHash = string.Empty,
|
||||
Timestamp = DateTimeOffset.UtcNow
|
||||
},
|
||||
Inclusion = new RekorProofResponse.RekorInclusionProof
|
||||
{
|
||||
LeafHash = string.Empty,
|
||||
Path = Array.Empty<string>()
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Attestor.Core.Rekor;
|
||||
using StellaOps.Attestor.Core.Submission;
|
||||
|
||||
namespace StellaOps.Attestor.Infrastructure.Rekor;
|
||||
|
||||
internal sealed class StubRekorClient : IRekorClient
|
||||
{
|
||||
private readonly ILogger<StubRekorClient> _logger;
|
||||
|
||||
public StubRekorClient(ILogger<StubRekorClient> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task<RekorSubmissionResponse> SubmitAsync(AttestorSubmissionRequest request, RekorBackend backend, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var uuid = Guid.NewGuid().ToString();
|
||||
_logger.LogInformation("Stub Rekor submission for bundle {BundleSha} -> {Uuid}", request.Meta.BundleSha256, uuid);
|
||||
|
||||
var proof = new RekorProofResponse
|
||||
{
|
||||
Checkpoint = new RekorProofResponse.RekorCheckpoint
|
||||
{
|
||||
Origin = backend.Url.Host,
|
||||
Size = 1,
|
||||
RootHash = request.Meta.BundleSha256,
|
||||
Timestamp = DateTimeOffset.UtcNow
|
||||
},
|
||||
Inclusion = new RekorProofResponse.RekorInclusionProof
|
||||
{
|
||||
LeafHash = request.Meta.BundleSha256,
|
||||
Path = Array.Empty<string>()
|
||||
}
|
||||
};
|
||||
|
||||
var response = new RekorSubmissionResponse
|
||||
{
|
||||
Uuid = uuid,
|
||||
Index = Random.Shared.NextInt64(1, long.MaxValue),
|
||||
LogUrl = new Uri(backend.Url, $"/api/v2/log/entries/{uuid}").ToString(),
|
||||
Status = "included",
|
||||
Proof = proof
|
||||
};
|
||||
|
||||
return Task.FromResult(response);
|
||||
}
|
||||
|
||||
public Task<RekorProofResponse?> GetProofAsync(string rekorUuid, RekorBackend backend, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogInformation("Stub Rekor proof fetch for {Uuid}", rekorUuid);
|
||||
return Task.FromResult<RekorProofResponse?>(new RekorProofResponse
|
||||
{
|
||||
Checkpoint = new RekorProofResponse.RekorCheckpoint
|
||||
{
|
||||
Origin = backend.Url.Host,
|
||||
Size = 1,
|
||||
RootHash = string.Empty,
|
||||
Timestamp = DateTimeOffset.UtcNow
|
||||
},
|
||||
Inclusion = new RekorProofResponse.RekorInclusionProof
|
||||
{
|
||||
LeafHash = string.Empty,
|
||||
Path = Array.Empty<string>()
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Attestor.Core.Storage;
|
||||
|
||||
namespace StellaOps.Attestor.Infrastructure.Storage;
|
||||
|
||||
internal sealed class InMemoryAttestorDedupeStore : IAttestorDedupeStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, (string Uuid, DateTimeOffset ExpiresAt)> _store = new();
|
||||
|
||||
public Task<string?> TryGetExistingAsync(string bundleSha256, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_store.TryGetValue(bundleSha256, out var entry))
|
||||
{
|
||||
if (entry.ExpiresAt > DateTimeOffset.UtcNow)
|
||||
{
|
||||
return Task.FromResult<string?>(entry.Uuid);
|
||||
}
|
||||
|
||||
_store.TryRemove(bundleSha256, out _);
|
||||
}
|
||||
|
||||
return Task.FromResult<string?>(null);
|
||||
}
|
||||
|
||||
public Task SetAsync(string bundleSha256, string rekorUuid, TimeSpan ttl, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_store[bundleSha256] = (rekorUuid, DateTimeOffset.UtcNow.Add(ttl));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Attestor.Core.Storage;
|
||||
|
||||
namespace StellaOps.Attestor.Infrastructure.Storage;
|
||||
|
||||
internal sealed class InMemoryAttestorDedupeStore : IAttestorDedupeStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, (string Uuid, DateTimeOffset ExpiresAt)> _store = new();
|
||||
|
||||
public Task<string?> TryGetExistingAsync(string bundleSha256, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_store.TryGetValue(bundleSha256, out var entry))
|
||||
{
|
||||
if (entry.ExpiresAt > DateTimeOffset.UtcNow)
|
||||
{
|
||||
return Task.FromResult<string?>(entry.Uuid);
|
||||
}
|
||||
|
||||
_store.TryRemove(bundleSha256, out _);
|
||||
}
|
||||
|
||||
return Task.FromResult<string?>(null);
|
||||
}
|
||||
|
||||
public Task SetAsync(string bundleSha256, string rekorUuid, TimeSpan ttl, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_store[bundleSha256] = (rekorUuid, DateTimeOffset.UtcNow.Add(ttl));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Attestor.Core.Storage;
|
||||
|
||||
namespace StellaOps.Attestor.Infrastructure.Storage;
|
||||
|
||||
internal sealed class NullAttestorArchiveStore : IAttestorArchiveStore
|
||||
{
|
||||
private readonly ILogger<NullAttestorArchiveStore> _logger;
|
||||
|
||||
public NullAttestorArchiveStore(ILogger<NullAttestorArchiveStore> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Attestor.Core.Storage;
|
||||
|
||||
namespace StellaOps.Attestor.Infrastructure.Storage;
|
||||
|
||||
internal sealed class NullAttestorArchiveStore : IAttestorArchiveStore
|
||||
{
|
||||
private readonly ILogger<NullAttestorArchiveStore> _logger;
|
||||
|
||||
public NullAttestorArchiveStore(ILogger<NullAttestorArchiveStore> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task ArchiveBundleAsync(AttestorArchiveBundle bundle, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogDebug("Archive disabled; skipping bundle {BundleSha}", bundle.BundleSha256);
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StackExchange.Redis;
|
||||
using StellaOps.Attestor.Core.Options;
|
||||
using StellaOps.Attestor.Core.Storage;
|
||||
|
||||
namespace StellaOps.Attestor.Infrastructure.Storage;
|
||||
|
||||
internal sealed class RedisAttestorDedupeStore : IAttestorDedupeStore
|
||||
{
|
||||
private readonly IDatabase _database;
|
||||
private readonly string _prefix;
|
||||
|
||||
public RedisAttestorDedupeStore(IConnectionMultiplexer multiplexer, IOptions<AttestorOptions> options)
|
||||
{
|
||||
_database = multiplexer.GetDatabase();
|
||||
_prefix = options.Value.Redis.DedupePrefix ?? "attestor:dedupe:";
|
||||
}
|
||||
|
||||
public async Task<string?> TryGetExistingAsync(string bundleSha256, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var value = await _database.StringGetAsync(BuildKey(bundleSha256)).ConfigureAwait(false);
|
||||
return value.HasValue ? value.ToString() : null;
|
||||
}
|
||||
|
||||
public Task SetAsync(string bundleSha256, string rekorUuid, TimeSpan ttl, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _database.StringSetAsync(BuildKey(bundleSha256), rekorUuid, ttl);
|
||||
}
|
||||
|
||||
private RedisKey BuildKey(string bundleSha256) => new RedisKey(_prefix + bundleSha256);
|
||||
}
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StackExchange.Redis;
|
||||
using StellaOps.Attestor.Core.Options;
|
||||
using StellaOps.Attestor.Core.Storage;
|
||||
|
||||
namespace StellaOps.Attestor.Infrastructure.Storage;
|
||||
|
||||
internal sealed class RedisAttestorDedupeStore : IAttestorDedupeStore
|
||||
{
|
||||
private readonly IDatabase _database;
|
||||
private readonly string _prefix;
|
||||
|
||||
public RedisAttestorDedupeStore(IConnectionMultiplexer multiplexer, IOptions<AttestorOptions> options)
|
||||
{
|
||||
_database = multiplexer.GetDatabase();
|
||||
_prefix = options.Value.Redis.DedupePrefix ?? "attestor:dedupe:";
|
||||
}
|
||||
|
||||
public async Task<string?> TryGetExistingAsync(string bundleSha256, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var value = await _database.StringGetAsync(BuildKey(bundleSha256)).ConfigureAwait(false);
|
||||
return value.HasValue ? value.ToString() : null;
|
||||
}
|
||||
|
||||
public Task SetAsync(string bundleSha256, string rekorUuid, TimeSpan ttl, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _database.StringSetAsync(BuildKey(bundleSha256), rekorUuid, ttl);
|
||||
}
|
||||
|
||||
private RedisKey BuildKey(string bundleSha256) => new RedisKey(_prefix + bundleSha256);
|
||||
}
|
||||
|
||||
@@ -1,49 +1,49 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Attestor.Core.Submission;
|
||||
|
||||
namespace StellaOps.Attestor.Infrastructure.Submission;
|
||||
|
||||
public sealed class DefaultDsseCanonicalizer : IDsseCanonicalizer
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = false,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
public Task<byte[]> CanonicalizeAsync(AttestorSubmissionRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var node = new JsonObject
|
||||
{
|
||||
["payloadType"] = request.Bundle.Dsse.PayloadType,
|
||||
["payload"] = request.Bundle.Dsse.PayloadBase64,
|
||||
["signatures"] = CreateSignaturesArray(request)
|
||||
};
|
||||
|
||||
var json = node.ToJsonString(SerializerOptions);
|
||||
return Task.FromResult(JsonSerializer.SerializeToUtf8Bytes(JsonNode.Parse(json)!, SerializerOptions));
|
||||
}
|
||||
|
||||
private static JsonArray CreateSignaturesArray(AttestorSubmissionRequest request)
|
||||
{
|
||||
var array = new JsonArray();
|
||||
foreach (var signature in request.Bundle.Dsse.Signatures)
|
||||
{
|
||||
var obj = new JsonObject
|
||||
{
|
||||
["sig"] = signature.Signature
|
||||
};
|
||||
if (!string.IsNullOrWhiteSpace(signature.KeyId))
|
||||
{
|
||||
obj["keyid"] = signature.KeyId;
|
||||
}
|
||||
|
||||
array.Add(obj);
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
}
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Attestor.Core.Submission;
|
||||
|
||||
namespace StellaOps.Attestor.Infrastructure.Submission;
|
||||
|
||||
public sealed class DefaultDsseCanonicalizer : IDsseCanonicalizer
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = false,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
public Task<byte[]> CanonicalizeAsync(AttestorSubmissionRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var node = new JsonObject
|
||||
{
|
||||
["payloadType"] = request.Bundle.Dsse.PayloadType,
|
||||
["payload"] = request.Bundle.Dsse.PayloadBase64,
|
||||
["signatures"] = CreateSignaturesArray(request)
|
||||
};
|
||||
|
||||
var json = node.ToJsonString(SerializerOptions);
|
||||
return Task.FromResult(JsonSerializer.SerializeToUtf8Bytes(JsonNode.Parse(json)!, SerializerOptions));
|
||||
}
|
||||
|
||||
private static JsonArray CreateSignaturesArray(AttestorSubmissionRequest request)
|
||||
{
|
||||
var array = new JsonArray();
|
||||
foreach (var signature in request.Bundle.Dsse.Signatures)
|
||||
{
|
||||
var obj = new JsonObject
|
||||
{
|
||||
["sig"] = signature.Signature
|
||||
};
|
||||
if (!string.IsNullOrWhiteSpace(signature.KeyId))
|
||||
{
|
||||
obj["keyid"] = signature.KeyId;
|
||||
}
|
||||
|
||||
array.Add(obj);
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user