Table of Contents

Migrating from v0.8 to v0.9

Overview

v0.9.0 keeps the cross-format signing model introduced in v0.8.0, but makes level-fulfillment claims stricter. A successful timestamp is now bound to the exact request, external signatures are verified before packaging, and each supported B-LTA topology constructs and verifies its ETSI archive preimage.

CAdES and XAdES archive level

AdesBaselineProfile.Archive(...) produces a standards-defined archival timestamp rather than hashing the serialized CMS/XML container.

  • CAdES writes archiveTimestampV3; its RFC 3161 token carries ATSHashIndexV3 as a token unsigned attribute and covers the ETSI-defined archive input exactly once.
  • XAdES supports the signer-produced non-distributed topology, same-document distributed unsigned properties addressed through ordered bare-name Include references, and preceding CounterSignature properties. Its archive input processes XMLDSIG references, canonicalized SignedInfo, SignatureValue, KeyInfo, unsigned properties, and unreferenced ds:Object values in ETSI order. Distributed Include processing removes comments before canonicalization.
  • External Include resources, ambiguous multi-signature operations without an explicit target, and unsupported XMLDSig transforms still fail strictly with SigningErrorReason.LevelNotAchievable, or explicitly downgrades through ReturnLowerLevel; it never reports B-LTA without coverage.

The normal archive profile remains:

builder.WithLevel(AdesBaselineProfile.Archive(timestamp, ltv));

PAdES document timestamps are unchanged by this restriction.

Multiple XAdES signatures

XadesSignatureValidator.Validate(...) now accepts exactly one XMLDSig signature. It returns an invalid structured result for a document containing multiple signatures, rather than silently validating the first one. Use ValidateAll(...) to receive one XadesValidationResult per signature, with its SignatureId when the XMLDSig element declares one:

IReadOnlyList<XadesValidationResult> results = validator.ValidateAll(signedXml, trustAnchors);

TSA response validation

Custom or test TSAs must return a real RFC 3161 TimeStampToken containing:

  • CMS SignedData with id-ct-TSTInfo content;
  • the requested hash OID and message imprint; and
  • the same nonce emitted in the request.

Malformed tokens, a different imprint/OID, or an omitted/mismatched nonce now fail instead of being embedded. Test doubles should construct their response from the received request rather than return a fixed token.

XAdES signature timestamps must cover the exclusive-canonicalized ds:SignatureValue element. v0.8 and earlier SimpleSign artifacts that timestamped the decoded signature-value bytes are intentionally no longer accepted as B-T.

External signing

IExternalSigner now receives the resolved PSS parameters when the selected scheme is id-RSASSA-PSS: message digest, MGF1 digest, salt length, and trailer field. Its returned RSA or ECDSA bytes are verified against the supplied certificate before CMS/XML generation. An invalid result throws SigningException with SigningErrorReason.AlgorithmIncompatible; an exception from the signer is normalized as SigningErrorReason.ExternalSignerFailure.

The digest used by WithHashAlgorithm must agree with any combined signature OID selected through WithSignatureAlgorithm. For example, rsa-with-SHA512 cannot be combined with SHA-256.

When only a combined signature OID is configured, all three formats infer its digest consistently. The library convention for an unrestricted RSA-PSS key is MGF1 with the message digest, a salt length equal to that digest length, and trailer field 1. A certificate with explicit id-RSASSA-PSS public-key restrictions rejects an incompatible request.

The payload-only FuncExternalSigner adapter was removed. Implement IExternalSigner directly; this is required so an HSM, KMS, or remote service can receive and honor the complete resolved signing request.

PAdES input and execution lifecycle

PadesSigner.Document(byte[]) snapshots the input as before. Builders created from PadesSigner.Document(Stream) are now single-use across all fluent clones:

var builder = PadesSigner.Document(pdfStream).WithCertificate(certificate);
await builder.SignAsync();
// Create a new builder before signing another document.

Writing to a caller-owned output stream is transactional: it is copied only after all strict enrichment steps complete. Appearance images, extra appearance lines, metadata, and CMS extra attributes are snapshotted when configured.

Error handling

Certificate validity failures use SigningException and a stable reason:

  • CertificateExpired for an expired certificate;
  • CertificateNotCurrentlyValid for a certificate whose validity has not started.

The check uses the real UTC operation time. WithSigningTime remains signed document metadata and does not bypass credential validity.

Canonical auxiliary APIs

BatchSigner now uses the same AdesBaselineProfile, IExternalSigner, IHttpClientProvider, and SignatureFieldOptions configuration as the primary PAdES builder. The legacy TSA/LTV sequence and delegate-based external signer overloads were removed.

XAdES validation reports AdesBaselineLevel; the format-specific XadesLevel and unused CadesLevel types were removed. CAdES and XAdES configure diagnostics only with .WithLogger(logger) after Document(bytes).

Deferred PAdES signing

The static deferred operations and mutable field/timestamp convenience methods were removed. Start a workflow with a document builder and resume phase two from its persisted session:

var signer = DeferredSigner.Document(pdfBytes)
    .WithCertificate(certificate)
    .WithSessionIntegrityKey(sessionIntegrityKey)
    .WithFieldOptions(new SignatureFieldOptions { SignerName = "Jane Doe" })
    .WithLevel(AdesBaselineProfile.Basic());

var prepared = await signer.PrepareAsync();
// Persist prepared.SessionData and send prepared.HashToSign to the key holder.

byte[] rawSignature = await keyHolder.SignAsync(prepared.HashToSign);
byte[] signedPdf = await DeferredSigner.Resume(prepared.SessionData, sessionIntegrityKey)
    .CompleteAsync(rawSignature);

sessionIntegrityKey is mandatory, must contain at least 32 bytes, and remains server-owned. It authenticates the serialized session with HMAC-SHA256 and is never serialized with it. Persist the session only on the server and send the browser a single-use opaque identifier rather than session bytes.

DeferredSigner.Document snapshots the input. Resume deliberately does not need the original PDF or certificate: all cryptographically necessary state, including the requested baseline level and TSA endpoint, is inside the server-side session. Runtime HTTP providers are not serialized; attach a server-owned provider after Resume(...) when the endpoint requires one. A deferred terminal returns only bytes, so it accepts strict profiles only; a best-effort profile is rejected rather than silently returning a lower baseline level.