Create and verify ASiC-S and ASiC-E (Associated Signature Containers) with RFC 3161 timestamps in .NET.
Prove that data existed at a specific point in time using trusted Timestamp Authorities (TSA) like DigiCert — without needing your own signing certificate.
ASiC containers are ZIP files (per ETSI EN 319 162) that bundle your files with cryptographic timestamps:
- ASiC-S (Simple) — A single file with a timestamp token
- ASiC-E (Extended) — Multiple files with an ASiCManifest XML listing each file's digest, timestamped together
This proves that this exact data existed at this exact time, signed by a trusted third party. The format is recognized by the EU eIDAS regulation for legal validity.
dotnet add package AsicSharpdotnet tool install -g AsicSharp.CliusingAsicSharp.Configuration;usingAsicSharp.Services;// Standalone (no DI)usingvarhttpClient=newHttpClient();varoptions=newAsicTimestampOptions{TimestampAuthorityUrl=WellKnownTsa.DigiCert};vartsaClient=newTsaClient(httpClient,options);varasicService=newAsicService(tsaClient,options);// Create a timestamped container (single file → ASiC-S)vardata=File.ReadAllBytes("contract.pdf");varresult=awaitasicService.CreateAsync(data,"contract.pdf");File.WriteAllBytes("contract.pdf.asics",result.ContainerBytes);Console.WriteLine($"Timestamped at: {result.Timestamp:O}");// Create an ASiC-E container (multiple files)varfiles=newList<(stringFileName,byte[]Data)>{("contract.pdf",File.ReadAllBytes("contract.pdf")),("annex.pdf",File.ReadAllBytes("annex.pdf"))};varextended=awaitasicService.CreateExtendedAsync(files);File.WriteAllBytes("bundle.asice",extended.ContainerBytes);// Verify any container (auto-detects ASiC-S or ASiC-E)varverification=asicService.VerifyFile("contract.pdf.asics");Console.WriteLine($"Valid: {verification.IsValid}");Console.WriteLine($"Timestamp: {verification.Timestamp:O}");Console.WriteLine($"TSA: {verification.TsaCertificate?.Subject}");// Renew timestamp for long-term archival (adds archive timestamp per ETSI EN 319 162-1 §5.4)varrenewed=awaitasicService.RenewFileAsync("contract.pdf.asics");File.WriteAllBytes("contract.pdf.asics",renewed.ContainerBytes);Console.WriteLine($"Renewed at: {renewed.Timestamp:O}");The byte[] API above holds the payload and the finished container in memory at once. For large
files, three members avoid that entirely — the payload is hashed in chunks and copied straight
into the container:
// Create without materialising the payload or the containerawaitusing(varinput=File.OpenRead("huge-archive.zip"))awaitusing(varoutput=File.Create("huge-archive.zip.asics")){varresult=awaitasicService.CreateToStreamAsync(input,"huge-archive.zip",output);Console.WriteLine($"Wrote {result.BytesWritten:N0} bytes, stamped {result.Timestamp:O}");}// Verify in place, without copying the container into a byte[]awaitusing(varcontainer=File.OpenRead("huge-archive.zip.asics")){varverification=asicService.Verify(container);Console.WriteLine($"Valid: {verification.IsValid}");}// Extract straight back out to a streamawaitusing(varcontainer=File.OpenRead("huge-archive.zip.asics"))awaitusing(varrestored=File.Create("restored.zip")){varfileName=awaitasicService.ExtractToStreamAsync(container,restored);Console.WriteLine($"Extracted {fileName}");}Streaming a 64 MB payload allocates roughly 300 KB, against 128 MB+ for the byte[] path.
Three things to know:
- All three require a seekable stream.
CreateToStreamAsyncreads the payload twice, because the TSA must see its hash before the first byte can be written; on the read side,ZipArchivedoes not reject a forward-only stream — it silently buffers the whole thing, which would defeat the purpose. A non-seekable stream throwsArgumentException; thebyte[]overloads are the fallback. CreateToStreamAsynccannot add a CAdES signature and throwsNotSupportedExceptionifSigningCertificateis set, since signing needs the whole payload in memory.CreateAsync(Stream, …)is not one of these. It buffers the stream into abyte[]internally; it saves you the read, not the memory. ASiC-E creation and renewal arebyte[]-only.
// In Startup / Program.csbuilder.Services.AddAsicSharp(options =>{options.TimestampAuthorityUrl=WellKnownTsa.DigiCert;options.HashAlgorithm=HashAlgorithmName.SHA256;});// Or bind from configurationbuilder.Services.AddAsicSharp(builder.Configuration.GetSection("AsicTimestamp"));// appsettings.json
{
"AsicTimestamp": {
"TimestampAuthorityUrl": "http://timestamp.digicert.com",
"HashAlgorithm": "SHA256",
"RequestSignerCertificates": true,
"UseNonce": true,
"Timeout": "00:00:30",
"MaxFileSize": 10485760
}
}A single TSA is a single point of failure. Set TimestampAuthorityUrls and each URL is tried in
order until one answers; the request only fails once every one of them has. The URL that actually
answered comes back on AsicCreateResult.TimestampAuthorityUrl, which may not be the first you
listed.
{
"AsicTimestamp": {
"TimestampAuthorityUrls": [
"http://timestamp.digicert.com",
"http://timestamp.sectigo.com",
"https://freetsa.org/tsr"
],
"HashAlgorithm": "SHA256"
}
}When TimestampAuthorityUrls is non-empty, the singular TimestampAuthorityUrl is ignored.
| Option | Default | What it does |
|---|---|---|
TimestampAuthorityUrl | DigiCert | The TSA to use. Ignored when TimestampAuthorityUrls is set. |
TimestampAuthorityUrls | (empty) | TSAs to try in order, falling through on failure. |
HashAlgorithm | SHA256 | Hash for timestamping. SHA384 and SHA512 also supported. |
RequestSignerCertificates | true | Ask the TSA to embed its certificate chain in the token. Keep it on — without it, a token cannot be verified offline. |
UseNonce | true | Send a random nonce and validate it in the response, so a captured token cannot be replayed as an answer to a new request. |
Timeout | 30 s | HTTP timeout per TSA request. |
MaxFileSize | 10 MB | Reject any single data file larger than this, on creation and per ZIP entry on read. null disables the limit. |
SigningCertificate | null | When set, a detached CAdES signature (META-INF/signature.p7s) is added, asserting who vouches for the data as well as when it existed. |
// In your servicepublicclassMyService{privatereadonlyIAsicService_asicService;publicMyService(IAsicServiceasicService){_asicService=asicService;}publicasyncTaskTimestampDocument(byte[]data,stringfileName){varresult=await_asicService.CreateAsync(data,fileName);// result.ContainerBytes, result.Timestamp, result.DataHash}}# Timestamp a single file (ASiC-S)
asicts stamp contract.pdf
asicts stamp contract.pdf --tsa http://timestamp.digicert.com --algorithm SHA256
# Timestamp multiple files (ASiC-E)
asicts stamp contract.pdf annex.pdf terms.txt
# Verify a container (auto-detects ASiC-S or ASiC-E)
asicts verify contract.pdf.asics
asicts verify bundle.asice --verbose
# Renew timestamp for long-term archival
asicts renew contract.pdf.asics
asicts renew bundle.asice --tsa http://timestamp.sectigo.com
# Extract files from any container
asicts extract contract.pdf.asics --output ./extracted/
asicts extract bundle.asice --output ./extracted/
# List known TSA servers
asicts info| Target | Status |
|---|---|
| .NET 10.0 | ✅ |
| .NET 8.0 | ✅ |
| .NET Standard 2.1 | ✅ (.NET Core 3.0+) |
| TSA | URL | Notes |
|---|---|---|
| DigiCert | http://timestamp.digicert.com | Default, widely trusted |
| Sectigo | http://timestamp.sectigo.com | Formerly Comodo |
| GlobalSign | http://timestamp.globalsign.com/tsa/r6advanced1 | |
| FreeTSA | https://freetsa.org/tsr | Free & open |
| Apple | http://timestamp.apple.com/ts01 | |
| Entrust | http://timestamp.entrust.net/TSS/RFC3161sha2TS |
document.pdf.asics (ZIP)
├── mimetype → "application/vnd.etsi.asic-s+zip"
├── document.pdf → Your original file (unchanged)
└── META-INF/
├── timestamp.tst → RFC 3161 timestamp token (covers the data file)
├── timestamp-002.tst → (After renewal) Archive timestamp covering timestamp.tst
└── signature.p7s → (Optional) CMS/CAdES signature
bundle.asice (ZIP)
├── mimetype → "application/vnd.etsi.asic-e+zip"
├── contract.pdf → Data file 1
├── annex.pdf → Data file 2
└── META-INF/
├── ASiCManifest.xml → Lists all files with their digests
├── timestamp.tst → RFC 3161 timestamp token (covers the manifest)
├── timestamp-002.tst → (After renewal) Archive timestamp covering timestamp.tst
└── signature.p7s → (Optional) CMS/CAdES signature
The timestamp in ASiC-E covers the ASiCManifest XML, which in turn contains cryptographic digests of every data file — so all files are transitively timestamped.
Because the proof runs through the manifest, AsicCreateResult.DataHash is the manifest's hash on
this profile, not any file's. Use FileNames and FileHashes when you mean a specific file — these
are the very digests written into the manifest, and both are populated for ASiC-S too, so you never
have to branch on the profile:
varresult=awaitasicService.CreateExtendedAsync(files);foreach(varnameinresult.FileNames!)Console.WriteLine($"{name}{result.FileHashes![name]}");Console.WriteLine($"Manifest hash (what the token covers): {result.DataHash}");Both are null on a result from RenewAsync, which adds a timestamp token and touches no data file.
Timestamp tokens have a validity period tied to the TSA certificate's lifetime (typically 5-10 years). For long-term archival, renew the timestamp before the original TSA certificate expires. Per ETSI EN 319 162-1 §5.4, each archive timestamp covers the previous token's bytes, creating a chain:
timestamp.tst → Covers data (or manifest)
timestamp-002.tst → Covers timestamp.tst bytes
timestamp-003.tst → Covers timestamp-002.tst bytes
Verification walks the full chain, ensuring each link is valid. The original timestamp proves when the data existed; renewal timestamps extend the proof indefinitely.
If you need to prove who timestamped the data (not just when), provide a signing certificate:
services.AddAsicSharp(options =>{options.SigningCertificate=newX509Certificate2("cert.pfx","password");});This adds a CMS/CAdES detached signature (META-INF/signature.p7s) alongside the timestamp.
IsValid is the conjunction of every step. One failing VerificationStep makes the whole
container invalid, and Error is then the "; "-joined Detail of each failing step (null when
valid). Error is a diagnostic message, not a stable code — don't parse it. Step names, by
contrast, are contract: tests assert them and asicts verify -v prints them verbatim.
Valid means unaltered, not trusted. No X509Chain is built for the TSA certificate, so a
self-issued TSA still yields IsValid == true. The certificate is handed to you on
TsaCertificate precisely so that the trust decision stays yours.
The Verify method returns detailed step-by-step results:
varresult=asicService.Verify(containerBytes);foreach(varstepinresult.Steps){Console.WriteLine($"{(step.Passed?"✓":"✗")}{step.Name}: {step.Detail}");}// ✓ Container structure: Valid ZIP archive// ✓ MIME type: application/vnd.etsi.asic-s+zip// ✓ Data file: contract.pdf (125432 bytes)// ✓ Timestamp token decode: Token decoded, timestamp: 2026-02-28T14:30:00Z// ✓ Timestamp signature: Valid, signed by: CN=DigiCert Timestamp 2024, O=DigiCert// ✓ Data hash match: Hash in timestamp matches data file// The data itself, without a second Extract() call — ASiC-S only, null for ASiC-Eif(result.DataBytesis not null)File.WriteAllBytes("recovered.pdf",result.DataBytes);An ASiC-E timestamp covers the ASiCManifest, and the manifest lists one hash per data file. A ZIP entry the manifest never references is therefore covered by no proof of existence at all — yet the referenced files' proofs are untouched, so the container still verifies.
Those entries are reported rather than ignored:
varresult=asicService.VerifyFile("bundle.asice");// IsValid says "nothing listed in the manifest was altered"Console.WriteLine($"Valid: {result.IsValid}");// ... which is not the same as "every file here was timestamped"if(result.UnreferencedFileNamesis{Count:>0}uncovered)Console.WriteLine($"Not covered by the timestamp: {string.Join(", ",uncovered)}");UnreferencedFileNames is empty when every entry is referenced and null for ASiC-S, which has no
manifest. It deliberately does not affect IsValid, since third-party containers may
legitimately carry extra entries — if your policy is that every byte must be covered, enforce it on
this list. ExtractAll already refuses to hand back uncovered entries, so a verify-then-extract
caller cannot surface unstamped bytes as though they were timestamped.
Everything this library throws derives from AsicTimestampException, so one catch covers all of
it:
AsicTimestampException
├── TimestampAuthorityException — every configured TSA URL failed
├── InvalidAsicContainerException — the container is unreadable or structurally wrong
└── AsicVerificationException — a verification operation could not be completed
try{varresult=awaitasicService.CreateFromFileAsync("contract.pdf");}catch(TimestampAuthorityExceptionex){// No TSA answered — retry later, or add fallbacks via TimestampAuthorityUrlsConsole.Error.WriteLine($"No timestamp authority available: {ex.Message}");}catch(AsicTimestampExceptionex){Console.Error.WriteLine($"Timestamping failed: {ex.Message}");}Note what does not throw:
VerifyandVerifyFilereport, they do not throw. A corrupt ZIP, a missing entry, an undecodable token — all come back asIsValid == falsewith a failed step. The only exceptions areArgumentExceptionfor empty input andFileNotFoundExceptionfor a missing file.GetContainerTypenever throws. Anything unrecognized isAsicContainerType.None.- Bad arguments still throw
ArgumentExceptionfrom the create, extract and renew paths — an empty payload, a file name containing a path separator, or data overMaxFileSize.
- ETSI EN 319 162-1 — Associated Signature Containers (ASiC) baseline (ASiC-S)
- ETSI EN 319 162-2 — Associated Signature Containers extended (ASiC-E)
- ETSI TS 102 918 — ASiCManifest XML schema
- RFC 3161 — Internet X.509 PKI Time-Stamp Protocol
- RFC 5652 — Cryptographic Message Syntax (CMS)
- EU eIDAS Regulation — Electronic identification and trust services
MIT — see LICENSE