license switch agpl -> busl1, sprints work, new product advisories
This commit is contained in:
@@ -10,15 +10,31 @@ public sealed class BundleBuilder : IBundleBuilder
|
||||
{
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
private readonly ITsaChainBundler _tsaChainBundler;
|
||||
private readonly IOcspResponseFetcher _ocspFetcher;
|
||||
private readonly ICrlFetcher _crlFetcher;
|
||||
|
||||
public BundleBuilder() : this(TimeProvider.System, SystemGuidProvider.Instance)
|
||||
public BundleBuilder() : this(
|
||||
TimeProvider.System,
|
||||
SystemGuidProvider.Instance,
|
||||
null,
|
||||
null,
|
||||
null)
|
||||
{
|
||||
}
|
||||
|
||||
public BundleBuilder(TimeProvider timeProvider, IGuidProvider guidProvider)
|
||||
public BundleBuilder(
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider guidProvider,
|
||||
ITsaChainBundler? tsaChainBundler,
|
||||
IOcspResponseFetcher? ocspFetcher,
|
||||
ICrlFetcher? crlFetcher)
|
||||
{
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
|
||||
_tsaChainBundler = tsaChainBundler ?? new TsaChainBundler();
|
||||
_ocspFetcher = ocspFetcher ?? new OcspResponseFetcher();
|
||||
_crlFetcher = crlFetcher ?? new CrlFetcher();
|
||||
}
|
||||
|
||||
public async Task<BundleManifest> BuildAsync(
|
||||
@@ -135,10 +151,44 @@ public sealed class BundleBuilder : IBundleBuilder
|
||||
files.ToImmutableArray()));
|
||||
}
|
||||
|
||||
var timestamps = new List<TimestampEntry>();
|
||||
long timestampSizeBytes = 0;
|
||||
var timestampConfigs = request.Timestamps ?? Array.Empty<TimestampBuildConfig>();
|
||||
foreach (var timestampConfig in timestampConfigs)
|
||||
{
|
||||
switch (timestampConfig)
|
||||
{
|
||||
case Rfc3161TimestampBuildConfig rfc3161:
|
||||
var (rfcEntry, rfcSizeBytes) = await BuildRfc3161TimestampAsync(
|
||||
rfc3161,
|
||||
outputPath,
|
||||
ct).ConfigureAwait(false);
|
||||
timestamps.Add(rfcEntry);
|
||||
timestampSizeBytes += rfcSizeBytes;
|
||||
break;
|
||||
case EidasQtsTimestampBuildConfig eidas:
|
||||
var qtsComponent = await CopyTimestampFileAsync(
|
||||
eidas.SourcePath,
|
||||
eidas.RelativePath,
|
||||
outputPath,
|
||||
ct).ConfigureAwait(false);
|
||||
timestamps.Add(new EidasQtsTimestampEntry
|
||||
{
|
||||
QtsMetaPath = qtsComponent.RelativePath
|
||||
});
|
||||
timestampSizeBytes += qtsComponent.SizeBytes;
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException(
|
||||
$"Unsupported timestamp build config type '{timestampConfig.GetType().Name}'.");
|
||||
}
|
||||
}
|
||||
|
||||
var totalSize = feeds.Sum(f => f.SizeBytes) +
|
||||
policies.Sum(p => p.SizeBytes) +
|
||||
cryptoMaterials.Sum(c => c.SizeBytes) +
|
||||
ruleBundles.Sum(r => r.SizeBytes);
|
||||
ruleBundles.Sum(r => r.SizeBytes) +
|
||||
timestampSizeBytes;
|
||||
|
||||
var manifest = new BundleManifest
|
||||
{
|
||||
@@ -152,6 +202,7 @@ public sealed class BundleBuilder : IBundleBuilder
|
||||
Policies = policies.ToImmutableArray(),
|
||||
CryptoMaterials = cryptoMaterials.ToImmutableArray(),
|
||||
RuleBundles = ruleBundles.ToImmutableArray(),
|
||||
Timestamps = timestamps.ToImmutableArray(),
|
||||
TotalSizeBytes = totalSize
|
||||
};
|
||||
|
||||
@@ -180,7 +231,116 @@ public sealed class BundleBuilder : IBundleBuilder
|
||||
return new CopiedComponent(source.RelativePath, digest, info.Length);
|
||||
}
|
||||
|
||||
private async Task<(Rfc3161TimestampEntry Entry, long SizeBytes)> BuildRfc3161TimestampAsync(
|
||||
Rfc3161TimestampBuildConfig config,
|
||||
string outputPath,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (config.TimeStampToken is not { Length: > 0 })
|
||||
{
|
||||
throw new ArgumentException("RFC3161 timestamp token is required.", nameof(config));
|
||||
}
|
||||
|
||||
var tokenHash = SHA256.HashData(config.TimeStampToken);
|
||||
var tokenPrefix = Convert.ToHexString(tokenHash).ToLowerInvariant()[..12];
|
||||
|
||||
var chainResult = await _tsaChainBundler.BundleAsync(
|
||||
config.TimeStampToken,
|
||||
outputPath,
|
||||
tokenPrefix,
|
||||
ct).ConfigureAwait(false);
|
||||
|
||||
var ocspBlobs = await _ocspFetcher.FetchAsync(chainResult.Certificates, ct).ConfigureAwait(false);
|
||||
var (ocspPaths, ocspSizeBytes) = await WriteRevocationBlobsAsync(
|
||||
"tsa/ocsp",
|
||||
"der",
|
||||
tokenPrefix,
|
||||
ocspBlobs,
|
||||
outputPath,
|
||||
ct).ConfigureAwait(false);
|
||||
|
||||
var crlBlobs = await _crlFetcher.FetchAsync(chainResult.Certificates, ct).ConfigureAwait(false);
|
||||
var (crlPaths, crlSizeBytes) = await WriteRevocationBlobsAsync(
|
||||
"tsa/crl",
|
||||
"crl",
|
||||
tokenPrefix,
|
||||
crlBlobs,
|
||||
outputPath,
|
||||
ct).ConfigureAwait(false);
|
||||
|
||||
var entry = new Rfc3161TimestampEntry
|
||||
{
|
||||
TsaChainPaths = chainResult.ChainPaths,
|
||||
OcspBlobs = ocspPaths,
|
||||
CrlBlobs = crlPaths,
|
||||
TstBase64 = Convert.ToBase64String(config.TimeStampToken)
|
||||
};
|
||||
|
||||
return (entry, chainResult.TotalSizeBytes + ocspSizeBytes + crlSizeBytes);
|
||||
}
|
||||
|
||||
private static async Task<(ImmutableArray<string> Paths, long SizeBytes)> WriteRevocationBlobsAsync(
|
||||
string baseDir,
|
||||
string extension,
|
||||
string prefix,
|
||||
IReadOnlyList<TsaRevocationBlob> blobs,
|
||||
string outputPath,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (blobs.Count == 0)
|
||||
{
|
||||
return ([], 0);
|
||||
}
|
||||
|
||||
var paths = new List<string>(blobs.Count);
|
||||
long totalSize = 0;
|
||||
|
||||
foreach (var blob in blobs
|
||||
.OrderBy(b => b.CertificateIndex)
|
||||
.ThenBy(b => ComputeShortHash(blob.Data), StringComparer.Ordinal))
|
||||
{
|
||||
var hash = ComputeShortHash(blob.Data);
|
||||
var fileName = $"{prefix}-{blob.CertificateIndex:D2}-{hash}.{extension}";
|
||||
var relativePath = $"{baseDir}/{fileName}";
|
||||
var targetPath = PathValidation.SafeCombine(outputPath, relativePath);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(targetPath) ?? outputPath);
|
||||
await File.WriteAllBytesAsync(targetPath, blob.Data, ct).ConfigureAwait(false);
|
||||
|
||||
totalSize += blob.Data.Length;
|
||||
paths.Add(relativePath);
|
||||
}
|
||||
|
||||
return (paths.ToImmutableArray(), totalSize);
|
||||
}
|
||||
|
||||
private static string ComputeShortHash(byte[] data)
|
||||
{
|
||||
var hash = SHA256.HashData(data);
|
||||
return Convert.ToHexString(hash).ToLowerInvariant()[..16];
|
||||
}
|
||||
|
||||
private static async Task<CopiedTimestampComponent> CopyTimestampFileAsync(
|
||||
string sourcePath,
|
||||
string relativePath,
|
||||
string outputPath,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var targetPath = PathValidation.SafeCombine(outputPath, relativePath);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(targetPath) ?? outputPath);
|
||||
|
||||
await using (var input = File.OpenRead(sourcePath))
|
||||
await using (var output = File.Create(targetPath))
|
||||
{
|
||||
await input.CopyToAsync(output, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var info = new FileInfo(targetPath);
|
||||
return new CopiedTimestampComponent(relativePath, info.Length);
|
||||
}
|
||||
|
||||
private sealed record CopiedComponent(string RelativePath, string Digest, long SizeBytes);
|
||||
private sealed record CopiedTimestampComponent(string RelativePath, long SizeBytes);
|
||||
}
|
||||
|
||||
public interface IBundleBuilder
|
||||
@@ -195,7 +355,8 @@ public sealed record BundleBuildRequest(
|
||||
IReadOnlyList<FeedBuildConfig> Feeds,
|
||||
IReadOnlyList<PolicyBuildConfig> Policies,
|
||||
IReadOnlyList<CryptoBuildConfig> CryptoMaterials,
|
||||
IReadOnlyList<RuleBundleBuildConfig> RuleBundles);
|
||||
IReadOnlyList<RuleBundleBuildConfig> RuleBundles,
|
||||
IReadOnlyList<TimestampBuildConfig>? Timestamps = null);
|
||||
|
||||
public abstract record BundleComponentSource(string SourcePath, string RelativePath);
|
||||
|
||||
@@ -227,6 +388,14 @@ public sealed record CryptoBuildConfig(
|
||||
DateTimeOffset? ExpiresAt)
|
||||
: BundleComponentSource(SourcePath, RelativePath);
|
||||
|
||||
public abstract record TimestampBuildConfig;
|
||||
|
||||
public sealed record Rfc3161TimestampBuildConfig(byte[] TimeStampToken)
|
||||
: TimestampBuildConfig;
|
||||
|
||||
public sealed record EidasQtsTimestampBuildConfig(string SourcePath, string RelativePath)
|
||||
: TimestampBuildConfig;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for building a rule bundle component.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user