72 lines
2.6 KiB
C#
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));
|
|
}
|
|
}
|