Command line
The package ships vendor/bin/signet, built on symfony/console, with six commands. It is meant for operators and for CI rather than as the primary API.
vendor/bin/signet sign contract.pdf --certificate cert.pfx
vendor/bin/signet verify contract-signed.pdf
vendor/bin/signet fields contract.pdf
vendor/bin/signet field:add contract.pdf Approval --out prepared.pdf
vendor/bin/signet extend archive.pdf --out archive-renewed.pdf
vendor/bin/signet checksign
Signs a document with an A1 certificate.
Every option maps onto one call on the fluent builder, so a shell script and a PHP application describe the same signature in the same words.
| Option | Requirement | Meaning | Builder |
|---|---|---|---|
pdf | argument, required | path to the document | pdf() |
--certificate, -c | required | path to the PKCS#12 or PEM certificate | certificate() |
--password-env | default SIGNET_PASSWORD | name of the environment variable holding the password | |
--document-password-env | the same for the document's password, when it is encrypted | pdf($path, $password) | |
--out, -o | where to write the signed document | save() | |
--profile, -p | default pades-b-b | legacy, pades-b-b, pades-b-t, pades-b-lt, pades-b-lta | profile() |
--tsa | timestamp authority URL, required from pades-b-t up | ||
--chain | repeatable | a PEM or DER certificate to fold into the chain, for a bundle that carries only the leaf | chain() |
--name, --reason, --location, --contact | what the signature says about itself | info() | |
--seal | draw a visible seal, rendered from the certificate | seal() | |
--seal-image | stamp your own artwork instead, and implies --seal | sealFrom() | |
--seal-page | default last | first, last, or a page number | SealPage, SealPlacement::$page |
--seal-every-page | put it on every page | SealPlacement::$onEveryPage | |
--seal-x, --seal-y, --seal-width, --seal-height | where it goes, in points from the bottom-left corner | SealPlacement | |
--certify | no-changes, form-filling or annotations | certify() | |
--lock | all, include:A,B or exclude:A,B | lock() | |
--into-field | fill a signature field the document already carries | intoField() | |
--field-name | default Signature | name the field this signature creates | fieldName() |
export SIGNET_PASSWORD='the certificate password'
vendor/bin/signet sign contract.pdf -c cert.pfx -o signed.pdf \
--name 'Lucas Nepomuceno' --reason Contract \
--seal --seal-page first --seal-x 40 --seal-y 60 --seal-width 120 --seal-height 30 \
--certify form-filling --lock include:Amount,DateThere is no --seal-placement=bottom-right
Data\SealPlacement is absolute user space, so a named corner would have to be resolved against the page box, and doing that correctly is its own piece of work: a crop box smaller than the sheet and a /UserUnit on a plot both move where a corner is. Inventing a vocabulary the library does not have would put that arithmetic in the command rather than where it belongs.
--into-field and --field-name are mutually exclusive
One fills a field the document already carries and the other names one this signature creates. The command refuses rather than picking: resolving it by precedence would create a field beside the one you meant to fill.
export SIGNET_PASSWORD='the certificate password'
vendor/bin/signet sign contract.pdf -c cert.pfx -o signed.pdf
vendor/bin/signet sign contract.pdf \
-c cert.pfx \
-o signed.pdf \
-p pades-b-t \
--tsa https://freetsa.org/tsrThe passwords are never arguments
They are read from environment variables whose names you pass, because a command line is visible in ps and lands in shell history. --password-env and --document-password-env name the variables; neither takes a password.
verify
vendor/bin/signet verify contract-signed.pdf
vendor/bin/signet verify contract-signed.pdf --trust /etc/ssl/anchors
vendor/bin/signet verify contract-signed.pdf --json| Option | Meaning |
|---|---|
--json | print a machine-readable report |
--trust | a PEM file or a directory of roots to validate the chain against |
--document-password-env | names the environment variable holding an encrypted document's password |
A signature verifies without the document's password, because a signature's own bytes are never encrypted. What the password buys is the validation material a pades-b-lt document carries: without it the store's OCSP responses and CRLs are present and unreadable, and revocation reports as unknown.
The verdict is in the exit status, so a build can gate on it without parsing anything:
| Status | Means |
|---|---|
0 | every signature verifies |
1 | one does not |
2 | the document could not be read |
if ! vendor/bin/signet verify "$file" --json > report.json; then
echo "refusing to publish $file"
exit 1
fifields
Lists the signature fields a document declares, signed or not. It is the question that comes before signing into a template someone else laid out.
vendor/bin/signet fields template.pdf
vendor/bin/signet fields template.pdf --jsonfield:add
Adds an empty signature field, which sign --into-field then fills. It is what lets a template be prepared from a shell script rather than from a word processor.
vendor/bin/signet field:add contract.pdf Approval --out prepared.pdf
vendor/bin/signet field:add contract.pdf Approval -o prepared.pdf \
--x=40 --y=60 --width=180 --height=60 --page=last| Option | Meaning |
|---|---|
--out, -o | where to write the result |
--in-place | overwrite the document instead of writing a copy |
--page | first, last, or a page number |
--x, --y, --width, --height | the box, in points, measured from the bottom-left corner of the visible area |
--document-password-env | names the environment variable holding an encrypted document's password |
No certificate, and no --password-env. Adding a field is not a cryptographic act, so this command takes no key material at all (0111).
With no --width and no --height the field is invisible, which is legal and common. Passing one without the other is refused rather than guessed at.
extend
Appends a fresh archive timestamp to a document that already carries signatures, which is what keeps a B-LTA archive checkable as the algorithms under it age (0022).
No certificate is involved. A DocTimeStamp is signed by the authority and not by the signer, so this is the one thing in the package that belongs in a cron entry with no key material on the machine.
vendor/bin/signet extend archive.pdf --out archive-renewed.pdf
vendor/bin/signet extend archive.pdf --in-place --if-due=365 --json| Option | Meaning |
|---|---|
--out, -o | where to write the extended document |
--in-place | overwrite the document instead of writing a copy |
--tsa | timestamp authority URL |
--if-due | extend only when the newest archive timestamp is older than this many days |
--json | print a machine-readable report |
--document-password-env | names the environment variable holding an encrypted document's password |
The destination is never guessed
One of --out and --in-place is required. Writing in place is what a retention job usually wants and is also the only version that can destroy an archive, so it is stated rather than fallen into.
--if-due is what turns the entry from "extend everything every night" into something that can run over a directory: a document stamped last month is left alone, and the authority is not asked about it. An age the command cannot establish counts as due, since extending a document that did not need it costs one request and skipping one that did lets an archive age out.
The three failures are three different problems, and only one of them is worth retrying:
| Status | Means |
|---|---|
0 | extended, or nothing was due |
1 | something else failed, including a document that could not be written |
2 | the document could not be read |
3 | the document carries no signature, so there is nothing to archive |
4 | the document is certified no-changes, which forbids the revision |
75 | the authority did not answer. EX_TEMPFAIL, and the one to retry |
# renew every archive older than a year, retrying only what is worth retrying
for file in /var/archive/*.pdf; do
vendor/bin/signet extend "$file" --in-place --if-due=365 --tsa https://freetsa.org/tsr
test $? -eq 75 && echo "$file" >> /var/archive/retry.txt
donecheck
Reports what this package needs from the environment, before anything is signed.
vendor/bin/signet check
vendor/bin/signet check --tsa
vendor/bin/signet check --tsa --tsa-url https://freetsa.org/tsr| Option | Meaning |
|---|---|
--tsa | also reach the configured timestamp authority |
--tsa-url | the authority to reach, with --tsa |
It exists because a missing openssl binary once made validation report every signature as invalid, in silence. Run it in the image build, not after the first support ticket.