Skip to content

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.

bash
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 check

sign

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.

OptionRequirementMeaningBuilder
pdfargument, requiredpath to the documentpdf()
--certificate, -crequiredpath to the PKCS#12 or PEM certificatecertificate()
--password-envdefault SIGNET_PASSWORDname of the environment variable holding the password
--document-password-envthe same for the document's password, when it is encryptedpdf($path, $password)
--out, -owhere to write the signed documentsave()
--profile, -pdefault pades-b-blegacy, pades-b-b, pades-b-t, pades-b-lt, pades-b-ltaprofile()
--tsatimestamp authority URL, required from pades-b-t up
--chainrepeatablea PEM or DER certificate to fold into the chain, for a bundle that carries only the leafchain()
--name, --reason, --location, --contactwhat the signature says about itselfinfo()
--sealdraw a visible seal, rendered from the certificateseal()
--seal-imagestamp your own artwork instead, and implies --sealsealFrom()
--seal-pagedefault lastfirst, last, or a page numberSealPage, SealPlacement::$page
--seal-every-pageput it on every pageSealPlacement::$onEveryPage
--seal-x, --seal-y, --seal-width, --seal-heightwhere it goes, in points from the bottom-left cornerSealPlacement
--certifyno-changes, form-filling or annotationscertify()
--lockall, include:A,B or exclude:A,Block()
--into-fieldfill a signature field the document already carriesintoField()
--field-namedefault Signaturename the field this signature createsfieldName()
bash
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,Date

There 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.

bash
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/tsr

The 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

bash
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
OptionMeaning
--jsonprint a machine-readable report
--trusta PEM file or a directory of roots to validate the chain against
--document-password-envnames 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:

StatusMeans
0every signature verifies
1one does not
2the document could not be read
bash
if ! vendor/bin/signet verify "$file" --json > report.json; then
    echo "refusing to publish $file"
    exit 1
fi

fields

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.

bash
vendor/bin/signet fields template.pdf
vendor/bin/signet fields template.pdf --json

field: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.

bash
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
OptionMeaning
--out, -owhere to write the result
--in-placeoverwrite the document instead of writing a copy
--pagefirst, last, or a page number
--x, --y, --width, --heightthe box, in points, measured from the bottom-left corner of the visible area
--document-password-envnames 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.

bash
vendor/bin/signet extend archive.pdf --out archive-renewed.pdf
vendor/bin/signet extend archive.pdf --in-place --if-due=365 --json
OptionMeaning
--out, -owhere to write the extended document
--in-placeoverwrite the document instead of writing a copy
--tsatimestamp authority URL
--if-dueextend only when the newest archive timestamp is older than this many days
--jsonprint a machine-readable report
--document-password-envnames 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:

StatusMeans
0extended, or nothing was due
1something else failed, including a document that could not be written
2the document could not be read
3the document carries no signature, so there is nothing to archive
4the document is certified no-changes, which forbids the revision
75the authority did not answer. EX_TEMPFAIL, and the one to retry
bash
# 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
done

check

Reports what this package needs from the environment, before anything is signed.

bash
vendor/bin/signet check
vendor/bin/signet check --tsa
vendor/bin/signet check --tsa --tsa-url https://freetsa.org/tsr
OptionMeaning
--tsaalso reach the configured timestamp authority
--tsa-urlthe 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.

Released under the MIT Licence.