v2.0.0
The first major version since 1.0. Every package moves to 2.0.0 together, as usual.
Two things made a major version necessary: assemblies are strong-named, which changes their
identity, and the analyzer that was supposed to be locking the public API is now actually
locking it, which meant fixing the defects in that surface while doing so was still free.
Most of the rest is work that had to land before the surface froze.
Read Upgrading to 2.0 first if you bind to an assembly
identity by hand — a PackageReference needs no change, but a binding redirect, an
InternalsVisibleTo, or an Assembly.Load string does.
Breaking changes
Assembly identity
- All eight packages are strong-named (
eng/VellumPdf.snk), with public key token
b2757187a6d18ae5.AssemblyVersionis pinned to2.0.0.0for the whole 2.x line, so
servicing releases will not force another rebind. (#53)
Public API
| Change | Was | Now |
|---|---|---|
PdfSignature.ByteRange |
int[] |
ReadOnlyMemory<long> (#178) |
PdfLinkAnnotation.Flags |
int |
PdfAnnotationFlags (#176) |
TextEncodingWarning character |
char |
System.Text.Rune (#177) |
CcittImageLoader.Load |
two overloads, four positional knobs | one overload taking CcittOptions (#177) |
PdfPreflight.Validate(PdfDocumentReader, PdfConformance) |
public | internal (#176) |
HttpRevocationClient(HttpClient, TimeSpan) |
both arguments required | both optional, matching HttpTimestampClient (#177) |
PdfSignatureSettings.SubFilter |
any string accepted | only ETSI.CAdES.detached and adbe.pkcs7.detached (#176) |
SignaturePlaceholderOptions.SubFilter |
any string accepted | the same two values |
Each is explained under Added, Changed, or Fixed below.
Behaviour
- A PAdES signature no longer carries a CMS
signing-timesigned attribute. ETSI
EN 319 142-1 admits only the signed attributes its table 1 lists, andsigning-timeis not
among them — PAdES conveys the claimed time in the signature dictionary's/M, which this
library already wrote from the same value. Emitting it anyway held every signature at
PAdES-BES instead of PAdES-BASELINE-B. Code readingsigning-timeout ofSignerInfoon a
signature written with the default/SubFilter ETSI.CAdES.detachedwill no longer find it;
/Mstill carries the value, andadbe.pkcs7.detachedkeeps the attribute, since it makes
no ETSI claim. (#170) - A tagged document with no tagged content now emits
/StructTreeRoot. Setting
Tagged = trueand drawing nothing previously produced no structure tree at all, which
failed PDF/A-2a and PDF/UA-1 validation.Taggednow means tagged. (#120) - A certificate with a non-minimally-encoded serial is rejected up front on the
in-process signing paths, with a message naming the offending bytes and the way forward,
instead of an opaqueArgumentExceptionraised from inside the BCL's CMS encoder. The
exception type is unchanged, so acatch (ArgumentException)behaves as before. (#167) vellum-preflight --format jsonreports check accounting differently.summary.total
used to befailed + passed + notEvaluated, adding a count of assertions to a count of
checks; it is now the profile's catalog size.summary.failedChecks,summary.inconclusive,
and the matchingfailedChecksandinconclusivearrays are new, and the text output gains
anINCONCLUSIVEline. Exit codes and the conformance verdict are unchanged.- A
nulloptions argument now means "use the default" rather than throwing, on
CcittImageLoader.Loadand theHttpRevocationClientconstructor. With= nulldefaults
there is no way to tell an omitted argument from an explicitly null one, so the old
ArgumentNullExceptionfired on exactly the call the default exists to serve. (#177) - External-signer CMS digest
AlgorithmIdentifiers now match RFC 5754 — both
SignedData.digestAlgorithmsandSignerInfo.digestAlgorithmomit their parameters field
instead of carrying a redundant DER NULL, per RFC 5754 §2 ("implementations MUST generate
SHA2 AlgorithmIdentifiers with absent parameters").SignerInfo.signatureAlgorithmwas
already correct and is unchanged. Neither change touches the signature value:
AlgorithmIdentifiers sit outside the SignedAttrs digest. (#166)
Added
-
Async I/O surface for
Save,Sign, andLoadTrueTypeFont—PdfDocument.SaveAsync,
Document.SaveAsync(Stream)/SaveAsync(string),Document.LoadTrueTypeFontAsync, and
SigningExtensions.SignAsync(both overloads), each taking aCancellationToken. Existing
sync methods are unchanged.ITimestampClientandIRevocationClientgain default-implemented
GetTimestampTokenAsync/GetRevocationDataAsyncmembers, so custom implementations keep
compiling unchanged. (#54) -
IExternalSigner— a two-phase async external-signer API for a cloud KMS or remote HSM
where the signing call itself is a network round-trip (Azure Key Vault, AWS KMS, GCP KMS). No
BCL API supports this today, sinceCmsSigneronly accepts a synchronous, in-process private
key; VellumPdf computes the CMS signed-attributes digest itself, hands it to the caller's async
signer, and assembles the resultingSignerInfoby hand. SetPdfSignatureSettings.ExternalSigner
and sign withSignAsync; the synchronousSignoverloads throw, since there is no synchronous
way to bridge a network call.EcdsaSignatureConverteris included for KMS providers, such as
Azure Key Vault, that return a raw ECDSA signature rather than the DER encoding CMS requires. (#165) -
PdfSignatureSettings.ExternalPrivateKey— signs with a private key supplied separately
fromCertificate, for HSM/PKCS#11/cloud-KMS-backed certificates whose key isn't attached to
theX509Certificate2(Azure Key Vault, AWS KMS,Pkcs11Interop.X509Store, and similar).
Windows CNG-integrated smart cards and hardware tokens already work through the existing
Certificate-only path and need no change. (#54) -
The ESS
signing-certificate-v2signed attribute (RFC 5035) is now emitted on both
signing paths, so a signature written with the default/SubFilter ETSI.CAdES.detached
carries the attribute the ETSI profile expects rather than only claiming to.hashAlgorithm
is omitted for SHA-256 (the DERDEFAULTrule) and written with absent parameters for
SHA-384/512;issuerSerialis built from the same bytes as theSignerInfo, so the two
cannot disagree. (#168)Together with the
signing-timeremoval below, this is what makes the/SubFilterclaim
true rather than merely asserted. Measured with the EU DSS reference validator, against
fixtures differing only in the code that signed them:Signature 1.11.0 2.0.0 B-B, ETSI.CAdES.detachedPDF-NOT-ETSIPAdES-BASELINE-BB-T, with an RFC 3161 timestamp PAdES-BESPAdES-BASELINE-Tadbe.pkcs7.detachedPKCS7-BPKCS7-B(unchanged) -
A PDF/A-2a check for page content that no structure element describes — reported at
ISO 19005-2 clause 6.7.3.3, at Warning severity, since veraPDF's own PDF/A-2a profile
implements no equivalent rule and the verdict must keep matching it. (#120) -
PdfAnnotationFlags— the ISO 32000-1 Table 165 annotation bitfield as an enum, so the
§6.3.2 PDF/A requirement can be written asPdfAnnotationFlags.Printrather than4.
Emitted bytes are unchanged, and a test pins that. (#176) -
SubFilterEtsiCAdESDetachedandSubFilterAdbePkcs7Detachedconstants on both
PdfSignatureSettingsandSignaturePlaceholderOptions, so the two accepted values need not
be hardcoded. (#176)
Changed
VellumPdf.Conformancegraduates from Preview to Stable.VellumPdf.Cliwas already
Stable while the engine it wraps was Preview. veraPDF parity is about 99% across
PDF/A-2b/2u/2a and PDF/UA-1, both paths of every rule are cross-validated against it in CI,
and the remaining gaps are tracked as issues — better stated as known issues on a stable
package than as a preview label on the whole engine. (#173)- The public API surface is now under the analyzer gate the README describes. That README
has claimed "the public API is locked (analyzer-enforced)" since 1.0, but
VellumPdf.Signing'sPublicAPI.Shipped.txtwas a zero-byte file and Kernel's had not been
touched since 1.2.0, so 232 entries across five
packages sat where the analyzer permits silent removal. They are recorded now, and every
PublicAPI.Unshipped.txtis reset to its header, so a 2.x addition shows as a diff against
an accurate baseline.VellumPdf.Readeris deliberately left Unshipped: it stays Preview
through the v2.1 structural-reader work, and the convention is that a surface moves at
graduation, not before. (#173) - The synchronous timestamp and revocation clients no longer block on an async call.
HttpTimestampClientandHttpRevocationClientalready issued the request through
HttpClient.Send; only the response body was read by blocking onReadAsByteArrayAsync,
which deadlocks on a synchronization context and starves the thread pool under load. That is
HttpContent.ReadAsStreamnow, which is genuinely synchronous. NoGetAwaiter().GetResult()
remains anywhere insrc/. The synchronous interface members stay: they are the required
ones while the async counterparts are default-implemented, so removing them would break every
existing implementation. (#177) System.Security.Cryptography.Pkcsmoves to 10.0.11, matching the .NET 10 servicing band.
Fixed
/ByteRangeoffsets are no longer truncated toint.PdfSignature.ByteRangewas
int[]filled from alongthrough an unchecked cast, so an author-controlled offset past
int.MaxValuewrapped silently — 4,294,967,296 became 0, which madeCheckByteRangereturn
early and skip the ISO 19005-2 §6.4.3-1 coverage check entirely. The same truncating parse
existed in two independent places, and both are fixed. (#178)- A CRL that revokes the signing certificate is no longer treated as valid when that
certificate's serial is non-minimally encoded. The comparison held the certificate's raw
serial bytes against the CRL's, which are always minimal because a real CA's CRL is DER, so
it never matched and the revoking CRL was embedded in the/DSS. Both sides are normalized
now. (#167) - A Warning no longer withdraws a passing claim in the preflight report, and a failing rule
now blames the check it names rather than every catalogued check sharing its clause number.
Withdrawn checks used to appear in no section of the report at all. vellum-preflightprints its findings when there are findings, rather than only when the
verdict is FAIL.--fail-on warningused to fail a run while listing nothing, and
--severity warninglisted nothing on a conformant document whose own header said a warning
existed./StructParentskeys no longer wrap into each other's key space — the key and the
/Numsentries are range-checked.- Long-term validation no longer reuses a live object number when a document's
/Sizeis
smaller than the highest object number actually present. - An astral character is reported once, as itself, in
TextEncodingWarning. It used to be
reported twice, as its two UTF-16 surrogate halves, with a code point in 0xD800–0xDFFF — a
value no Unicode character has. An unpaired surrogate is reported as U+FFFD. Emitted bytes are
deliberately unchanged: an astral character still writes two?bytes, because collapsing it
to one would shift text a caller had already measured. (#177) - Decode-to-raster on a Group 3 1-D CCITT stream with byte-aligned rows is reachable — the
ImageLoadOptionsoverload could not carry the CCITT knobs at all, so that combination had no
public expression. (#177) - Clearer
ExternalSignerCmsfailure messages — aCheckSignaturefailure now names
RSASSA-PSS (unsupported — seeIExternalSigner's documentation) and a KMS key id pointing at
the wrong key as likely causes, rather than pointing only at signature format. (#167)