PHP library to digitally sign PDFs using A1 certificates (.pfx/.p12) with a simple, developer-friendly API.
If you need backend PDF signing with cryptographic validity, this library provides a direct flow to:
- apply digital signatures to PDF files
- include signer metadata
- add visible signatures (image)
- apply RFC3161 timestamping (TSA)
- sign the same PDF multiple times (incremental flow)
- PKCS#12 (
.pfx/.p12) digital signature - Fluent builder API (
Signer::signer()) - Invisible signature
- Visible signature with image (
PNG/JPEG) - Automatic default visible appearance (built-in fallback)
- Signature metadata (
name,contactInfo,reason,location) - DocMDP certification (levels 1, 2 and 3)
- Brazil policy mode (
br-iti) signing preset - PAdES Baseline-B profile mode (SubFilter
ETSI.CAdES.detached) - PAdES Baseline-T profile mode (PAdES-B + required timestamp)
- PAdES Baseline-LT profile mode (PAdES-T + embedded DSS/Certs)
- PAdES Baseline-LTA profile mode (PAdES-LT + extra archival timestamp)
- Multiple signatures in the same document
- Optional RFC3161 timestamping
- RFC3161 timestamping with public default TSA when enabled (
withTimestamp()) - PDF permission protection (for example, block content copying)
- Validation of existing digital signatures in PDF files
- PHP
^8.4 ext-opensslext-curl- recommended:
ext-zlibandext-fileinfo
Install with Composer:
composer require jeidison/signer-php<?php
use SignerPHP\Presentation\Signer;
$signedPdf = Signer::signer()
->withPdfContent(file_get_contents('/tmp/input.pdf'))
->withCertificatePath('/tmp/certificate.pfx', 'secret-password')
->sign();
file_put_contents('/tmp/output-signed.pdf', $signedPdf);By default, the library applies a fallback visible appearance with a styled built-in stamp (internal image + default position) for simpler usage.
If you already have PKCS#12 in memory, use content instead of a file path:
$pkcs12 = file_get_contents('/tmp/certificate.pfx');
$signedPdf = Signer::signer()
->withPdfContent(file_get_contents('/tmp/input.pdf'))
->withCertificateContent($pkcs12, 'secret-password')
->sign();<?php
use SignerPHP\Application\DTO\SignatureActorDto;
use SignerPHP\Application\DTO\SignatureMetadataDto;
use SignerPHP\Presentation\Signer;
$metadata = new SignatureMetadataDto(
reason: 'Contract approval',
location: 'Sao Paulo - BR',
actor: new SignatureActorDto(
name: 'Maria Silva',
contactInfo: 'maria@company.com'
)
);
$signedPdf = Signer::signer()
->withPdfContent(file_get_contents('/tmp/input.pdf'))
->withCertificatePath('/tmp/certificate.pfx', 'secret-password')
->withMetadata($metadata)
->sign();<?php
use SignerPHP\Application\DTO\SignatureAppearanceDto;
use SignerPHP\Presentation\Signer;
$appearance = new SignatureAppearanceDto(
imagePath: '/tmp/signature.png',
rect: [350, 770, 500, 830], // [x1, y1, x2, y2]
page: 0 // 0-based index
);
$signedPdf = Signer::signer()
->withPdfContent(file_get_contents('/tmp/input.pdf'))
->withCertificatePath('/tmp/certificate.pfx', 'secret-password')
->withAppearance($appearance)
->sign();<?php
use SignerPHP\Application\DTO\SignatureAppearanceDto;
use SignerPHP\Presentation\Signer;
$base64Image = base64_encode(file_get_contents('/tmp/signature.png'));
$appearance = new SignatureAppearanceDto(
imagePath: $base64Image,
rect: [350, 770, 500, 830],
page: 0
);
$signedPdf = Signer::signer()
->withPdfContent(file_get_contents('/tmp/input.pdf'))
->withCertificatePath('/tmp/certificate.pfx', 'secret-password')
->withAppearance($appearance)
->sign();<?php
use SignerPHP\Presentation\Signer;
$signedPdf = Signer::signer()
->withPdfContent(file_get_contents('/tmp/input.pdf'))
->withCertificatePath('/tmp/certificate.pfx', 'secret-password')
->withoutDefaultAppearance()
->sign();Use the signed output as input for the next signature:
<?php
use SignerPHP\Presentation\Signer;
$step1 = Signer::signer()
->withPdfContent(file_get_contents('/tmp/input.pdf'))
->withCertificatePath('/tmp/signer-a.pfx', 'password-a')
->sign();
$step2 = Signer::signer()
->withPdfContent($step1)
->withCertificatePath('/tmp/signer-b.pfx', 'password-b')
->sign();
file_put_contents('/tmp/output-multi-signed.pdf', $step2);To enable timestamping with default configuration, call withTimestamp().
In this case, the library uses a public default TSA (https://freetsa.org/tsr).
<?php
use SignerPHP\Presentation\Signer;
$signedPdf = Signer::signer()
->withPdfContent(file_get_contents('/tmp/input.pdf'))
->withCertificatePath('/tmp/certificate.pfx', 'secret-password')
->withTimestamp()
->sign();If you want a custom TSA only for this signing flow, use withTimestamp(new TimestampOptionsDto(...)).
The hashAlgorithm field accepts HashAlgorithm (recommended) or a compatible string (sha256, sha384, sha512, sha224, sha1).
<?php
use SignerPHP\Application\DTO\TimestampOptionsDto;
use SignerPHP\Presentation\Signer;
$signedPdf = Signer::signer()
->withPdfContent(file_get_contents('/tmp/input.pdf'))
->withCertificatePath('/tmp/certificate.pfx', 'secret-password')
->withDefaultTimestampProfile(new TimestampOptionsDto(
tsaUrl: 'https://timestamp.your-provider.com',
hashAlgorithm: 'sha256',
certReq: true,
username: null,
password: null,
timeoutSeconds: 15
))
->sign();<?php
use SignerPHP\Presentation\Signer;
$signedPdf = Signer::signer()
->withPdfContent(file_get_contents('/tmp/input.pdf'))
->withCertificatePath('/tmp/certificate.pfx', 'secret-password')
->withoutTimestamp()
->sign();For SaaS/API flows, you can run a real TSA routine (not only endpoint ping) using the timestamp facade:
<?php
use SignerPHP\Application\DTO\TimestampOptionsDto;
use SignerPHP\Presentation\Signer;
$tsa = Signer::timestamp()
->withOptions(new TimestampOptionsDto(
tsaUrl: 'https://freetsa.org/tsr',
hashAlgorithm: 'sha256',
));
$connection = $tsa->testConnection();
if (! $connection->success) {
throw new RuntimeException('TSA test failed: '.($connection->message ?? 'unknown error'));
}
$tokenHex = $tsa
->withContent('probe-content')
->requestTokenHex();<?php
use SignerPHP\Presentation\Signer;
$signedPdf = Signer::signer()
->withPdfContent(file_get_contents('/tmp/input.pdf'))
->withCertificatePath('/tmp/certificate.pfx', 'secret-password')
->withPadesBaselineB()
->sign();<?php
use SignerPHP\Presentation\Signer;
$signedPdf = Signer::signer()
->withPdfContent(file_get_contents('/tmp/input.pdf'))
->withCertificatePath('/tmp/certificate.pfx', 'secret-password')
->withPadesBaselineT()
->sign();In PAdES-T mode, timestamp must be active (for example, withTimestamp(...)).
<?php
use SignerPHP\Presentation\Signer;
$signedPdf = Signer::signer()
->withPdfContent(file_get_contents('/tmp/input.pdf'))
->withCertificatePath('/tmp/certificate.pfx', 'secret-password')
->withPadesBaselineLT()
->sign();In PAdES-LT mode, besides timestamping, the library applies DSS enrichment with certificates extracted from CMS/RFC3161 signatures.
<?php
use SignerPHP\Presentation\Signer;
$signedPdf = Signer::signer()
->withPdfContent(file_get_contents('/tmp/input.pdf'))
->withCertificatePath('/tmp/certificate.pfx', 'secret-password')
->withPadesBaselineLTA()
->sign();In PAdES-LTA mode, after LT enrichment, the library adds one extra archival Document Timestamp on the final document revision.
<?php
use SignerPHP\Application\DTO\CertificationLevel;
use SignerPHP\Presentation\Signer;
$signedPdf = Signer::signer()
->withPdfContent(file_get_contents('/tmp/input.pdf'))
->withCertificatePath('/tmp/certificate.pfx', 'secret-password')
->withCertificationLevel(CertificationLevel::FormFillAndSignatures)
->sign();Available levels:
CertificationLevel::NoChangesAllowed(1): no changes allowed after certification.CertificationLevel::FormFillAndSignatures(2): allows form filling and additional signatures.CertificationLevel::FormFillSignaturesAndAnnotations(3): allows forms, signatures, and annotations.
Default behavior:
- Without
withCertificationLevel(...), DocMDP is not explicitly set (null). - In
withBrazilPolicy(...), the library forcesCertificationLevel::FormFillAndSignatures(level2).
<?php
use SignerPHP\Application\DTO\BrazilSignaturePolicyOptionsDto;
use SignerPHP\Presentation\Signer;
$signedPdf = Signer::signer()
->withPdfContent(file_get_contents('/tmp/input.pdf'))
->withCertificatePath('/tmp/certificate.pfx', 'secret-password')
->withBrazilPolicy(new BrazilSignaturePolicyOptionsDto(
tsaUrl: 'https://tsa.your-icpbrasil-provider.com',
hashAlgorithm: 'sha256',
timeoutSeconds: 20,
certReq: true,
))
->sign();This preset applies PAdES-LTA + DocMDP=2 + explicit policy timestamp.
To switch quickly to another TSA:
$policy = BrazilSignaturePolicyOptionsDto::tsa('https://tsa.your-provider.com')
->withHashAlgorithm('sha256')
->withTimeoutSeconds(20);SERPRO support (homologation):
- OAuth2 token:
https://gateway.apiserpro.serpro.gov.br/token - ASN.1 timestamp endpoint:
https://gateway.apiserpro.serpro.gov.br/apitimestamp/v1/stamps-asn1
SERPRO helper example:
<?php
use SignerPHP\Application\DTO\BrazilSignaturePolicyOptionsDto;
use SignerPHP\Presentation\Signer;
$signedPdf = Signer::signer()
->withPdfContent(file_get_contents('/tmp/input.pdf'))
->withCertificatePath('/tmp/certificate.pfx', 'secret-password')
->withBrazilPolicy(BrazilSignaturePolicyOptionsDto::serpro(
consumerKey: 'YOUR_CONSUMER_KEY',
consumerSecret: 'YOUR_CONSUMER_SECRET',
hashAlgorithm: 'sha256',
timeoutSeconds: 20
))
->sign();<?php
use SignerPHP\Application\DTO\ProtectionOptionsDto;
use SignerPHP\Presentation\Signer;
$protectedPdf = Signer::protection()
->withPdfContent(file_get_contents('/tmp/input.pdf'))
->withProtection(ProtectionOptionsDto::preventCopy(
ownerPassword: 'owner-secret',
userPassword: ''
))
->protect();
file_put_contents('/tmp/output-protected.pdf', $protectedPdf);Use this flow to avoid ordering mistakes and ensure the signature is applied on the already protected PDF.
<?php
use SignerPHP\Application\DTO\ProtectionOptionsDto;
use SignerPHP\Presentation\Signer;
$signedProtectedPdf = Signer::signer()
->withPdfContent(file_get_contents('/tmp/input.pdf'))
->withProtection(ProtectionOptionsDto::preventCopy(
ownerPassword: 'owner-secret',
userPassword: ''
))
->withCertificatePath('/tmp/certificate.pfx', 'secret-password')
->protectThenSign();
file_put_contents('/tmp/output-protected-signed.pdf', $signedProtectedPdf);<?php
use SignerPHP\Presentation\Signer;
$validation = Signer::validation()
->withPdfContent(file_get_contents('/tmp/input.pdf'))
->validate();
if (! $validation->hasSignatures) {
// PDF has no signatures
}
if ($validation->allValid) {
// all signatures were verified
}<?php
use SignerPHP\Presentation\Signer;
$validation = Signer::validation()
->withPdfContent(file_get_contents('/tmp/input.pdf'))
->enableTrustChainValidation('/etc/ssl/certs/ca-certificates.crt') // optional; if omitted, tries the system default bundle
->validate();
foreach ($validation->entries as $entry) {
var_dump($entry->trustValid); // true|false|null
}<?php
use SignerPHP\Presentation\Signer;
$validation = Signer::validation()
->withPdfContent(file_get_contents('/tmp/input.pdf'))
->withBrazilPolicy('/path/to/icp-brasil-bundle.pem')
->validate();withBrazilPolicy(...) trust store precedence:
- If
trustStorePathis provided, it is used directly. - If
trustStorePathisnull, the library automatically builds/updates a local cached ICP-Brasil bundle and uses it.
If trustStorePath is null, br-iti mode builds an ICP-Brasil trust anchors bundle automatically in local cache:
- default directory:
sys_get_temp_dir()/signer-php/trust-anchors - default URLs:
http://acraiz.icpbrasil.gov.br/Certificado_AC_Raiz.crthttp://acraiz.icpbrasil.gov.br/credenciadas/RAIZ/ICP-Brasilv2.crthttp://acraiz.icpbrasil.gov.br/credenciadas/RAIZ/ICP-Brasilv5.crthttp://acraiz.icpbrasil.gov.br/credenciadas/RAIZ/ICP-Brasilv6.crthttp://acraiz.icpbrasil.gov.br/credenciadas/RAIZ/ICP-Brasilv7.crt
In br-iti mode, validation also verifies ICP-Brasil PAdES policy list (LPA):
https://politicas.icpbrasil.gov.br/LPA_PAdES.derhttps://politicas.icpbrasil.gov.br/LPA_PAdES.p7s
You can override these URLs:
<?php
use SignerPHP\Application\DTO\BrazilPolicyLpaUrlsDto;
use SignerPHP\Application\DTO\BrazilTrustAnchorsOptionsDto;
use SignerPHP\Presentation\Signer;
$validation = Signer::validation()
->withPdfContent(file_get_contents('/tmp/input.pdf'))
->withBrazilPolicy(
'/path/to/icp-brasil-bundle.pem',
new BrazilPolicyLpaUrlsDto(
lpaUrlAsn1Pades: 'https://your-endpoint/LPA_PAdES.der',
lpaUrlAsn1SignaturePades: 'https://your-endpoint/LPA_PAdES.p7s',
),
new BrazilTrustAnchorsOptionsDto(
directory: '/tmp/my-trust-anchors-cache',
urls: [
'http://acraiz.icpbrasil.gov.br/Certificado_AC_Raiz.crt',
'http://acraiz.icpbrasil.gov.br/credenciadas/RAIZ/ICP-Brasilv7.crt',
],
),
)
->validate();trustValid = true: certificate chain is valid for the trust store in use.trustValid = false: chain is invalid, incomplete, or not trusted by the trust store in use.trustValid = null: trust chain validation was not executed for that signature.policyValid = true: PAdES LPA check (ICP-Brasil or overridden URLs) passed.policyValid = false: policy/LPA check failed for that signature.policyValid = null: policy mode was not requested in the flow.
Besides signing, the project provides bin/signer-inspect for technical diagnostics of signed PDFs.
php bin/signer-inspect --input=/tmp/output-signed.pdfphp bin/signer-inspect --input=/tmp/output-signed.pdf --jsonMain inspection fields:
inferred_profile: inferred profile (pades-baseline-b,t,lt,lta).features: presence ofDSS,VRI,DocMDP,OCSPs,CRLs.revocation_endpoints: OCSP/CRL endpoints per discovered certificate.revocation_risk_summary: connectivity/missing-endpoint risk flags.
This inspection helps explain warnings reported by external validators (for example CRL/OCSP connectivity issues).
Use bin/signer-doctor to quickly validate runtime dependencies (PHP version, required extensions, OpenSSL binary and openssl ts, qpdf, and temporary directory access).
php bin/signer-doctorphp bin/signer-doctor --json- Sign with
--policy=br-itiusingbin/signer-sign. - Validate programmatically with
Signer::validation()->withBrazilPolicy(...). - Inspect final PDF with
bin/signer-inspect --json. - If revocation warnings appear, check
revocation_risk_summaryand endpoint availability for OCSP/CRL URLs.
This project already includes docker-compose.yml and Dockerfile for the app service.
docker network create kool_globaldocker compose up -d --builddocker compose exec app composer installdocker compose exec app php ./vendor/bin/pest --configuration phpunit.xml testsdocker compose downThe project provides bin/signer-sign, bin/signer-inspect, and bin/signer-doctor.
php bin/signer-sign \
--input=/tmp/input.pdf \
--output=/tmp/output-signed.pdf \
--cert=/tmp/certificate.pfx \
--password='secret-password'php bin/signer-sign \
--input=/tmp/input.pdf \
--output=/tmp/output-signed.pdf \
--cert=/tmp/certificate.pfx \
--password='secret-password' \
--pades-baseline-b \
--timestamp-url='https://tsa.example.com' \
--timestamp-hash='sha256' \
--timestamp-timeout=20php bin/signer-sign \
--input=/tmp/input.pdf \
--output=/tmp/output-certified.pdf \
--cert=/tmp/certificate.pfx \
--password='secret-password' \
--certification-level=2php bin/signer-sign \
--input=/tmp/input.pdf \
--output=/tmp/output-br-iti.pdf \
--cert=/tmp/certificate.pfx \
--password='secret-password' \
--policy=br-iti \
--policy-serpro-consumer-key='YOUR_CONSUMER_KEY' \
--policy-serpro-consumer-secret='YOUR_CONSUMER_SECRET' \
--policy-timestamp-hash='sha256' \
--policy-timestamp-timeout=20php bin/signer-sign \
--input=/tmp/input.pdf \
--output=/tmp/output-signed-lt.pdf \
--cert=/tmp/certificate.pfx \
--password='secret-password' \
--pades-baseline-lt \
--timestamp-url='https://freetsa.org/tsr' \
--timestamp-hash='sha256' \
--timestamp-timeout=20php bin/signer-sign \
--input=/tmp/input.pdf \
--output=/tmp/output-signed-lta.pdf \
--cert=/tmp/certificate.pfx \
--password='secret-password' \
--pades-baseline-lta \
--timestamp-url='https://freetsa.org/tsr' \
--timestamp-hash='sha256' \
--timestamp-timeout=20php bin/signer-sign --helpSignerPHP\\Domain\\Exception\\InvalidCertificateExceptionSignerPHP\\Domain\\Exception\\SignProcessExceptionSignerPHP\\Domain\\Exception\\SignerException
- For RFC3161 timestamping, host
opensslmust supportopenssl ts. - For PDF permissions protection, host
qpdfmust be installed. - If you use the public default TSA, consider availability/SLA for production and prefer a dedicated provider.
- A digital signature covers specific PDF bytes (
ByteRange). Because of that, operations that rewrite file bytes should run before signing. - Validation checks
ByteRangeintegrity and CMS/PKCS#7 cryptographic validity with OpenSSL. Optionally, it can validate trust chain (enableTrustChainValidation(...)) and Brazil policy (withBrazilPolicy(...)). - PAdES profiles:
- Baseline-B:
SubFilter /ETSI.CAdES.detached - Baseline-T: Baseline-B + RFC3161
- Baseline-LT: Baseline-T +
DSS(Certs,OCSPs,CRLs,VRI) with best-effort evidence collection depending on chain endpoint availability - Baseline-LTA: Baseline-LT + additional archival
Document Timestamp
- Baseline-B:
- Default appearance uses
page = 0and an internal default rectangle; for full control usewithAppearance(...). - Some PNG variations (specific filters/modes) may be rejected.
- Current PDF parser technical scope:
- Objects with generation different from
0are not supported - Extended object streams are not supported
- Objects with generation different from
vendor/bin/pest --configuration phpunit.xml testscomposer pint:check
composer pint:fix
composer security:audit
composer tests:coverage
composer psalm
composer deptrac
composer infection