Skip to content
Validating PAdES Signatures

Validating PAdES Signatures

Validation runs off the reader: you hand ValidateSignatures a list of handlers, it finds every signature field in the document, matches each one to the first handler that claims it, and returns a result per field. For PAdES the handler is the Level B one, built without a key or certificate:

handlerPAdES, err := sighandler.NewEtsiPAdESLevelB(nil, nil, nil)
if err != nil {
    return err
}

Both the key and the certificate are nil, because validation reads the certificate out of the signature itself. The example puts this handler in one list with the other three handlers UniPDF provides, so the same program validates PAdES and non-PAdES files. Validating Digital Signature walks through that list and the loop that prints the results.

One handler for every PAdES level

The PAdES handler does not branch on its level when validating. It claims any signature whose filter is Adobe.PPKLite and whose subfilter is ETSI.CAdES.detached, then verifies the CMS structure the same way regardless of whether the file was signed at B-B, B-T or B-LT. So a single NewEtsiPAdESLevelB(nil, nil, nil) handler covers all three.

Do not reach for NewEtsiPAdESLevelLT to do this. That constructor dereferences its appender argument, so passing nil panics rather than returning an error. Use the Level B or Level T constructor when the handler is only going to validate.

Document timestamps are a separate case. They carry the subfilter ETSI.RFC3161, which the PAdES handler does not claim, so validating a B-LTA file needs a DocTimeStamp handler in the list as well:

handlerDocTimeStamp, err := sighandler.NewDocTimeStamp("", 0)
if err != nil {
    return err
}

handlers := []model.SignatureHandler{
    handlerX509RSASHA1,
    handlerPKCS7Detached,
    handlerPAdES,
    handlerDocTimeStamp,
}

An empty server URL and a zero hash are fine here; both are only used when signing. Each handler claims a different subfilter, so the order of the list does not change which one validates a PAdES signature. A signature with no matching handler still appears in the results, with handler not set in its Errors slice.

Reading the result

item.String() prints a readable summary, and the fields behind it are worth knowing individually.

FieldMeaning
IsSignedA signature dictionary was found and the handler validated it without an error. False when Validate fails, for example on a tampered B-T file.
IsVerifiedThe CMS signature verifies against the bytes covered by its byte range.
GeneralizedTimeThe time from the embedded timestamp token. Zero when the signature has none, which is how you tell B-B from B-T.
IsCrlFound, IsOcspFoundRevocation data was present in the signature’s Adobe revocation-info attribute.
DiffResultsSet only by the DocMDP handler. See Sign and configure DocMDP restriction.
FieldsThe form field the signature is attached to.

IsVerified is a statement about bytes, not about trust. It says the digest matches and the CMS structure checks out against the certificate embedded in the file. It says nothing about whether that certificate chains to a root you accept, and nothing about whether it has since been revoked.

ValidateSignatures never sets IsTrusted, so the summary reads Trusted: Untrusted certificate and no trust roots configured even for a signature from a well-known CA. Pass your trust roots to ValidateSignaturesWithOpts to get a trust verdict; with RevocationCheckOffline it also reads the revocation data a B-LT or B-LTA file carries in its DSS. See Validating Signer Trust and Revocation.

From ValidateSignatures, IsCrlFound and IsOcspFound report on the signed attribute inside the CMS, not on the document security store. A file whose validation data lives only in the DSS reports false for both, even though the data is there. With a revocation check through ValidateSignaturesWithOpts they report the evidence actually used for the signer, DSS included.

Limitations

ValidateSignatures returns nil, nil when the document has no AcroForm or no form fields. An empty result slice means there is nothing to validate, not that validation failed, which is why the example treats a zero-length result as its own error case.

Per-signature failures do not stop the walk. When a handler’s Validate returns an error, the message is appended to that result’s Errors and the remaining signatures are still processed. Check Errors as well as IsVerified.

Validation covers the byte range each signature declares. A signature that only covers part of the file still verifies as long as those bytes are intact. The coverage fields say how much it spans: on a B-LT or B-LTA file the PAdES signature reports IsCoveringEntireRevision with revisions appended after it, because the DSS and the document timestamp are written later. Whether those later revisions were acceptable is not part of the check.

Run the example

The example validates every signature in the file with the four handlers and prints each result. A B-LTA file gives two results: the PAdES signature, with GeneralizedTime from its signature timestamp, and the document timestamp. If any signature is not signed or does not verify, the example prints all results, then exits with status 1 and Fail: validation failed for signature(s) followed by the failing numbers.

git clone https://github.com/unidoc/unipdf-examples.git
cd unipdf-examples/signatures/validation
go run pdf_sign_validate.go <INPUT_PDF_PATH>

If this is your first time using UniPDF, follow the getting started guide to create an API key and set up your development environment.

View the full source

Sample output

Run against the output of the B-LTA example, signed with a certificate from a local test CA:

--- Signature 1
Name: PAdES B-LTA Signature PDF
Date: 2026-10-21 13:00:21 +0700 UTC+0700
Reason: TestPAdESPDF
Location not specified
Contact info not specified
Fields: 1
Signed: Document is signed
Signature validation: Is valid
Trusted: Untrusted certificate
    no trust roots configured
Certificate: CN=Audit Signer
Certificate issuer: CN=Audit Test CA
Certificate validity: 2026-10-21T05:59:20Z - 2027-08-17T05:59:20Z
Certificate chain: 2 certificate(s)
GeneralizedTime: 2026-10-21 06:00:24 +0000 UTC
Coverage: Entire revision is covered by the signature (2 revision(s) appended after signing)
ByteRange gap: matches /Contents
Revocation data: CRL found
Revocation data: OCSP not found

--- Signature 2
Name: Test Signature
Date: 2026-10-21 13:00:24 +0700 UTC+0700
No reason specified
Location not specified
Contact info not specified
Fields: 1
Signed: Document is signed
Signature validation: Is valid
Trusted: Untrusted certificate
    no trust roots configured
Certificate: CN=www.freetsa.org,OU=TSA,O=Free TSA,L=Wuerzburg,ST=Bayern,C=DE,...
Certificate issuer: CN=www.freetsa.org,OU=Root CA,O=Free TSA,L=Wuerzburg,ST=Bayern,C=DE,...
Certificate validity: 2026-02-15T19:44:22Z - 2040-02-02T19:44:22Z
Certificate chain: 2 certificate(s)
GeneralizedTime: 2026-10-21 06:00:24 +0000 UTC
Coverage: Entire file is covered by the signature
ByteRange gap: matches /Contents
Revocation data: CRL not found
Revocation data: OCSP not found

CRL found on the first signature reports the revocation-info attribute that the B-LT handler wrote into the CMS while signing. The two revisions after it are the DSS and the document timestamp.

Last updated on