Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Implement `SbomIngestServiceCollectionExtensionsTests` to verify the SBOM ingestion pipeline exports snapshots correctly. - Create `SbomIngestTransformerTests` to ensure the transformation produces expected nodes and edges, including deduplication of license nodes and normalization of timestamps. - Add `SbomSnapshotExporterTests` to test the export functionality for manifest, adjacency, nodes, and edges. - Introduce `VexOverlayTransformerTests` to validate the transformation of VEX nodes and edges. - Set up project file for the test project with necessary dependencies and configurations. - Include JSON fixture files for testing purposes.
173 lines
6.7 KiB
C#
173 lines
6.7 KiB
C#
using System;
|
|
using System.Net.Http;
|
|
using System.Net.Http.Headers;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using Org.BouncyCastle.Asn1;
|
|
using Org.BouncyCastle.Asn1.Nist;
|
|
using Org.BouncyCastle.Asn1.Oiw;
|
|
using Org.BouncyCastle.Math;
|
|
using Org.BouncyCastle.Tsp;
|
|
using StellaOps.EvidenceLocker.Core.Configuration;
|
|
using StellaOps.EvidenceLocker.Core.Signing;
|
|
|
|
namespace StellaOps.EvidenceLocker.Infrastructure.Signing;
|
|
|
|
public sealed class Rfc3161TimestampAuthorityClient : ITimestampAuthorityClient
|
|
{
|
|
private readonly HttpClient _httpClient;
|
|
private readonly IOptions<EvidenceLockerOptions> _options;
|
|
private readonly ILogger<Rfc3161TimestampAuthorityClient> _logger;
|
|
|
|
public Rfc3161TimestampAuthorityClient(
|
|
HttpClient httpClient,
|
|
IOptions<EvidenceLockerOptions> options,
|
|
ILogger<Rfc3161TimestampAuthorityClient> logger)
|
|
{
|
|
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
}
|
|
|
|
public async Task<TimestampResult?> RequestTimestampAsync(
|
|
ReadOnlyMemory<byte> signature,
|
|
string hashAlgorithm,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var timestamping = _options.Value.Signing?.Timestamping;
|
|
if (timestamping is null || !timestamping.Enabled)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(timestamping.Endpoint))
|
|
{
|
|
throw new InvalidOperationException("Timestamping endpoint must be configured when enabled.");
|
|
}
|
|
|
|
var digest = ComputeDigest(signature.Span, hashAlgorithm);
|
|
var hashOid = ResolveHashAlgorithmOid(hashAlgorithm);
|
|
|
|
var requestGenerator = new TimeStampRequestGenerator();
|
|
requestGenerator.SetCertReq(true);
|
|
|
|
Span<byte> nonceBuffer = stackalloc byte[16];
|
|
RandomNumberGenerator.Fill(nonceBuffer);
|
|
var nonce = new BigInteger(1, nonceBuffer);
|
|
|
|
var timeStampRequest = requestGenerator.Generate(new DerObjectIdentifier(hashOid), digest, nonce);
|
|
var requestBytes = timeStampRequest.GetEncoded();
|
|
|
|
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, timestamping.Endpoint);
|
|
httpRequest.Content = new ByteArrayContent(requestBytes);
|
|
httpRequest.Content.Headers.ContentType = new MediaTypeHeaderValue("application/timestamp-query");
|
|
httpRequest.Headers.Accept.Clear();
|
|
httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/timestamp-reply"));
|
|
httpRequest.Headers.UserAgent.ParseAdd("StellaOpsEvidenceLocker/1.0");
|
|
|
|
if (timestamping.RequestTimeoutSeconds > 0)
|
|
{
|
|
_httpClient.Timeout = TimeSpan.FromSeconds(timestamping.RequestTimeoutSeconds);
|
|
}
|
|
|
|
if (timestamping.Authentication is { Username: { Length: > 0 } } auth)
|
|
{
|
|
var credentials = $"{auth.Username}:{auth.Password ?? string.Empty}";
|
|
var credentialBytes = Encoding.UTF8.GetBytes(credentials);
|
|
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(credentialBytes));
|
|
}
|
|
|
|
using var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
|
|
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
if (timestamping.RequireTimestamp)
|
|
{
|
|
throw new InvalidOperationException($"Timestamp authority responded with status code {(int)response.StatusCode} ({response.StatusCode}).");
|
|
}
|
|
|
|
if (_logger.IsEnabled(LogLevel.Warning))
|
|
{
|
|
_logger.LogWarning("Timestamp authority request failed with status {StatusCode}.", response.StatusCode);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
var responseBytes = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
|
|
|
|
TimeStampResponse tspResponse;
|
|
try
|
|
{
|
|
tspResponse = new TimeStampResponse(responseBytes);
|
|
tspResponse.Validate(timeStampRequest);
|
|
}
|
|
catch (Exception ex) when (!timestamping.RequireTimestamp)
|
|
{
|
|
_logger.LogWarning(ex, "Timestamp authority returned an invalid response.");
|
|
return null;
|
|
}
|
|
|
|
if (tspResponse.Status is not 0 and not 1)
|
|
{
|
|
if (timestamping.RequireTimestamp)
|
|
{
|
|
throw new InvalidOperationException($"Timestamp authority declined request with status code {tspResponse.Status}.");
|
|
}
|
|
|
|
_logger.LogWarning("Timestamp authority declined request with status code {Status}.", tspResponse.Status);
|
|
return null;
|
|
}
|
|
|
|
var token = tspResponse.TimeStampToken;
|
|
if (token is null)
|
|
{
|
|
if (timestamping.RequireTimestamp)
|
|
{
|
|
throw new InvalidOperationException("Timestamp authority response did not include a token.");
|
|
}
|
|
|
|
_logger.LogWarning("Timestamp authority response missing token for bundle timestamp request.");
|
|
return null;
|
|
}
|
|
|
|
var info = token.TimeStampInfo;
|
|
var authority = info.Tsa?.Name?.ToString() ?? timestamping.Endpoint;
|
|
var tokenBytes = token.GetEncoded();
|
|
|
|
return new TimestampResult(info.GenTime, authority, tokenBytes);
|
|
}
|
|
|
|
private static byte[] ComputeDigest(ReadOnlySpan<byte> data, string algorithm)
|
|
{
|
|
var hashAlgorithm = GetHashAlgorithmName(algorithm);
|
|
using var hasher = IncrementalHash.CreateHash(hashAlgorithm);
|
|
hasher.AppendData(data);
|
|
return hasher.GetHashAndReset();
|
|
}
|
|
|
|
private static HashAlgorithmName GetHashAlgorithmName(string algorithm)
|
|
=> (algorithm ?? string.Empty).ToUpperInvariant() switch
|
|
{
|
|
"SHA256" => HashAlgorithmName.SHA256,
|
|
"SHA384" => HashAlgorithmName.SHA384,
|
|
"SHA512" => HashAlgorithmName.SHA512,
|
|
"SHA1" => HashAlgorithmName.SHA1,
|
|
_ => throw new InvalidOperationException($"Unsupported timestamp hash algorithm '{algorithm}'.")
|
|
};
|
|
|
|
private static string ResolveHashAlgorithmOid(string algorithm)
|
|
=> (algorithm ?? string.Empty).ToUpperInvariant() switch
|
|
{
|
|
"SHA256" => NistObjectIdentifiers.IdSha256.Id,
|
|
"SHA384" => NistObjectIdentifiers.IdSha384.Id,
|
|
"SHA512" => NistObjectIdentifiers.IdSha512.Id,
|
|
"SHA1" => OiwObjectIdentifiers.IdSha1.Id,
|
|
_ => throw new InvalidOperationException($"Unsupported timestamp hash algorithm '{algorithm}'.")
|
|
};
|
|
}
|