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.
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:
| Method | Takes |
|---|---|
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.
->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 elseThe 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:
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
->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
->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:
$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()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:
$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:
$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
| Structure | Handled |
|---|---|
| Classic cross-reference table, §7.5.4 | read and written |
| Cross-reference stream, §7.5.8 | read and written, following the form the document already uses (0009) |
| Object streams, §7.5.7 | packed 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 supported | Why |
|---|---|
| RC4-encrypted documents | refused deliberately: signing one means writing RC4 back into it |
| A security handler other than the standard one | its key comes from somewhere this package cannot reach |
| A3 tokens, smart cards, HSMs | out of scope: this package signs with A1 material it can hold |