Releases: Tim81/VellumPDF
Release list
v2.3.0
Breaking changes
-
A same-revision
/XRefStmno longer overrides a classic cross-reference table's free entry
for the same object. Given one revision whose classic table marks object N free and whose
/XRefStmdefines it,PdfReadernow resolves N tonull, matching qpdf; it previously
resolved N from the stream. The construct is describable beyond a hand-freed/Contents: an
incremental update from a writer that copies/XRefStmforward without understanding it, and
also carries the previous revision's free entries along unchanged, produces exactly this shape —
the case MatthiasValvekens describes in
pdf-association/pdf-issues#237, by
his own account without having checked it against a real processor ("I'm not aware of any
processors that do either of this, so maybe my intuition is completely wrong"). The
cross-section arrangement ISO 32000-2 §7.5.8.4 actually describes, where the free entry sits in
an earlier/Prevrevision, is unaffected only in the two-revision case; a hybrid revision
sitting between two others in a/Prevchain is not exempt, and loses its own copy the same way
a same-revision one does while also suppressing whatever a still-older revision defined. What
survives a chain like that is the definition in whichever revision is newest among the ones that
mention the object at all, not simply whichever one sits outside a same-revision pairing. This
aligns with the reading in issue #237, open at the time of writing; if it or errata
#523 resolves the other way, this is
revisited.VellumPdf.Readeris still Preview, where a behaviour change would ordinarily stay
under Changed, but silently dropping rendered content is closer to what this section otherwise
covers than a changed exception type is, so it's recorded here instead. (#206)Consequences, measured rather than assumed except where a bullet says otherwise — the
/Encrypt
shape has no test, and the/DecodeParmshalf rests on a code reading rather than a fixture:- A page's content stream can disappear: if N is a page's
/Contents, the page now has no
content stream where it had one. - A page can disappear outright, not just its content. If N is the page-tree root, the page
count drops from 1 to 0; if N is an intermediate/Pagesnode, freeing it drops only its own
subtree — a three-page document with a two-page branch under that node loses those two pages
and keeps the third, 3 becoming 1. The surviving root still declares its old/Count, so a
caller trusting/Countand one walking/Kidsnow disagree.PreflightContext.WalkPages
walks/Kidsthe same way, so page-scoped PDF/A rules silently stop covering the lost subtree
too. - A rarer variant costs more: if N is the object an
/Encryptreference points at, the document
now fails to open withInvalidDataExceptioninstead of decrypting, because/Encryptcan no
longer resolve to a dictionary at all. This shape has no test yet. - Worse still, if N is the catalog itself, the document does not open at all:
PdfReader.Open
throwsInvalidDataException: Malformed PDF: /Root does not resolve to a dictionary.where it
previously opened. - A
/Filteror/DecodeParmsobject resolving tonulldoes not degrade tonulloutput —
it produces wrong bytes.PdfFilters.GetFilterListtreats an unresolvable/Filteras no
filter at all, soGetDecodedStreamDatareturns the raw, still-encoded body. Measured: a
24-byte plaintext body, FlateDecode-compressed to 32 bytes (zlib header78 9C), comes back as
those 32 raw bytes instead of the 24-byte plaintext once its/Filterreference is freed this
way; a byte-identical control with the filter object live decodes correctly./DecodeParms
degrades the same way — a PNG predictor's rows are never undone. qpdf degrades identically
here, so this is not a divergence from the oracle, only from what this entry previously implied
the general case is. - When N is an
/ObjStmcontainer rather than an ordinary object, its compressed members have
to drop out of the merged table along with it, or a member nobody asked to free resolves
through a container that no longer exists andPdfDocumentReaderthrows
InvalidDataException: Object stream container N not found in xref.— a more surprising
failure than most other consequences here, and harsher than qpdf, which resolves such a member
tonull(with a warning) and keeps the document open. Fixed: such a member now resolves to
null, where it previously resolved to its live compressed value. §7.5.8.4's own EXAMPLE frees
a hidden object's/ObjStmcontainer alongside its members in the same table, and the reader's
existing member-level free tracking already handled that case before this change; what the fix
actually needed is narrower — a writer that frees the container without also freeing its
members, leaving their compressed rows pointing at nothing. - That container-cascade removal reaches further than "a freed object drops out": members never
themselves named by any free entry, only orphaned when their container was, also drop out of
the merged table, and therefore out of anything built from it —
PreflightContext.EnumerateIndirectObjectsandEnumerateStreamsincluded. Measured: an
ISO19005-2:6.1.13-nameError for an over-long name inside such a member disappears from a
PDF/A validation, even though nothing in the file ever freed that member's own object number. - Freeing the
/AcroFormthis way makesreader.Signatures.Countgo from 1 to 0 with no
exception and no warning, so code that reads an empty signature list as "unsigned" now
misreads a signed document. - A freed object drops out of the merged table entirely, so
ObjectNumbersshrinks, and — when
/Sizealso understates the count —NextFreeObjectNumbercan shrink with it, both feeding
code outside this package (PreflightContextandObjectLayoutRule;DssBuilderand
ArchiveTimestampBuilder). The fixture measuring this shrink has its own/Sizealready
understating the file's real object count, independent of anything freed — ISO 32000-2 Table 15
already calls that non-conforming ("any object in a cross-reference section whose number is
greater than this value shall be ignored and defined to be missing by a PDF reader") — so the
shrink measured there is not attributable to this change alone. TheNextFreeObjectNumber
shrink has no constructed consequence beyond itself; theObjectNumbersshrink has two, both
below — a disappearing name-limit Error and a flipped PDF/A verdict, each reached through
PreflightContext, the consumer this bullet names. - This reading can flip a PDF/A conformance verdict, not only drop content — and the verdict it
flips to is the one veraPDF gives. A PDF/A-2B file whose only violation is an external-stream
object (/F//FFilter//FDecodeParms) isIsCompliant=Falsewith rule
ISO19005-2:6.1.7.1-external-streamwhen that object is live, andIsCompliant=Truewith zero
assertions once it is freed this way. veraPDF 1.30.2 — the validator this repository gates on,
and the closest thing PDF/A has to a reference implementation — reaches those same two verdicts
on those same two files:FAIL … 2bagainst ISO 19005-2:2011 clause 6.1.7.1 test 3 for the live
one,PASS … 2bfor the freed one. Two controls rule out the deflationary readings: a variant
where the object is defined only by the/XRefStmand not freed still fails, so veraPDF is
not simply ignoring/XRefStm; and a variant violating a different rule flips the same way on
clause 6.1.7.2, so the agreement is about the cross-reference reading rather than one clause.
So the flip lands on veraPDF's side of a question the specification leaves open — which makes it
a correction under this reading rather than a straightforward cost, with the same caveat as
everything else here: if #237 or #523 resolves the other way, so does this. The mechanism:
PdfPreflight's file-structure rules walk the cross-reference keyspace directly
(PreflightContext.EnumerateStreams), specifically so an object the file never draws still gets
checked, so an object this reading removes from the merged table drops out of that enumeration
the same way a freed page does. The control differs from the freed file only in that object's
xref row, and is non-compliant on both builds.
- A page's content stream can disappear: if N is a page's
-
A type-2 (compressed) cross-reference entry whose container has no live entry anywhere in the
merged table now resolves tonullinstead of sometimes throwing. The container-cascade sweep
above used to also require the container to have actually been freed by some revision
(freed.Contains(container)) before dropping its orphaned members. That pairing looked like it
distinguished "genuinely freed" from "never mentioned by anything", but it does not: object 0
and any object an ordinary incremental update deletes are already infreedregardless, so the
pairing told the two cases apart from neither in practice. Dropped: the sweep now runs whenever
the container is absent from the merged table, freed or not. The one behaviour change this
reaches beyond the rest of this entry: a dangling type-2 reference to a container no revision
ever mentions, in a file with no free entry anywhere near it, now resolves tonullrather than
throwingInvalidDataException: Object stream container N not found in xref., matching qpdf. A
member the sweep drops this way is absent from_xrefitself — the table a future full
re-serialisation (tracked in #186) would walk to decide what to emit — so it is not carried ...
v2.2.0
Breaking changes
PdfDictionary.Set,TryGetandGetnow throwArgumentNullExceptionfor anullkey.
PreviouslyTryGet(null, out _)returnedfalse,Get(null)returnednull, and
Set(null, value)appended an entry that only failed later — with aNullReferenceExceptionout
ofWriteTo, once the dictionary was serialised. All three are Stable API, which is why this is
recorded here rather than under Security, where the rest of this fix lives: without the guard, a
nullkey would behave differently depending on which side of the internal indexing threshold a
dictionary sits — returningfalsebelow it, throwing above it — exactly the property that
threshold is supposed to be free to move without changing what callers observe. (#208)
Security
-
OwnerPassword = ""beside a realUserPasswordproduced a file with no real password
protection at all, in every release from v1.0.0 through v2.1.0.??treats an empty string as
a value, not as "unset", so that combination sealed/Oand/OE(ISO 32000-2 §7.6.4.4.6,
Algorithm 9) under the empty password instead of falling back to the user password. At/R6 an
empty password fails the/Ucheck and satisfies the/Ocheck, so any conforming reader lands
on owner access when no password is supplied. That is a property of the file itself, not of the
order a particular reader tries/Uand/Oin. Verified concretely: a file written by v2.1.0
withUserPassword = "hunter2", OwnerPassword = ""opens with no password supplied at all,
and the catalog, hence the whole object graph, decrypts. The document's confidentiality is gone,
not merely its permission flags: nothing enforcedPermissions, but nothing enforced
UserPasswordeither.OwnerPassword = ""beside a non-emptyUserPasswordnow throws instead
of producing a file. Both passwords empty is unchanged, since an unprotected document is
legitimate and ISO 32000-2 permits an empty owner password.OwnerPassword = nullis unchanged
too: that is the documented fallback to the user password as owner, and it does not reproduce
this defect (/Ois sealed under the real user password, not the empty string) — though anyone
who can open the document still holds owner access under it, soPermissionsstill binds nobody.
The guard sits inStandardSecurityHandler's constructor, which is Stable API and callable
directly, and inPdfDocument.Encrypt, so the failure surfaces at the call site rather than at
Save();VellumPdf.Layout.Document.Encryptinherits it by delegation. (EncryptionSetup.TryAuthenticate,
this library's own authentication order, is why our reader reports such a file's access level as
owner rather than user — that explains what we report, not why the exposure exists.)A document already written with this shape cannot be fixed in place: correcting it means
re-deriving/Oand/OE, which needs a real owner password behind them from the start. Affected
documents must be re-encrypted from the original plaintext, with a distinctOwnerPasswordthis
time. (#211) -
PdfDictionarylookup is no longer quadratic in the key count.SetandTryGetnow build a
hash index once a dictionary passes 16 entries, rather than scanning the whole entry list on every
call. The/Encryptdictionary is parsed, and copied again byEncryptionSetup.DereferenceValues,
before any password is checked, on a file anyone can send, so a hostile document naming tens of
thousands of keys there previously cost time quadratic in that count with nothing to show for it:
opening a fixture with an 80,000-key/Encryptdictionary took about 27 seconds before this fix and
well under a second after.EncryptionSetup's/CFcap (MaxCryptFilters, still 64) keeps its
comment but loses the reason it used to give: every term that touches/Encryptis linear now, so
the cap no longer bears any of the weight of bounding this cost. It stays because a real document
names one or two crypt filters, not sixty-four. (#208)
v2.1.0
Breaking changes
-
A password-protected document now reaches
PdfPreflightasPdfPasswordException, which no
existingcatchcovers. Every prior version threwUnsupportedPdfFeatureException, and so
NotSupportedException, for any/Encryptat all, so that is what a caller of
PdfPreflight.ValidateorPdfPreflight.DetectClaimedProfileswrote to detect an encrypted file.
Both are Stable API in a Stable package, and both now open an encrypted document whose empty user
password suffices, and throwPdfPasswordExceptionfor one that needs a non-empty password. That
exception derives fromExceptiondirectly, and deliberately so: a document the reader
understands but was not given the credentials for is not an unsupported feature. An existing
catch (NotSupportedException)around either method therefore lets it through. Catch
PdfPasswordExceptionbeside it. (#97) -
PdfDocument.DocumentIdnow throwsArgumentExceptionfor a value that is not 16 bytes.
Previously any other length was accepted and then written as no/IDat all — silently. ISO
32000-2 Table 15 requires/IDonce/Encryptis present, so on an encrypted document that
produced a file qpdf rejects outright ("invalid /ID in trailer dictionary"), with nothing to tell
the caller which value caused it. On an unencrypted document the old behaviour merely omitted an
optional entry, so code that set a wrong-length id and relied on that omission now sees an
exception.DocumentIdis Stable API, which is why this is recorded here rather than under
Fixed. (#97)
Added
-
A committed corpus of encrypted PDFs, one per standard-security-handler
/V+/Rcombination.
Generated once with qpdf and committed rather than shelled out for at test time, so the corpus is
byte-identical on CI and locally and leaves no silently-skipped gate. A guard test pins each fixture
by SHA-256 as well as/V,/Rand/CFM: qpdf refuses to write RC4 without--allow-weak-crypto
and still leaves a zero-byte file behind, and two fixtures are both/V 4 /R 4, differing only in
the cipher — neither an existence check nor a/V+/Rcheck would notice either. Groundwork for
the decrypt side. (#99) -
Hand-written RC4 and MD5 primitives for the legacy (
/V1–2) decryption path. Internal only —
no public surface yet, no reader wiring, no security handler; that is a separate change. The BCL
has never shipped RC4, and itsMD5type defers to the OS crypto library everywhere except Browser
WASM, where MD5 is unsupported outright, so decrypting an old PDF under Blazor WASM would otherwise
be a dead end.PdfDocument's/IDgeneration (ISO 32000-2 §14.4) now goes through this MD5 too,
in place of the BCL call it used before, so the codebase is actually clear of CA5351 (flags MD5 as
weak) rather than clear of it only on the new, not-yet-wired decryption path — and Browser WASM
document writing, not just decryption, no longer depends on a platform MD5 that isn't there. RC4 is
verified against all three vectors in draft-kaukonen-cipher-arcfour-03 Appendix A, including the
309-byte vector that runs the keystream past its first 256-byte cycle; MD5 against the full RFC
1321 §A.5 suite plus a length sweep across the padding and block boundaries, and a differential
sweep against the BCL./IDitself is pinned by a known-answer test, because nothing else
pinned it: every golden document sets its own id, and the computed one folds in a millisecond
timestamp, so no snapshot could cover it. Groundwork for the decrypt side. (#97) -
The decrypt side of the Standard security handler, covering every
/V+/Rcombination the
committed corpus (#99) exercises:/V1//R2 (RC4-40) through/V5//R6 (AES-256).
Internal only: no public surface, noPdfReaderwiring, no/Encryptgate; a decrypting reader
is separate work. Covers Algorithm 2 (file key from a password), Algorithms 4/5 and 7 (verifying
a user or owner password), Algorithm 2.A and the R6 permission check for/V5, and the
per-object key that folds in the object's generation number, which is why this had to wait for
#121. Verified against every corpus fixture: deriving the file key from both the correct and a
wrong password for each one, and decrypting a real content stream to the exact bytes qpdf's own
encryption produced — checked against an external tool's output, not only internal consistency.
The/EncryptMetadata falsefixtures pin that Algorithm 2 step (f) shifts the derived key, not
just/U. The empty user password most encrypted PDFs actually use had no fixture when this
landed and was covered by independently computed vectors;enc-aes-128-emptyuser.pdf, added with
the reader wiring below, covers it end to end. (#97) -
Decryption on read:
PdfReader.Opentakes a password and reads encrypted PDFs. The Standard
security handler at/V1, 2, 4 and 5 and/R2 through 6 — RC4-40 through RC4-128, AES-128
(/AESV2) and AES-256 (/AESV3) — plus the/Cryptfilter (ISO 32000-2 §7.4.10) and crypt
filters naming different methods for strings and streams. Strings are decrypted inResolveunder
the identity of the indirect object containing them (ISO 32000-1 §7.6.2, Algorithm 1) and stream
bodies on the decode path;ParsedStream.RawBodystill holds the verbatim file bytes, which
StreamRuleandHexStringRuleneed for byte offsets and lengths.The supplied password is tried as the owner password first and the user password second, so one
that satisfies both reports the higher-privilege access. A wrong one throws the new
PdfPasswordException.PdfDocumentReader.Encryptionreports/V,/R, the stream and string
ciphers, key length, permissions,/EncryptMetadataand which password authenticated — no key
material, and no/O,/U,/OEor/UE.What is left in the clear, per the spec: the trailer
/ID, the/Encryptdictionary's own
strings, cross-reference streams (§7.5.8.2, body and dictionary alike), streams whose data lives
in an external file (§7.6.1), the document's metadata stream under/EncryptMetadata false
(Table 21 — a page's or an XObject's metadata is not exempt), and a signature dictionary's
/Contents, which ISO 32000-1 leaves unstated: a signer patches those hex digits into
already-serialized bytes, so decrypting them would corrupt/ByteRangeverification. That last
exemption covers/Type /Sig,/Type /DocTimeStamp, and a/Type-less dictionary carrying a
/ByteRangearray with a string/Contents, since Table 252 makes/Typeoptional. qpdf agrees
on the shape that matters most: it leaves a/Type /Sigdictionary's/Contentsbyte-identical
while encrypting that same dictionary's/Reason,/Locationand/M, and does so whether or not
the dictionary is reachable from a signature field. Its rule keys on/Type /Sigalone, so it does
encrypt/Contentson the other two shapes exempted here — meaning a document qpdf encrypted after
signing can still hand back an archive timestamp's ciphertext. This exemption is the reading that
cannot corrupt a signature, not a claim about what every producer does.At
/R5 and 6 the permissions come from/Perms(ISO 32000-2 Algorithm 13), the copy sealed
under the file key, rather than the dictionary's/P, which nothing protects at those revisions.
Only where the document carries a/Permsthat recovers, though: Table 21 does not require the
entry, so deleting it — or corrupting one byte of it, which fails Algorithm 13's marker check —
falls back to/Pand reports whatever an editor wrote there. qpdf, poppler and pdfium all behave
the same way, and refusing the file over an optional entry would make this the only reader that
cannot open it.PdfEncryptionInfo.Permissionsdocuments the distinction.Verified against the committed corpus (#99): for the eleven rows built from the baseline with the
u/opassword pair, the page content decrypts to the baseline's bytes,/Info /Titleto its
exact expected text, and each opens under both passwords. The other rows take their own passwords
or are not the baseline's object graph, and their own tests say what each pins. Nine fixtures were
added for this work, covering an empty user password, an object stream with a cross-reference
stream, nested strings, a 40-character password, one password serving as both roles, a non-ASCII
password whose/Uis PDFDocEncoding-derived, an incremental update over an encrypted document,
a linearized document, and one combining linearization, object streams and cleartext metadata.
(#97) -
A committed corpus of PDFs not produced by VellumPdf's own writer. Test-only; nothing ships.
Every reader fixture before this one came from VellumPdf's writer, which only ever emits
generation 0 and never a hybrid-reference file or another producer's object-stream layout — the
#121 review found three defects that shared exactly that root cause. Sourced from qpdf and
poppler where a tool can produce the shape, hand-built where none can: qpdf recomputes/Length
on every write, so it cannot produce a/Length-mismatched file, and separately its own
documentation states "We do not support creation of hybrid files." Covers object streams,
cross-reference streams, linearization, a poppler-produced incremental update, a
nonzero-generation catalog surviving both a read and a poppler-appended revision, a freed object
number reused at a bumped generation, and three damaged-file shapes (a truncated tail, an
out-of-rangestartxref, and a/Lengththat disagrees with the real stream body). One fixture
pins ISO 32000-2 §7.5.8.4's "hidden object" convention; qpdf is the independent oracle for the
hidden object itself. A second, related fixture puts the same free-then-redefine shape in a
single revision, a shape §7.5.8...
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,...
v1.11.0
What's Changed
- barcodes: complete the #155 encoder backlog (QR Kanji, Structured Append, Compact & Macro PDF417, Code 128 FNC4) by @Tim81 in #163
Full Changelog: v1.10.0...v1.11.0
v1.10.0
What's Changed
- docs: per-package NuGet READMEs and packaging wiring by @Tim81 in #156
- feat(barcodes): Reed-Solomon + GS1 groundwork, and GS1-128 HRI fix (#155) by @Tim81 in #157
- feat(barcodes): Code 39 (#154) and UPC-E (#155) by @Tim81 in #158
- feat(barcodes): GS1-mode QR (#152) by @Tim81 in #159
- feat(barcodes): Data Matrix / GS1 Data Matrix (#151) by @Tim81 in #160
- feat(barcodes): Aztec Code (#153) by @Tim81 in #161
- release(barcodes): graduate VellumPdf.Barcodes to Stable; v1.10.0 by @Tim81 in #162
Full Changelog: v1.9.0...v1.10.0
v1.9.0
What's Changed
Full Changelog: v1.8.2...v1.9.0
v1.8.2
What's Changed
Full Changelog: v1.8.1...v1.8.2
v1.8.1
What's Changed
Full Changelog: v1.8.0...v1.8.1