Skip to content

Releases: Tim81/VellumPDF

v2.3.0

Choose a tag to compare

@github-actions github-actions released this 01 Sep 04:05
15e191e

Breaking changes

  • A same-revision /XRefStm no 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
    /XRefStm defines it, PdfReader now resolves N to null, 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 /XRefStm forward 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 /Prev revision, is unaffected only in the two-revision case; a hybrid revision
    sitting between two others in a /Prev chain 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.Reader is 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 /DecodeParms half 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 /Pages node, 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 /Count and one walking /Kids now disagree. PreflightContext.WalkPages
      walks /Kids the 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 /Encrypt reference points at, the document
      now fails to open with InvalidDataException instead of decrypting, because /Encrypt can 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
      throws InvalidDataException: Malformed PDF: /Root does not resolve to a dictionary. where it
      previously opened.
    • A /Filter or /DecodeParms object resolving to null does not degrade to null output —
      it produces wrong bytes. PdfFilters.GetFilterList treats an unresolvable /Filter as no
      filter at all, so GetDecodedStreamData returns the raw, still-encoded body. Measured: a
      24-byte plaintext body, FlateDecode-compressed to 32 bytes (zlib header 78 9C), comes back as
      those 32 raw bytes instead of the 24-byte plaintext once its /Filter reference 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 /ObjStm container 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 and PdfDocumentReader throws
      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
      to null (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 /ObjStm container 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.EnumerateIndirectObjects and EnumerateStreams included. Measured: an
      ISO19005-2:6.1.13-name Error 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 /AcroForm this way makes reader.Signatures.Count go 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 ObjectNumbers shrinks, and — when
      /Size also understates the count — NextFreeObjectNumber can shrink with it, both feeding
      code outside this package (PreflightContext and ObjectLayoutRule; DssBuilder and
      ArchiveTimestampBuilder). The fixture measuring this shrink has its own /Size already
      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. The NextFreeObjectNumber
      shrink has no constructed consequence beyond itself; the ObjectNumbers shrink 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) is IsCompliant=False with rule
      ISO19005-2:6.1.7.1-external-stream when that object is live, and IsCompliant=True with 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 … 2b against ISO 19005-2:2011 clause 6.1.7.1 test 3 for the live
      one, PASS … 2b for the freed one. Two controls rule out the deflationary readings: a variant
      where the object is defined only by the /XRefStm and 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 type-2 (compressed) cross-reference entry whose container has no live entry anywhere in the
    merged table now resolves to null instead 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 in freed regardless, 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 to null rather than
    throwing InvalidDataException: Object stream container N not found in xref., matching qpdf. A
    member the sweep drops this way is absent from _xref itself — the table a future full
    re-serialisation (tracked in #186) would walk to decide what to emit — so it is not carried ...

Read more

v2.2.0

Choose a tag to compare

@github-actions github-actions released this 28 Aug 14:25
27b9344

Breaking changes

  • PdfDictionary.Set, TryGet and Get now throw ArgumentNullException for a null key.
    Previously TryGet(null, out _) returned false, Get(null) returned null, and
    Set(null, value) appended an entry that only failed later — with a NullReferenceException out
    of WriteTo, 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
    null key would behave differently depending on which side of the internal indexing threshold a
    dictionary sits — returning false below 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 real UserPassword produced 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 /O and /OE (ISO 32000-2 §7.6.4.4.6,
    Algorithm 9) under the empty password instead of falling back to the user password. At /R 6 an
    empty password fails the /U check and satisfies the /O check, 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 /U and /O in. Verified concretely: a file written by v2.1.0
    with UserPassword = "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 enforced Permissions, but nothing enforced
    UserPassword either. OwnerPassword = "" beside a non-empty UserPassword now 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 = null is unchanged
    too: that is the documented fallback to the user password as owner, and it does not reproduce
    this defect (/O is sealed under the real user password, not the empty string) — though anyone
    who can open the document still holds owner access under it, so Permissions still binds nobody.
    The guard sits in StandardSecurityHandler's constructor, which is Stable API and callable
    directly, and in PdfDocument.Encrypt, so the failure surfaces at the call site rather than at
    Save(); VellumPdf.Layout.Document.Encrypt inherits 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 /O and /OE, which needs a real owner password behind them from the start. Affected
    documents must be re-encrypted from the original plaintext, with a distinct OwnerPassword this
    time. (#211)

  • PdfDictionary lookup is no longer quadratic in the key count. Set and TryGet now build a
    hash index once a dictionary passes 16 entries, rather than scanning the whole entry list on every
    call. The /Encrypt dictionary is parsed, and copied again by EncryptionSetup.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 /Encrypt dictionary took about 27 seconds before this fix and
    well under a second after. EncryptionSetup's /CF cap (MaxCryptFilters, still 64) keeps its
    comment but loses the reason it used to give: every term that touches /Encrypt is 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

Choose a tag to compare

@github-actions github-actions released this 28 Aug 07:22
ae5112d

Breaking changes

  • A password-protected document now reaches PdfPreflight as PdfPasswordException, which no
    existing catch covers.
    Every prior version threw UnsupportedPdfFeatureException, and so
    NotSupportedException, for any /Encrypt at all, so that is what a caller of
    PdfPreflight.Validate or PdfPreflight.DetectClaimedProfiles wrote 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 throw PdfPasswordException for one that needs a non-empty password. That
    exception derives from Exception directly, 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
    PdfPasswordException beside it. (#97)

  • PdfDocument.DocumentId now throws ArgumentException for a value that is not 16 bytes.
    Previously any other length was accepted and then written as no /ID at all — silently. ISO
    32000-2 Table 15 requires /ID once /Encrypt is 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. DocumentId is 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+/R combination.
    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, /R and /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+/R check would notice either. Groundwork for
    the decrypt side. (#99)

  • Hand-written RC4 and MD5 primitives for the legacy (/V 1–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 its MD5 type 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 /ID generation (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. /ID itself 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+/R combination the
    committed corpus (#99) exercises: /V 1//R 2 (RC4-40) through /V 5//R 6 (AES-256).

    Internal only: no public surface, no PdfReader wiring, no /Encrypt gate; 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 /V 5, 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 false fixtures 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.Open takes a password and reads encrypted PDFs. The Standard
    security handler at /V 1, 2, 4 and 5 and /R 2 through 6 — RC4-40 through RC4-128, AES-128
    (/AESV2) and AES-256 (/AESV3) — plus the /Crypt filter (ISO 32000-2 §7.4.10) and crypt
    filters naming different methods for strings and streams. Strings are decrypted in Resolve under
    the identity of the indirect object containing them (ISO 32000-1 §7.6.2, Algorithm 1) and stream
    bodies on the decode path; ParsedStream.RawBody still holds the verbatim file bytes, which
    StreamRule and HexStringRule need 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.Encryption reports /V, /R, the stream and string
    ciphers, key length, permissions, /EncryptMetadata and which password authenticated — no key
    material, and no /O, /U, /OE or /UE.

    What is left in the clear, per the spec: the trailer /ID, the /Encrypt dictionary'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 /ByteRange verification. That last
    exemption covers /Type /Sig, /Type /DocTimeStamp, and a /Type-less dictionary carrying a
    /ByteRange array with a string /Contents, since Table 252 makes /Type optional. qpdf agrees
    on the shape that matters most: it leaves a /Type /Sig dictionary's /Contents byte-identical
    while encrypting that same dictionary's /Reason, /Location and /M, and does so whether or not
    the dictionary is reachable from a signature field. Its rule keys on /Type /Sig alone, so it does
    encrypt /Contents on 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 /R 5 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 /Perms that 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 /P and 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.Permissions documents the distinction.

    Verified against the committed corpus (#99): for the eleven rows built from the baseline with the
    u/o password pair, the page content decrypts to the baseline's bytes, /Info /Title to 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 /U is 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-range startxref, and a /Length that 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...

Read more

v2.0.0

Choose a tag to compare

@github-actions github-actions released this 17 Aug 20:41
1c282f3

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. AssemblyVersion is pinned to 2.0.0.0 for 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-time signed attribute. ETSI
    EN 319 142-1 admits only the signed attributes its table 1 lists, and signing-time is 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 reading signing-time out of SignerInfo on a
    signature written with the default /SubFilter ETSI.CAdES.detached will no longer find it;
    /M still carries the value, and adbe.pkcs7.detached keeps the attribute, since it makes
    no ETSI claim. (#170)
  • A tagged document with no tagged content now emits /StructTreeRoot. Setting
    Tagged = true and drawing nothing previously produced no structure tree at all, which
    failed PDF/A-2a and PDF/UA-1 validation. Tagged now 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 opaque ArgumentException raised from inside the BCL's CMS encoder. The
    exception type is unchanged, so a catch (ArgumentException) behaves as before. (#167)
  • vellum-preflight --format json reports check accounting differently. summary.total
    used to be failed + 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 matching failedChecks and inconclusive arrays are new, and the text output gains
    an INCONCLUSIVE line. Exit codes and the conformance verdict are unchanged.
  • A null options argument now means "use the default" rather than throwing, on
    CcittImageLoader.Load and the HttpRevocationClient constructor. With = null defaults
    there is no way to tell an omitted argument from an explicitly null one, so the old
    ArgumentNullException fired on exactly the call the default exists to serve. (#177)
  • External-signer CMS digest AlgorithmIdentifiers now match RFC 5754 — both
    SignedData.digestAlgorithms and SignerInfo.digestAlgorithm omit their parameters field
    instead of carrying a redundant DER NULL, per RFC 5754 §2 ("implementations MUST generate
    SHA2 AlgorithmIdentifiers with absent parameters"). SignerInfo.signatureAlgorithm was
    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, and LoadTrueTypeFontPdfDocument.SaveAsync,
    Document.SaveAsync(Stream) / SaveAsync(string), Document.LoadTrueTypeFontAsync, and
    SigningExtensions.SignAsync (both overloads), each taking a CancellationToken. Existing
    sync methods are unchanged. ITimestampClient and IRevocationClient gain default-implemented
    GetTimestampTokenAsync/GetRevocationDataAsync members, 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, since CmsSigner only 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 resulting SignerInfo by hand. Set PdfSignatureSettings.ExternalSigner
    and sign with SignAsync; the synchronous Sign overloads throw, since there is no synchronous
    way to bridge a network call. EcdsaSignatureConverter is 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
    from Certificate, for HSM/PKCS#11/cloud-KMS-backed certificates whose key isn't attached to
    the X509Certificate2 (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-v2 signed 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 DER DEFAULT rule) and written with absent parameters for
    SHA-384/512; issuerSerial is built from the same bytes as the SignerInfo, so the two
    cannot disagree. (#168)

    Together with the signing-time removal below, this is what makes the /SubFilter claim
    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.detached PDF-NOT-ETSI PAdES-BASELINE-B
    B-T, with an RFC 3161 timestamp PAdES-BES PAdES-BASELINE-T
    adbe.pkcs7.detached PKCS7-B PKCS7-B (unchanged)

    Either change alone reaches only PAdES-BES. (#168, #170)

  • 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 as PdfAnnotationFlags.Print rather than 4.
    Emitted bytes are unchanged, and a test pins that. (#176)

  • SubFilterEtsiCAdESDetached and SubFilterAdbePkcs7Detached constants on both
    PdfSignatureSettings and SignaturePlaceholderOptions, so the two accepted values need not
    be hardcoded. (#176)

Changed

  • VellumPdf.Conformance graduates from Preview to Stable. VellumPdf.Cli was 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's PublicAPI.Shipped.txt was 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.txt is reset to its header, so a 2.x addition shows as a diff against
    an accurate baseline. VellumPdf.Reader is 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.
    HttpTimestampClient and HttpRevocationClient already issued the request through
    HttpClient.Send; only the response body was read by blocking on ReadAsByteArrayAsync,
    which deadlocks on a synchronization context and starves the thread pool under load. That is
    HttpContent.ReadAsStream now, which is genuinely synchronous. No GetAwaiter().GetResult()
    remains anywhere in src/. 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.Pkcs moves to 10.0.11, matching the .NET 10 servicing band.

Fixed

  • /ByteRange offsets are no longer truncated to int. PdfSignature.ByteRange was
    int[] filled from a long through an unchecked cast,...
Read more

v1.11.0

Choose a tag to compare

@github-actions github-actions released this 11 Jul 21:05
6960ee3

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

Choose a tag to compare

@github-actions github-actions released this 07 Jul 10:13
b1b09ab

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

Choose a tag to compare

@github-actions github-actions released this 06 Jul 09:52
fe86386

What's Changed

  • feat: VellumPdf.Barcodes — QR, Micro QR, PDF417, Code 128, EAN/UPC, ITF-14 (#51) by @Tim81 in #150

Full Changelog: v1.8.2...v1.9.0

v1.8.2

Choose a tag to compare

@github-actions github-actions released this 05 Jul 12:16
af82789

What's Changed

  • fix: Standard-14 font dicts as shared indirect objects; workflow action bumps by @Tim81 in #149

Full Changelog: v1.8.1...v1.8.2

v1.8.1

Choose a tag to compare

@github-actions github-actions released this 03 Jul 19:05
7426236

What's Changed

Full Changelog: v1.8.0...v1.8.1

v1.8.0

Choose a tag to compare

@github-actions github-actions released this 03 Jul 14:30
bc821a7

What's Changed

  • docs: reflect shipped vellum-preflight CLI in README by @Tim81 in #144
  • v1.8: Linearization (fast web view) — opt-in, qpdf-clean (#52) by @Tim81 in #147

Full Changelog: v1.7.8...v1.8.0