Recursive AI

Signature verification

All onepdf docs

OnePDF reads the signatures in a PDF as data and checks them. It never runs anything in the file and never touches a network: no certificate downloads, no OCSP or CRL fetches, no time-stamp requests. Verification is on demand; opening a document does no signature work. Signing is described below.

using var doc = PdfDocument.Open("contract.pdf");
var options = new PdfSignatureVerifyOptions { TrustAnchors = [rootCertificate] };
PdfSignatureReport report = doc.VerifySignatures(options);
foreach (PdfSignatureVerification v in report.Signatures)
{
    Console.WriteLine($"{v.Signature.FieldName}: valid={v.IsValid} integrity={v.Integrity} signature={v.SignatureCheck} chain={v.Chain} coverage={v.Coverage}");
}

doc.GetSignatures() lists PdfSignature objects without verifying (name, reason, location, contact, /M, /SubFilter, /ByteRange, Contents, CertificationLevel, FieldLock, CoveredRevision). PdfFormField.GetSignature() gives one field's.

What is checked

Each check has its own result; IsValid is true only when the byte range is valid, the digest matches, the signature verifies, the chain is Trusted at the validation time, any time-stamp is valid, revocation is not Revoked, and no disallowed change follows. A weak algorithm and unchecked revocation are warnings, not failures.

ResultMeaning
ByteRangeValid, or why not (Missing, Malformed, DoesNotStartAtZero, OverlapOrGap, OutsideFile, ContentsMismatch, DoesNotCoverRevision)
Integritythe digest of the signed bytes equals the digest the signature commits to (messageDigest, or the signed content for the legacy forms)
SignatureCheckthe signature over the signed attributes verifies with the signer certificate's key
Chain, ChainElementsthe chain from the signer certificate at ValidationTime: Trusted, UntrustedRoot, Expired, NotYetValid, Incomplete, Revoked, Invalid
Timestampthe RFC 3161 token: imprint, the authority's signature, its chain at the token's time, the time itself
RevocationNotChecked unless RevocationCheck was supplied
Coverage, Changessee below
Warningseverything that went wrong or could not be checked, in words

A check that cannot run (an unsupported algorithm, no signer certificate) is NotChecked with a warning, never a crash.

Byte range (ISO 32000-2 12.8.1)

The range must be four non-negative integers [0 b c d]; the bytes from b to c must be exactly the hex string <...> that is the value of /Contents (a range that leaves out only the digits between the brackets is accepted with a warning); c + d must be where a revision ends (an update's %%EOF), possibly followed only by white space, or the end of the file. All offsets are checked as long against the file length before anything is read. Linearized files chain their first-page cross-reference section after the main one although it lies earlier in the file; it counts as the same revision, not as an update. CoveredRevision is the index in PdfDocument.Revisions.

Algorithms

KindSupported
Sub-filtersadbe.pkcs7.detached, ETSI.CAdES.detached, ETSI.RFC3161; adbe.pkcs7.sha1 (weak); adbe.x509.rsa_sha1 is reported unsupported and weak
DigestsSHA-1 (weak), SHA-256, SHA-384, SHA-512. SHA-224 is reported unsupported
SignaturesRSA PKCS#1 v1.5 (rsaEncryption or shaNWithRSA), RSASSA-PSS (hash and MGF1 hash equal, salt as long as the hash is what the platform checks), ECDSA (DER, any curve the platform has)
CAdESthe signing-certificate attribute (v1 and v2) must name the signing certificate

The signature algorithm must agree with the digest algorithm of the signer info; if they differ the signature is Invalid. RSA keys below 2048 bits are flagged weak. The CMS reader accepts BER (including indefinite lengths) and checks every length before slicing; it is bounded (nesting 32, 16 signers, 256 certificates, 64 attributes per set, 1 MiB of embedded content).

Time and chains

The validation time is, in order: PdfSignatureVerifyOptions.ValidationTime if set; the time of a verified time-stamp token (TimePolicy.Auto); the signingTime attribute (or /M), flagged as the signer's unverified claim; otherwise now. TimePolicy.Now forces now. The chain is built with the platform's X509Chain with downloads and revocation checking off, using the certificates in the signature, the ExtraCertificates and the trust anchors. Without anchors (and without UseSystemTrustStore) no chain is trusted.

Revocation: pass RevocationCheck, a function that answers for a certificate and its issuer from CRLs or OCSP responses you already hold. Without it the result says revocation was not checked. Revocation data embedded in a signature is not used.

Changes after signing

The file is compared as revisions. For a signature covering revision r, every object written by later revisions is compared with its state in r (structural equality, so rewriting an object unchanged is not a change). The rules are advisory; a viewer may be stricter.

  • Level. The /P of the DocMDP transform of the certifying signature (/Perms /DocMDP in revision r): 1 no changes, 2 form fill and signing, 3 also annotations. A field lock's /P can lower it. With no certification the level is taken as 2: form fill and signing pass, everything else is flagged.
  • Always allowed: /DSS validation data; document time-stamps; cross-reference and object-stream objects; a changed document information dictionary or catalog /Metadata (reported as Other, allowed).
  • Level 2: a field's /V, /AP, /AS, /I; adding fields to /AcroForm and widgets to a page's /Annots; adding a signature.
  • Level 3: also adding, removing and changing other annotations.
  • Field locks: a /Lock dictionary on a signature field, or a FieldMDP transform in the signature's /Reference, of this or an earlier signature: All, Include or Exclude with field names. A change to a locked field is not allowed whatever the level.
  • Everything else is not allowed: page content and resources, the page tree, other catalog entries, deleted objects.

New objects take the category of the changed object that refers to them. Each PdfSignatureChange has the object number, a PdfChangeCategory (FormFieldValue, NewSignature, Annotation, PageContent, ValidationData, Other), whether it is allowed and a reason. Coverage is WholeDocument (nothing follows), EarlierRevisionAllowedChanges, EarlierRevisionDisallowedChanges, or Unknown (the byte range is not valid). Set AnalyzeModifications = false to skip the comparison.

Known attacks and what the result says

The published attacks on PDF signature validation (universal signature forgery, incremental saving, signature wrapping, shadow and certification attacks) are each reproduced by a crafted file in tests/OnePdf.Tests/Signatures/AttackTests.cs. Detected means IsValid is false. Advisory means the permission level of the document lets the change through, so IsValid stays true and the change is listed in Changes (with its category and Allowed = true); callers who show a document as "signed and unchanged" should show those entries.

ClassVariants testedOutcome
Universal signature forgery/ByteRange or /Contents absent, null, empty, all zeros, wrong type, [0 0 0 0], negative, real numbers, wrong count, shifted gap, indirect object or indirect numberDetected. An indirect /ByteRange or /Contents is refused (Malformed) even when it is correct, because the specification requires direct values
Incremental savingrewritten content, page, page tree, catalog, font; objects appended after %%EOF with no table, with a trailer only, or with a bad startxref; a trailer that points at a new catalog; an update stretched into the signed range; a rewritten signature dictionary or catalog to lower the certificationDetected. Data after the last %%EOF is reported as an unsigned change; a later /Root that differs from the signed one is a disallowed change
Signature wrappingsecond range starting early or inside the first, extra objects or a new table hidden in the excluded area, range moved past them, wrong lengths, more than one gapDetected: the gap must be exactly the /Contents string and the range must end at a revision boundary
Shadow: hide and replacepage content, page objects, font replaced by an update; xref switching to a second definition prepared before signingDetected
Shadow: form valuesa field's value or appearance replaced, or pointed at a prepared appearance stream, under P=2 or 3Advisory: allowed by the level and listed as FormFieldValue. Detected under P=1
Evil annotationannotation added under P=1 or 2Detected. Under P=3 advisory: listed as Annotation, allowed
Evil annotation as a form widgeta widget with an appearance added under P=2Advisory: a new form field is a permitted change at level 2, listed as FormFieldValue. Detected under P=1
Sneaky signaturean unsigned signature field with an appearance added under P=2; a signature added under P=1The field is advisory at P=2 and detected at P=1; the added signature is detected at P=1
Certificateuntrusted root, expired signer at the validation time, key usage without digitalSignature/nonRepudiation, extended key usage for TLS or time-stamping only, a time-stamp signed by a certificate without the time-stamping purposeDetected (Chain, KeyUsage, Timestamp)

RequireSigningKeyUsage (default true) makes a certificate that may not sign documents fail IsValid; with it off the problem is still in KeyUsage and the warnings. A key usage extension needs digitalSignature or nonRepudiation; an extended key usage, when present, must include document signing, e-mail protection, Adobe PDF signing or any purpose.

Two things the result does not do: it reads signatures from the newest catalog only, so a later /Root whose catalog has no /AcroForm shows no signatures (the report says so and is never "all valid"); and a field whose /FT is not /Sig is not treated as a signature.

Limits

A signature value larger than the file cannot occur; the excluded area is capped by PdfLimits.MaxMetadataBytes; object comparison is capped at 2^18 nodes per object and 2^20 changed objects. Cancellation is checked while the byte ranges are hashed and between signatures.

Signing

Signing is always an incremental update (ISO 32000-2 7.5.6), so earlier signatures stay valid and the original bytes are an exact prefix of the output. The document must be opened with PdfOpenOptions.AllowEditing; changes made through the mutable view before the call (a filled field, say) are part of the signed update.

using var doc = PdfDocument.Open("contract.pdf", new PdfOpenOptions { AllowEditing = true });
var credential = PdfSigningCredential.FromCertificate(signerWithKey, [intermediate]);
doc.Sign("contract-signed.pdf", credential, new PdfSigningOptions
{
    FieldName = "Approval",                           // an unsigned signature field, or a new name
    Rectangle = new PdfRectangle(72, 72, 272, 132),   // omit for an invisible signature
    Reason = "I approve this contract",
    Timestamp = request => MyTsaClient.Post(request.RequestDer.ToArray()),   // makes it PAdES B-T
});

Sign takes a Stream or a path and writes nothing unless the whole signature succeeded. It holds the output in memory (about the file size). After a call, successful or failed, the document holds the pending signature objects: reopen the output (or the file) to sign again; a second call on the same instance throws PdfSigningException (DocumentUsed). To sign more than once, chain the outputs: sign, open the result with AllowEditing, fill a field if you like, sign again, add a document time-stamp.

Profiles

PdfSigningOptions.FormatSub-filterSigned attributesNotes
PadesBaseline (default)ETSI.CAdES.detachedcontentType (id-data), messageDigest, signingCertificateV2No signingTime (ETSI EN 319 142-1 6.3: the time is /M). The signer certificate and the chain you gave are embedded. With Timestamp: B-T, the token is the signatureTimeStampToken unsigned attribute.
Pkcs7Detachedadbe.pkcs7.detachedcontentType, messageDigest, signingTimeFor older consumers.

Both use issuerAndSerialNumber, an empty detached encapContentInfo, digest identifiers with absent parameters (RFC 5754), and sha*WithRSAEncryption, RSASSA-PSS (hash = MGF1 hash, salt = hash length) or ecdsa-with-SHA*. A pre-2.0 file that has no signatures yet also gets /Extensions /ESIC (level 2 for CAdES, 1 for a document time-stamp; EN 319 142-1 5.6); a file that is already signed is not touched there, since a catalog change would count against the earlier signature.

Keys and external signers

PdfSigningCredential.FromCertificate(cert, chain) needs an RSA or ECDSA private key the process can use (NoPrivateKey otherwise). FromExternalSigner(cert, signer, chain) calls signer(PdfSignRequest) with the digest of the DER signed attributes, the hash algorithm, the key type and the RSA padding; it returns the signature value (RSA: the raw signature; ECDSA: DER or IEEE P1363). OnePDF checks the value with the certificate's public key and refuses a wrong one (SignerFailed). OnePDF does not build chains: chain is what gets embedded after the signer certificate.

Time-stamps

PdfTimestampCallback gets a PdfTimestampRequest (hash algorithm, message imprint, nonce, and the DER TimeStampReq with certReq set) and returns the TimeStampResp or the bare token. OnePDF checks the status (granted or granted with modifications), that the token is SignedData holding a TSTInfo, that its imprint equals the one sent and that it echoes the nonce; otherwise TimestampRejected. It does not verify the authority's signature or trust at signing time; VerifySignatures does. AddDocumentTimestamp writes a /DocTimeStamp field (Timestamp1, ...) whose token covers the whole file before it.

Certification and locks

PdfSigningOptions.Certify adds the DocMDP transform (/Reference) and /Perms /DocMDP; only the first signature of a document may certify (CertificationNotFirst). FieldLock writes a /Lock dictionary on the signature field.

Visible signatures

A rectangle (or an existing field with one) gives a visible signature. Its normal appearance is a Form XObject built with the content builder: an optional frame, an optional image on the left (a PdfCreatedImage of the same document, at most 40 percent of the width) and text lines (default: signer name, date, reason, location), in Helvetica sized to fit up to 10 points. An existing field that already has an appearance keeps it unless Appearance is given.

Space for /Contents

The placeholder is ContentsReserve bytes (default: an estimate from the certificates, key size and, for a time-stamp, 8 KiB; 12 KiB for a document time-stamp), written as twice as many hex digits and patched in place after the byte range is known. If the CMS does not fit, ContentsTooSmall carries RequiredBytes; PdfSigningResult.ContentsUsed tells what an earlier signature of the same kind took. The /Contents string of a signature dictionary is never encrypted (ISO 32000-2 7.6.2), and the reader does not decrypt it: the string of a dictionary with /Type /Sig or /DocTimeStamp, or one holding /ByteRange, is returned as stored. The rest of the update is encrypted like the document.

Not done in signing

The time-stamp authority's own signature is not checked when signing. A time-stamp token is embedded as the callback returned it (a BER token is not re-encoded to DER). Long-term validation data (/DSS, PAdES B-LT and B-LTA), several signers in one CMS, signature policies, commitment types and /Prop_Build are not written. Ed25519 and Ed448 keys are not supported (the platform has no such primitive).

Not done in verification

The public-key security handler, long-term validation data (/DSS) beyond noting it, CRL/OCSP parsing, and Ed25519/Ed448 (the platform has no such primitive) are not part of verification.