Skip to content

Standards and instruments

Everything this package does is somebody else's specification, implemented here. This page names each one, says where it is implemented and why it is needed, and then does the same for the tools that check the output.

It exists because "we follow PAdES" is a claim rather than information. What is useful is which clause, in which file, and what breaks without it.

About the links

Every link here was checked. The ISO ones go through the catalogue's search rather than to a document number, because iso.org refuses automated requests: a number nobody can verify is a link that eventually points at the wrong standard. Where a free copy exists it is linked directly, and for ISO 32000-1 one does: Adobe hosts the specification in full.

The document format

ISO 32000-1:2008, the PDF specification, and the edition this package is written against. Adobe hosts it in full, free.

ClauseImplemented inWhy
§7.5.6, incremental updatessrc/Signing/Incremental/RevisionWriter.php, src/Signing/Incremental/DocumentReader.phpThe whole design. Signing appends a revision instead of rebuilding, which is what keeps earlier signatures, annotations and form fields intact
§7.5.4, cross-reference tablesrc/Signing/Incremental/XrefSubsections.phpThe classic index, read and written, including its subsection form
§7.5.5, the trailersrc/Signing/Incremental/DocumentInfo.php, src/Validation/RevisionAnalyzer.php/Prev is how a revision chains to the one before it, and how a validator walks back through them
§7.5.7, object streamssrc/Signing/Incremental/ObjectStreamReader.phpWord and Chrome pack the catalog into one, and signing rewrites the catalog, so it has to be readable before anything can be signed
§7.5.8, cross-reference streamssrc/Signing/Incremental/XrefStreamReader.php, src/Signing/Incremental/XrefStreamWriter.phpThe compressed index. A revision follows whichever form the document already uses, because mixing them produces a file readers do not see as signed
§7.4, stream filterssrc/Support/PdfFilters.php, src/Enums/StreamFilter.phpFlate, LZW, ASCIIHex, ASCII85 and RunLength, needed to read what a document already encoded
§7.6, encryptionsrc/Signing/Encryption/StandardSecurityHandler.php, src/Signing/Encryption/EncryptionDictionary.phpSigning a password-protected document and re-encrypting it under its own key
§7.12, extensions dictionarysrc/Signing/Incremental/RevisionWriter.phpDeclaring the extension level a signed revision relies on
§12.5.5, annotation appearance streamssrc/Signing/Incremental/SealAppearance.php, src/Signing/Incremental/PageGeometry.phpA visible seal is an appearance stream on a widget annotation
§12.7.4.5, signature fields and lockssrc/Data/FieldLock.php, src/Enums/FieldLockAction.php/Lock with /All, /Include and /Exclude, written and, more importantly, honoured
§12.8.1, signature dictionariessrc/Signing/Incremental/ByteRangeCalculator.php, src/Validation/PdfSignatureExtractor.php/ByteRange and /Contents: what the signature covers, and where the CMS goes
§12.8.2.2, DocMDPsrc/Signing/IncrementalSigner.php, src/Signing/Incremental/CertificationReader.phpCertification: the author's statement about what may happen to the document afterwards
§14.4, file identifierssrc/Signing/Incremental/DocumentInfo.php/ID has to survive into the new revision, and readers use it to relate revisions

ISO 32000-2, PDF 2.0, is consulted for one thing only: src/Signing/Encryption/StandardSecurityHandler.php, where the AES-256 revision 6 handler is specified rather than in the 1.7 edition.

The signature

StandardImplemented inWhy
RFC 5652, CMSsrc/Validation/Pkcs7Reader.php, src/Validation/NativeSignatureVerifier.php, src/Validation/TimestampTokenReader.phpThe container the signature lives in. Reading it is how validation verifies rather than assumes, and §5.4 is why the signed attributes are re-tagged before the signature over them is checked
RFC 5035, ESS signing-certificate-v2src/Signing/Cades/CadesBuilder.php, src/Validation/NativeSignatureVerifier.phpPAdES requires this attribute, and openssl_pkcs7_sign() cannot emit it. That single sentence is why the CMS is built with tc-lib-pdf-sign instead. On the reading side it is what stops a substituted certificate
RFC 2985, PKCS#9src/Validation/PdfSignatureExtractor.phpThe signed attributes, including the message digest a signature commits to
RFC 5126, CAdESsrc/Enums/CmsAttribute.phpThe attribute set the PAdES levels are defined on top of
ETSI EN 319 142, PAdESsrc/Enums/SignatureProfile.php, src/Signing/ArchiveExtender.phpWhat B-B, B-T, B-LT and B-LTA each require, and what an archive timestamp has to cover

Time

StandardImplemented inWhy
RFC 3161, timestamp protocolsrc/Signing/Cades/HttpTransport.php, src/Validation/TimestampTokenReader.php, src/Testing/LocalTimestampAuthority.phpEverything above pades-b-b. It is the only reason a time in a document is attributable to anyone other than the signer

The local authority is in that list on purpose: the protocol is implemented on both sides, so the suite can exercise B-T and above without reaching a live service.

Certificates and revocation

StandardImplemented inWhy
RFC 7292, PKCS#12src/Certificates/NativeCertificateReader.php, src/Certificates/OpenSslCliCertificateReader.phpThe .pfx and .p12 bundle an A1 certificate arrives in
RFC 7468, PEMsrc/Support/Pem.phpThe other form it arrives in, and the form openssl takes on the command line
RFC 5280, X.509 and CRLsrc/Certificates/SubjectAlternativeNameReader.php, src/Validation/RevocationChecker.phpReading subjectAlternativeName, and evaluating a CRL the document carries
RFC 6960, OCSPsrc/Validation/RevocationChecker.phpEvaluating an OCSP response the document carries, and verifying it against its issuer before believing it

Images and streams

StandardImplemented inWhy
RFC 2083, PNGsrc/Support/PngReader.php, src/Support/PdfFilters.phpThe PNG predictors a cross-reference stream may use, and reading seal artwork

Conformance

ISO 19005 (PDF/A) and ISO 14289-1 (PDF/UA) are consulted in src/Signing/Incremental/RevisionWriter.php, src/Signing/Incremental/DocTimeStampWriter.php, src/Signing/Incremental/StructureTreeWriter.php and src/Signing/Incremental/SealAppearance.php, because a signed document has to stay conformant and the revision is what could break it: output intents, colour spaces and tagged structure all have rules a naive appended object violates. §7.18 is the one a visible seal used to fail, and the widget joins the structure tree to satisfy it (0113).

Neither is asserted. Both are measured, with veraPDF, below.

Brazil

DOC-ICP-04 and the ICP-Brasil certificate policy define the otherName OIDs a Brazilian certificate carries, implemented in src/IcpBrasil/Enums/OtherName.php and read by src/Certificates/SubjectAlternativeNameReader.php. PHP renders every one of those fields as othername:<unsupported>, which is why they are parsed here.

The instruments

Signed output is checked against tools written by other people, because a validator sharing its assumptions with the signer proves very little. Five are actually exercised.

ToolVersionDecidesExercised by
veraPDF1.30.2, pinnedPDF/A and PDF/UA conformancetests/Conformance/PdfAValidationTest.php, tests/Conformance/PdfUaValidationTest.php, tests/Timestamps/TimestampOfflineTest.php
poppler pdfsignot pinned, see belowwhether an independent reader sees the signaturestests/Certification/CertificationEnforcementTest.php
qpdfnot pinned, see belowstructural soundness, and reading back what was encryptedtests/Conformance/StructureTest.php, tests/Signing/EncryptedDocumentTest.php
pyHanko0.36.2, CLI 0.4.2, pinned/DocMDP enforcement, and signing the foreign document this package's validator is read againsttests/Validation/ForeignSignatureTest.php, tests/Certification/CertificationEnforcementTest.php
Arlington PDF Model testgrammarpinned by commitwhether the emitted objects match the specification's own grammartests/Conformance/ArlingtonTest.php

Why three are pinned and two are not

A validator that changes its verdicts between builds cannot be the thing a gate is measured against. veraPDF and pyHanko are pinned to a version, and the Arlington model by commit, because the tool and the TSV grammar live in the same tree and one SHA pins both together. All three are fetched from upstream, where a version stays available.

qpdf and poppler come from the distribution, and a distribution pin is worse than none: the exact version disappears from the archive when the runner image advances, and CI goes red for a reason unrelated to the code. The two ends already differ, which is the honest state of it: .docker carries qpdf 12.x from Alpine and the runners ship 11.x.

So the shape is asserted instead of the number. tests/Pest.php refuses qpdf output it cannot read, rather than collecting no complaints from it and reporting a sound file: an empty complaint list is what a good document produces, so a parser that stops matching would turn the gate green in silence. Both versions are printed into the CI log, so a verdict that changes can be read against the version that changed it.

Three rules that keep them where they belong

  1. Nothing in src/ may invoke one. tests/Project/ArchTest.php fails on any mention, and the ban list deliberately includes tools nobody has reached for yet.
  2. Nothing built for testing may ship.tests/Project/DistributionTest.php asks git archive what a release contains.
  3. A missing tool turns the run red, not green. A test whose instrument is absent calls markTestSkipped(), and composer test carries --fail-on-skipped, so an absent validator cannot quietly stop checking.

The one that has earned it most

pdfsig has caught defects the suite passed straight through. The clearest was a revision that located the first /Contents in a multi-signature document rather than the last, overwriting an earlier signature: every test passed, and an independent reader is what noticed (0006).

That is the argument for keeping instruments that were not written here, stated as an incident rather than as a principle.

Released under the MIT Licence.