Skip to content

jeidison/signer-php

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

14 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Signer PHP (PDF Module)

PHP library to digitally sign PDFs using A1 certificates (.pfx/.p12) with a simple, developer-friendly API.

What problem it solves

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)

Main features

  • 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

Requirements

  • PHP ^8.4
  • ext-openssl
  • ext-curl
  • recommended: ext-zlib and ext-fileinfo

Installation

Install with Composer:

composer require jeidison/signer-php

Usage

1) Basic signature

<?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();

2) Signature with metadata

<?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();

3) Visible signature with image

<?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();

4) Visible signature with base64 image

<?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();

4.1) Disable default appearance (invisible signature)

<?php

use SignerPHP\Presentation\Signer;

$signedPdf = Signer::signer()
 ->withPdfContent(file_get_contents('/tmp/input.pdf'))
 ->withCertificatePath('/tmp/certificate.pfx', 'secret-password')
 ->withoutDefaultAppearance()
 ->sign();

5) Multiple signatures in the same PDF

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);

6) Signature with RFC3161 timestamp

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

6.1) Override the default TSA for this flow

<?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();

6.2) Disable default timestamping

<?php

use SignerPHP\Presentation\Signer;

$signedPdf = Signer::signer()
 ->withPdfContent(file_get_contents('/tmp/input.pdf'))
 ->withCertificatePath('/tmp/certificate.pfx', 'secret-password')
 ->withoutTimestamp()
 ->sign();

6.3) Real TSA test and token generation (RFC3161)

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();

6.3) Enable PAdES Baseline-B profile

<?php

use SignerPHP\Presentation\Signer;

$signedPdf = Signer::signer()
 ->withPdfContent(file_get_contents('/tmp/input.pdf'))
 ->withCertificatePath('/tmp/certificate.pfx', 'secret-password')
 ->withPadesBaselineB()
 ->sign();

6.4) Enable PAdES Baseline-T profile

<?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(...)).

6.5) Enable PAdES Baseline-LT profile

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

6.6) Enable PAdES Baseline-LTA profile

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

6.7) Define document certification (DocMDP)

<?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 forces CertificationLevel::FormFillAndSignatures (level 2).

6.8) Brazil policy mode (br-iti)

<?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();

7) Protect PDF (block copy/print/modify)

<?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);

8) Recommended flow: protect and sign in the same builder

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);

9) Validate digital signatures in a PDF

<?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
}

9.1) Validation with trust chain (trust store)

<?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
}

9.2) Validation with Brazil policy mode (br-iti)

<?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 trustStorePath is provided, it is used directly.
  • If trustStorePath is null, 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.crt
  • http://acraiz.icpbrasil.gov.br/credenciadas/RAIZ/ICP-Brasilv2.crt
  • http://acraiz.icpbrasil.gov.br/credenciadas/RAIZ/ICP-Brasilv5.crt
  • http://acraiz.icpbrasil.gov.br/credenciadas/RAIZ/ICP-Brasilv6.crt
  • http://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.der
  • https://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();

9.3) How to interpret trustValid and policyValid

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

Signature inspection via CLI

Besides signing, the project provides bin/signer-inspect for technical diagnostics of signed PDFs.

Basic usage

php bin/signer-inspect --input=/tmp/output-signed.pdf

JSON output

php bin/signer-inspect --input=/tmp/output-signed.pdf --json

Main inspection fields:

  • inferred_profile: inferred profile (pades-baseline-b, t, lt, lta).
  • features: presence of DSS, 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).

Environment diagnostics via CLI

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-doctor
php bin/signer-doctor --json

Recommended flow (Brazil/ITI)

  1. Sign with --policy=br-iti using bin/signer-sign.
  2. Validate programmatically with Signer::validation()->withBrazilPolicy(...).
  3. Inspect final PDF with bin/signer-inspect --json.
  4. If revocation warnings appear, check revocation_risk_summary and endpoint availability for OCSP/CRL URLs.

Running with Docker Compose

This project already includes docker-compose.yml and Dockerfile for the app service.

1) Create external network (first time only)

docker network create kool_global

2) Start the service

docker compose up -d --build

3) Install dependencies

docker compose exec app composer install

4) Run tests

docker compose exec app php ./vendor/bin/pest --configuration phpunit.xml tests

5) Stop environment

docker compose down

Sign using command line (CLI)

The project provides bin/signer-sign, bin/signer-inspect, and bin/signer-doctor.

Basic usage

php bin/signer-sign \
 --input=/tmp/input.pdf \
 --output=/tmp/output-signed.pdf \
 --cert=/tmp/certificate.pfx \
 --password='secret-password'

Example with PAdES Baseline-B and explicit timestamp

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=20

Example with DocMDP certification

php bin/signer-sign \
 --input=/tmp/input.pdf \
 --output=/tmp/output-certified.pdf \
 --cert=/tmp/certificate.pfx \
 --password='secret-password' \
 --certification-level=2

Example with Brazil policy in CLI

php 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=20

Example with PAdES Baseline-LT

php 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=20

Example with PAdES Baseline-LTA

php 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=20

Options help

php bin/signer-sign --help

Exceptions you should handle

  • SignerPHP\\Domain\\Exception\\InvalidCertificateException
  • SignerPHP\\Domain\\Exception\\SignProcessException
  • SignerPHP\\Domain\\Exception\\SignerException

Operational requirements

  • For RFC3161 timestamping, host openssl must support openssl ts.
  • For PDF permissions protection, host qpdf must be installed.
  • If you use the public default TSA, consider availability/SLA for production and prefer a dedicated provider.

Implementation notes

  • A digital signature covers specific PDF bytes (ByteRange). Because of that, operations that rewrite file bytes should run before signing.
  • Validation checks ByteRange integrity 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
  • Default appearance uses page = 0 and an internal default rectangle; for full control use withAppearance(...).
  • Some PNG variations (specific filters/modes) may be rejected.
  • Current PDF parser technical scope:
    • Objects with generation different from 0 are not supported
    • Extended object streams are not supported

Running tests

vendor/bin/pest --configuration phpunit.xml tests

Code quality

composer pint:check
composer pint:fix
composer security:audit
composer tests:coverage
composer psalm
composer deptrac
composer infection

About

Sign PDF docs using PHP

Resources

Stars

Watchers

Forks

Packages

No packages published

Contributors 3

  •  
  •  
  •