Files
git.stella-ops.org/src/AirGap/__Libraries/StellaOps.AirGap.Bundle/Services/TsaChainBundler.cs
2026-02-04 19:59:20 +02:00

72 lines
2.6 KiB
C#

using System.Collections.Immutable;
using System.Security.Cryptography.Pkcs;
using System.Security.Cryptography.X509Certificates;
using System.Text;
namespace StellaOps.AirGap.Bundle.Services;
public sealed partial class TsaChainBundler : ITsaChainBundler
{
public async Task<TsaChainBundleResult> BundleAsync(
ReadOnlyMemory<byte> timeStampToken,
string outputPath,
string? filePrefix = null,
CancellationToken ct = default)
{
if (timeStampToken.IsEmpty)
{
throw new ArgumentException("RFC3161 timestamp token is required.", nameof(timeStampToken));
}
var signedCms = new SignedCms();
signedCms.Decode(timeStampToken.ToArray());
if (signedCms.SignerInfos.Count == 0)
{
throw new InvalidOperationException("RFC3161 timestamp token has no signer.");
}
var signerCert = signedCms.SignerInfos[0].Certificate;
if (signerCert is null)
{
throw new InvalidOperationException("RFC3161 timestamp token has no signer certificate.");
}
var certificates = new List<X509Certificate2>(signedCms.Certificates.Cast<X509Certificate2>());
if (!certificates.Any(c => string.Equals(c.Thumbprint, signerCert.Thumbprint, StringComparison.OrdinalIgnoreCase)))
{
certificates.Add(signerCert);
}
var chain = BuildChain(signerCert, certificates);
if (chain.Count == 0)
{
throw new InvalidOperationException("RFC3161 timestamp token contains no usable certificate chain.");
}
filePrefix ??= ComputePrefix(timeStampToken.Span);
var entries = new List<TsaChainEntry>(chain.Count);
for (var i = 0; i < chain.Count; i++)
{
var cert = chain[i];
var certHash = ComputeShortHash(cert.RawData);
var fileName = $"{filePrefix}-{i:D2}-{certHash}.pem";
var relativePath = $"tsa/chain/{fileName}";
var targetPath = PathValidation.SafeCombine(outputPath, relativePath);
Directory.CreateDirectory(Path.GetDirectoryName(targetPath) ?? outputPath);
await File.WriteAllTextAsync(targetPath, EncodePem(cert.RawData), Encoding.ASCII, ct)
.ConfigureAwait(false);
var info = new FileInfo(targetPath);
entries.Add(new TsaChainEntry(cert, relativePath, info.Length));
}
return new TsaChainBundleResult(
entries.Select(e => e.RelativePath).ToImmutableArray(),
entries.Select(e => e.Certificate).ToImmutableArray(),
entries.Sum(e => e.SizeBytes));
}
}