Skip to content

Signing a document

newSignature() returns a Signing\PendingSignature, the fluent builder that is the primary API. A chain needs a certificate, a document, and sign(). Everything else is optional.

php
use LSNepomuceno\Signet\Signet;

$signed = new Signet()->newSignature()
    ->certificate($pfxPath, $password)
    ->pdf($pdfPath)
    ->info(name: 'Lucas Nepomuceno', reason: 'Contract')
    ->sign();

Where the certificate comes from

One of these, and only one:

MethodTakes
certificate($path, $password)a PKCS#12 file on disk, .pfx or .p12
certificateContents($bytes, $password)PKCS#12 bytes already in hand
certificatePem($path, $keyPath, $password)PEM, key combined or in its own file
certificateFromPem($contents, $key, $password)PEM bytes already in hand
usingCertificate($certificate)an already-parsed Data\Certificate

certificate() checks the extension and raises InvalidPFXException for anything that is not .pfx or .p12, because a PEM handed to the PKCS#12 reader fails later and less clearly. Reading a certificate is covered in Working with certificates.

Where the document comes from

A path is the common case, not the only one.

php
->pdf('/path/contract.pdf')                        // a file on disk
->pdf('/path/contract.pdf', 'the document password')   // an encrypted file
->pdfContents($bytes, 'contract.pdf')              // bytes already in hand
->from($source)                                    // anything else

The second argument to pdf() is the document's password, not the certificate's. One opens the file, the other unlocks the key that signs it: see Encrypted documents.

from() takes a Contracts\PdfSource, so bytes in a queue payload, in object storage or in memory never need a temporary file:

php
use LSNepomuceno\Signet\Io\StringSource;
use LSNepomuceno\Signet\Io\StreamSource;
use LSNepomuceno\Signet\Io\FileSource;

->from(new StringSource($bytes, 'contract.pdf'))
->from(new StreamSource($handle, 'contract.pdf'))
->from(new FileSource('/path/contract.pdf'))

Implement Contracts\PdfSource for anything the three do not cover: an object store, a database blob, a remote fetch your application already owns.

What the signature says about itself

php
->info(
    name: 'Lucas Nepomuceno',
    location: 'Sao Paulo',
    reason: 'Contract',
    contactInfo: 'lsn.nepomuceno@gmail.com',
)

All four are optional and all four land in the signature dictionary, where a reader shows them. None of them is attested by anything: they are the signer's own claims, inside the range the signature covers, so altering them breaks the signature without making them true.

Naming the field

php
->fieldName('Signature1')

The name the signature field carries in the document. Omitted, the package picks one. Filling a field a template already declares is a different method, and laying one out is a different call entirely: both are on Signature fields.

What comes back

sign() returns a Data\SignedPdf, not a transport. The caller decides what it becomes:

php
$signed->contents();                    // string, the signed bytes
$signed->size();                        // int
$signed->name();                        // string, the file name it carries
$signed->save('/path/signed.pdf');      // string, the path written
$signed->writeTo($destination);         // string, via a Contracts\PdfDestination
(string) $signed;                       // same as contents()
php
use LSNepomuceno\Signet\Io\FileDestination;
use LSNepomuceno\Signet\Io\StreamDestination;

$signed->writeTo(new FileDestination('/var/documents'));
$signed->writeTo(new StreamDestination($handle));

There is no download() and no toResponse()

They existed before the extraction and were removed on purpose: a signing core that returns an HTTP response has an opinion about the framework calling it. Build the response in your application from contents() and name(), which is three lines and stays yours. See 0100.

Signing more than once

Every signature appends a revision, so signing an already-signed document is the normal path and needs nothing special:

php
$first  = $signet->newSignature()->certificate($pfxA, $pwA)->pdf($path)->sign();
$second = $signet->newSignature()
    ->certificate($pfxB, $pwB)
    ->pdfContents($first->contents(), 'contract.pdf')
    ->sign();

Both signatures verify, and each covers the bytes that existed when it was made. A validator reports the second as covering the whole document and the first as not, which is the truth rather than a defect: Verifying signatures explains how that reads.

The shortcuts

For callers that do not need the builder:

php
$signet->signFromFile($pfxPath, $password, $pdfPath);
$signet->signFromPem($pemPath, $password, $pdfPath, $privateKeyPath);

Both return the same Data\SignedPdf. They take the defaults for everything the builder would let you set, which makes them right for a one-shot script and wrong for anything that needs a profile, a seal or a field.

What the signer accepts

StructureHandled
Classic cross-reference table, §7.5.4read and written
Cross-reference stream, §7.5.8read and written, following the form the document already uses (0009)
Object streams, §7.5.7packed objects are read, and written back uncompressed by the revision that changes them (0015)

The last two travel together in practice: Word, "print to PDF" in Chrome and LaTeX with compression all emit both.

What it will not do

Not supportedWhy
RC4-encrypted documentsrefused deliberately: signing one means writing RC4 back into it
A security handler other than the standard oneits key comes from somewhere this package cannot reach
A3 tokens, smart cards, HSMsout of scope: this package signs with A1 material it can hold

Released under the MIT Licence.