Pular para o conteúdo

Assine um PDF com PAdES B-B e depois estenda-o para PAdES B-T

Use esta receita para produzir uma assinatura Portable Document Format (PDF) Advanced Electronic Signatures (PAdES) B-B: um Cryptographic Message Syntax (CMS) SignedData com atributos assinados (content-type, message-digest, signing-time). Depois, estenda essa assinatura para PAdES B-T adicionando um RFC 3161 signature-time-stamp. B-T é B-B mais um único carimbo de tempo; não é uma classe de assinatura separada. O limite de confiança é explícito: produzir uma assinatura não é o mesmo que um verificador decidir que ela é válida.

Ressalva U-1. O NextPDF não afirma nenhuma certificação independente ETSI EN 319 142-1 para PAdES B-T. A EN 319 142-1 não está no corpus de verificação; o requisito de B-T signature-time-stamp foi verificado em relação a ETSI EN 319 122-1 §5.3 em conjunto com RFC 3161, RFC 5652, RFC 5816 e ISO 32000-2 §12.8. O suporte ao perfil B-T não é uma certificação de conformidade nem de validade jurídica; um validador independente faz essa determinação.

B-LT e B-LTA (material de validação do Document Security Store (DSS), laço de carimbo de tempo de arquivamento) estão fora do escopo desta receita e não fazem parte da superfície de assinatura Core/Pro abordada aqui.

Terminal window
composer require nextpdf/core:^3

ext-openssl precisa estar habilitado porque CertificateInfo analisa as chaves por meio do OpenSSL. O B-T também precisa de um endpoint RFC 3161 Time Stamping Authority (TSA) acessível e de um cliente HTTP PHP Standards Recommendation (PSR)-18.

Uma assinatura PAdES B-B armazena um CMS SignedData codificado em Distinguished Encoding Rules (DER) na entrada Contents do dicionário de assinatura; o valor de Contents é uma string hexadecimal preenchida sobre o digest de intervalo de bytes (ISO 32000-2 §12.8.1). O digest cobre o arquivo e exclui o próprio valor da assinatura (ISO 32000-2 §12.8.1).

PAdES B-T adiciona exatamente um RFC 3161 signature-time-stamp. A impressão da mensagem do carimbo de tempo é o hash dos octetos do valor da assinatura do SignerInfo, sem tag nem prefixo de comprimento Abstract Syntax Notation One (ASN.1) (ETSI EN 319 122-1 §5.3; RFC 3161 Apêndice A). O token é transportado como o atributo não assinado id-aa-timeStampToken, object identifier (OID) 1.2.840.113549.1.9.16.2.14 (RFC 3161 Apêndice A), colocado em SignerInfo.unsignedAttrs [1] IMPLICIT (RFC 5652 §5.3). Como os atributos não assinados não são protegidos pela assinatura (RFC 5652 §5.4), o digest assinado do B-B, o /ByteRange e os bytes da assinatura B-B permanecem inalterados — o B-T apenas anexa o carimbo de tempo. O certificado do TSA é identificado com ESSCertIDv2 (RFC 5816 atualiza a RFC 3161).

Ressalva U-1 (reafirmada na declaração de B-T). O NextPDF não afirma nenhuma certificação independente ETSI EN 319 142-1 para PAdES B-T. A EN 319 142-1 não está no corpus de verificação; o requisito de B-T signature-time-stamp foi verificado em relação a ETSI EN 319 122-1 §5.3 em conjunto com RFC 3161, RFC 5652, RFC 5816 e ISO 32000-2 §12.8. O suporte ao perfil B-T não é uma certificação de conformidade nem de validade jurídica; um validador independente faz essa determinação.

SignatureLevel::PAdES_B_T está disponível no Core: SignatureLevel::PAdES_B_T->requiresTimestamp() é true, ->isAvailableInEnvironment() é true e ->requiresDss() é false — o B-T não traz consigo um Document Security Store. B-T ≠ B-LT ≠ B-LTA: um carimbo de tempo de assinatura não adiciona material de validação nem um carimbo de tempo de arquivamento; esses são níveis separados e superiores, não produzidos aqui.

O diagrama abaixo mostra o fluxo B-B e depois B-T na ordem usada pelo motor. O ByteRange é calculado somente após todo o arquivo ser gravado, de modo que os deslocamentos finais não podem alterar os bytes que estão sendo hasheados. O B-T então anexa um token RFC 3161 como atributo não assinado, deixando intocado o digest assinado do B-B.

RFC 3161 TSANextPDF DigitalSignerRFC 3161 TSANextPDF DigitalSignerReserve fixed-width /Contents slotand /ByteRange placeholderByteRange covers the whole fileexcluding the /Contents valuePAdES B-B completeB-T = B-B + 1 timestampB-B signed digest unchangedalt[level == PAdES B-T]Callersign — level B-B or B-T1Write the complete PDF incl. xref + EOF2Compute the two real ByteRange offsets3Hash the two concatenated segments4Build CMS SignedData with signed attrs5Hash the SignerInfo signature value — message imprint6TimeStampReq — message imprint + fresh nonce7TimeStampToken — signed, echoes imprint + nonce8Verify token — status, nonce, imprint, signature, time9Embed token in SignerInfo.unsignedAttrs10Signed PDF — /Contents = DER CMS SignedData11Caller
Diagram

O ponto de entrada de configuração é Document::setSignature(CertificateInfo $certInfo, SignatureLevel $level = SignatureLevel::PAdES_B_B, ?TsaClient $tsaClient = null). Essa chamada registra a intenção de assinar no documento. O motor de assinatura Core PAdES (NextPDF\Security\Signature\DigitalSigner) produz a assinatura criptográfica. Como a suíte de integração exercita esse motor e o exemplo executável o aciona diretamente, a saída é um objeto CMS real e analisável. SignatureLevel::PAdES_B_T exige um TsaClient não nulo; construir um assinador B-T sem um lança uma SignatureException.

API de alto nível — uma chamada, saída assinada

Seção intitulada “API de alto nível — uma chamada, saída assinada”

O caminho mais rápido é a API de alto nível: configure a assinatura no documento e depois serialize. Por baixo dos panos, ela executa o mesmo motor Core PAdES (DigitalSigner). Isso é uma fina camada de conveniência sobre o passo a passo de nível mais baixo abaixo, não um caminho de código separado.

<?php
declare(strict_types=1);
use NextPDF\Core\Document;
use NextPDF\Security\Signature\CertificateInfo;
use NextPDF\Security\Signature\SignatureLevel;
use NextPDF\Security\Timestamp\TsaClient;
$certInfo = CertificateInfo::fromPkcs12(
p12Path: __DIR__ . '/signer.p12',
password: 'p12-passphrase',
);
// PAdES B-B end to end: configure, then serialise.
$doc = Document::createStandalone();
$doc->addPage();
$doc->setFont('helvetica', '', 12);
$doc->cell(0, 10, 'Signed end to end.', newLine: true);
$doc->setSignature(certInfo: $certInfo, level: SignatureLevel::PAdES_B_B);
$doc->save(__DIR__ . '/signed.pdf'); // or output() to stream, getPdfData() for bytes
// PAdES B-T: pass a TsaClient on the same call — one RFC 3161
// signature-time-stamp is added (see the TsaClient hardening notes below).
$doc->setSignature(
certInfo: $certInfo,
level: SignatureLevel::PAdES_B_T,
tsaClient: $tsa,
);
$doc->save(__DIR__ . '/signed-bt.pdf');

Assim como output() e getPdfData(), save() grava a entrada /Contents como um CMS SignedData codificado em DER sob o SubFilter ETSI.CAdES.detached (ISO 32000-2 §12.8, §12.7.5.5; RFC 5652). A saída é verificável por CMS — um objeto CMS SignedData bem formado que um analisador CMS pode ler — o que não é o mesmo que conformidade com o perfil baseline ETSI EN 319 142-1 ou validade jurídica; um validador independente faz essas determinações (consulte a ressalva U-1 acima). Para B-T, a chamada de alto nível adiciona exatamente o único RFC 3161 signature-time-stamp descrito na visão geral conceitual; passar o TsaClient é a única diferença em relação ao B-B.

Use o passo a passo de nível mais baixo do DigitalSigner abaixo quando precisar de controle direto sobre o algoritmo, os dados do intervalo de bytes ou o SignatureResult.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Security\Signature\CertificateInfo;
use NextPDF\Security\Signature\DigitalSigner;
use NextPDF\Security\Signature\SignatureAlgorithm;
use NextPDF\Security\Signature\SignatureLevel;
$certInfo = CertificateInfo::fromPkcs12(
p12Path: __DIR__ . '/signer.p12',
password: 'p12-passphrase',
);
// PAdES B-B — a CMS SignedData, no timestamp.
$signer = new DigitalSigner(
certInfo: $certInfo,
level: SignatureLevel::PAdES_B_B,
algorithm: SignatureAlgorithm::Pkcs1v15,
);
$result = $signer->sign($byteRangeData);
echo $result->hasTimestamp() ? "B-T\n" : "B-B (no timestamp)\n";

Este programa autocontido roda sob o harness do cookbook. Ele espelha examples/36-sign-pades-b-b-and-b-t.php. Ele cria um documento, configura-o para uma assinatura PAdES e então assina em B-B e novamente em B-T com um cliente TSA. Em produção, aponte o TsaClient para um endpoint RFC 3161 real sobre um cliente PSR-18 endurecido: um cliente HTTP voltado à segurança que fixa (pin) o SubjectPublicKeyInfo (SPKI) do TSA e resolve o Domain Name System (DNS) com segurança. Para manter este programa offline e determinístico, ele injeta o cliente TSA falso de suporte a testes do repositório. O cliente TSA falso retorna um RFC 3161 TimeStampResp estruturalmente válido.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
use NextPDF\Security\Signature\CertificateInfo;
use NextPDF\Security\Signature\DigitalSigner;
use NextPDF\Security\Signature\SignatureAlgorithm;
use NextPDF\Security\Signature\SignatureLevel;
use NextPDF\Security\Timestamp\TsaClient;
use NextPDF\Tests\Support\FakeTsaHttpClient;
// In your application, build CertificateInfo from your own signing material:
// CertificateInfo::fromPkcs12($p12Path, $passphrase) — a .p12/.pfx bundle
// CertificateInfo::fromFiles($certPem, $keyPem, $pass) — separate PEM files
// This program uses the repository RSA-2048 test fixtures so it is offline.
$certDir = __DIR__ . '/tests/Fixtures/Certificates';
$certPath = $certDir . '/test-rsa-2048-cert.pem';
$keyPath = $certDir . '/test-rsa-2048-key.pem';
if (!is_file($certPath) || !is_file($keyPath)) {
fwrite(STDERR, "Certificate fixtures absent. Run tests/Fixtures/Certificates/generate.sh\n");
exit(1);
}
$certInfo = new CertificateInfo(
certificate: (string) file_get_contents($certPath),
privateKey: (string) file_get_contents($keyPath),
);
// Build the document and record the signing intent on it. The ByteRange
// digest input is the document bytes with the /Contents placeholder
// excluded (ISO 32000-2 §12.8); getPdfData() yields the bytes to hash.
$doc = Document::createStandalone();
$doc->setTitle('Signed Invoice 2026-0042');
$doc->setAuthor('NextPDF Cookbook');
$doc->addPage();
$doc->setFont('helvetica', '', 12);
$doc->cell(0, 10, 'This document is configured for a PAdES signature.', newLine: true);
$doc->setSignature(certInfo: $certInfo, level: SignatureLevel::PAdES_B_B);
$byteRangeData = $doc->getPdfData();
// --- PAdES B-B: a CMS SignedData, no timestamp ---
$bb = (new DigitalSigner(
certInfo: $certInfo,
level: SignatureLevel::PAdES_B_B,
algorithm: SignatureAlgorithm::Pkcs1v15,
))->sign($byteRangeData);
// --- PAdES B-T: B-B + one RFC 3161 signature-time-stamp ---
// In production, build the TsaClient with your TSA endpoint and a hardened
// PSR-18 client (use the security-aware HTTP client for SSRF/DNS pinning):
// $tsa = new TsaClient(
// tsaUrl: 'https://tsa.example.com/timestamp',
// httpClient: $hardenedPsr18Client,
// );
// Here the offline fake TSA client keeps the program network-free.
$tsa = new TsaClient(
tsaUrl: 'https://tsa.example.com/timestamp',
httpClient: new FakeTsaHttpClient(),
);
$bt = (new DigitalSigner(
certInfo: $certInfo,
tsaClient: $tsa,
level: SignatureLevel::PAdES_B_T,
algorithm: SignatureAlgorithm::Pkcs1v15,
))->sign($byteRangeData);
// B-T = B-B + a single timestamp token. The B-B signed digest is unchanged;
// $bt->timestampToken holds the DER-encoded RFC 3161 token.
printf("PAdES B-B CMS: %d bytes, timestamp=%s\n", $bb->getSize(), $bb->hasTimestamp() ? 'yes' : 'no');
printf(
"PAdES B-T CMS: %d bytes, timestamp=%s (%d-byte RFC 3161 token)\n",
$bt->getSize(),
$bt->hasTimestamp() ? 'yes' : 'no',
strlen($bt->timestampToken),
);
echo "B-T = B-B + one RFC 3161 signature-time-stamp (unsigned attribute).\n";
// The harness sets NEXTPDF_COOKBOOK_OUTPUT and runs this script under the
// semantic profile (the signed CMS/timestamp bytes are inherently
// non-reproducible and are asserted by the PHPUnit harness, not a byte hash).
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT');
file_put_contents($out !== false && $out !== '' ? $out : __DIR__ . '/signed-invoice.pdf', $byteRangeData);

STDOUT esperado (os tamanhos variam conforme o certificado e o token do TSA):

PAdES B-B CMS: <n> bytes, timestamp=no
PAdES B-T CMS: <n> bytes, timestamp=yes (<m>-byte RFC 3161 token)
B-T = B-B + one RFC 3161 signature-time-stamp (unsigned attribute).

Ressalva U-1 (colocada junto à declaração de produção de B-T). O NextPDF não afirma nenhuma certificação independente ETSI EN 319 142-1 para PAdES B-T. A EN 319 142-1 não está no corpus de verificação; o requisito de B-T signature-time-stamp foi verificado em relação a ETSI EN 319 122-1 §5.3 em conjunto com RFC 3161, RFC 5652, RFC 5816 e ISO 32000-2 §12.8. O suporte ao perfil B-T não é uma certificação de conformidade nem de validade jurídica; um validador independente faz essa determinação.

  • B-T sem um cliente TSA. Construir um B-T DigitalSigner sem nenhum TsaClient lança SignatureException (o TSA é obrigatório para B-T). Verifique a configuração do TSA antes de assinar.
  • Acessibilidade do TSA. O B-T realiza um ida e volta RFC 3161 ao vivo por assinatura. Uma indisponibilidade do TSA significa nenhuma assinatura B-T. Use um disjuntor (circuit breaker) e um acordo de nível de serviço (SLA) do TSA apropriado à sua vazão; o TsaClient aceita um disjuntor.
  • Endurecimento do cliente HTTP do TSA. Aponte o TsaClient para um cliente PSR-18 que fixa (pin) o SubjectPublicKeyInfo (SPKI, formato RFC 7469) do TSA e resolve o Domain Name System (DNS) com segurança; TsaClient::extractPublicKeyPin() deriva o pin a partir do certificado do TSA.
  • B-T não é B-LT/B-LTA. Um carimbo de tempo de assinatura não incorpora material de validação (certificados, Online Certificate Status Protocol (OCSP), lista de revogação de certificados (CRL) (CRL)) nem um carimbo de tempo de arquivamento. Esses são os níveis B-LT/B-LTA e não são produzidos por esta receita.
  • Conflito de linearização. enableLinearization() e uma assinatura configurada são mutuamente exclusivos — qualquer uma das chamadas lança InvalidConfigException quando a outra já está definida.
  • Chaves em HSM. Para uma chave mantida em módulo de segurança de hardware (HSM), construa CertificateInfo com CertificateInfo::fromHsm(); a chave privada nunca entra na memória do processo. O contrato do assinador PKCS#11 é Core; um provedor funcional é Premium.

Uma assinatura B-B é uma operação CMS local. O B-T adiciona um ida e volta HTTP RFC 3161 síncrono ao TSA por assinatura. Considere a latência e os limites de taxa do TSA em cargas de trabalho em lote. Use um TsaClient protegido por disjuntor.

Uma assinatura produzida não é, por si só, uma assinatura confiável. A verificação de uma assinatura depende do certificado, da sua âncora de confiança e da política do verificador, que ficam fora desta biblioteca. A criptografia protege a confidencialidade, não a integridade; a assinatura protege a integridade e a autenticidade, não a confidencialidade. Trate a guarda das chaves como o risco principal: uma chave em software na memória do processo é apenas tão segura quanto o host.

A operação de assinatura roda em processo; os bytes do documento e a chave privada não deixam o host, exceto pelo ida e volta B-T ao TSA, que envia apenas a impressão da mensagem (um hash do valor da assinatura), nunca o conteúdo do documento (RFC 3161 §2.4.1 MessageImprint). Nenhum texto do documento nem informação de identificação pessoal (PII) é transmitido ao TSA. Escolha um TSA cuja jurisdição corresponda à sua política de residência de dados.

DigitalSigner aceita um logger PSR-3 opcional. Ele registra o algoritmo e o nível, não material de chave nem bytes de assinatura. Os parâmetros password em CertificateInfo e TsaClient são marcados com #[SensitiveParameter], de modo que as senhas são redigidas dos rastros de pilha. Não registre o SignatureResult::$cmsSignedData nem o $timestampToken.

Considerados: entrada adulterada após a assinatura (detectada pelo digest de intervalo de bytes), comprometimento de chave (fora do escopo da biblioteca porque a guarda da chave é responsabilidade do integrador), falsificação do TSA (mitigada por fixação (pinning) de SPKI no cliente HTTP do TSA) e rebaixamento entre níveis (o enum de nível é explícito; o motor não rebaixa silenciosamente B-T para B-B). Não afirmado: ausência de vulnerabilidades nem que qualquer assinatura resultante seja juridicamente válida.

As primitivas de assinatura são fornecidas pelo OpenSSL. Em uma build do OpenSSL validada pelo Federal Information Processing Standards (FIPS), as operações RSA/ECDSA e SHA-256 são executadas por meio do provedor FIPS; o NextPDF não afirma, por si só, a validação FIPS. CryptoCapabilities relata as primitivas disponíveis no host; verifique a cadeia de provedores do OpenSSL na sua implantação.

DeclaraçãoEspecificaçãoCláusulareference_id
O digest de intervalo de bytes cobre o arquivo e exclui o valor da assinatura.ISO 32000-2§12.8.1
Contents contém CMS SignedData em DER; o Contents de um document-timestamp contém um TimeStampToken.ISO 32000-2§12.8.1
Contents é uma string hexadecimal preenchida sobre o digest de intervalo de bytes.ISO 32000-2§12.8.1
A impressão do signature-time-stamp é o hash dos octetos do valor da assinatura do SignerInfo (sem tag/length ASN.1).ETSI EN 319 122-1§5.3
O valor do signature-time-stamp é um SignatureTimeStampToken.ETSI EN 319 122-1§6
MessageImprint ::= SEQUENCE { hashAlgorithm, hashedMessage }.RFC 3161§2.4.1
A impressão do carimbo de tempo de assinatura é o hash do campo de assinatura do SignerInfo; SignatureTimeStampToken ::= TimeStampToken.RFC 3161Apênd. A
id-aa-timeStampToken é o OID 1.2.840.113549.1.9.16.2.14.RFC 3161Apênd. A
SignerInfo carrega unsignedAttrs [1] IMPLICIT UnsignedAttributes OPTIONAL.RFC 5652§5.3
Os atributos não assinados não são protegidos pela assinatura; o digest assinado do B-B permanece inalterado.RFC 5652§5.4
RFC 5816 atualiza a RFC 3161; ESSCertIDv2 identifica o certificado do TSA sem SHA-1.RFC 5816§1

Esta receita descreve como o NextPDF produz uma assinatura B-B e uma B-T. Ela não afirma que qualquer assinatura resultante seja juridicamente válida nem que a conformidade PAdES seja atendida; um validador independente faz essas determinações.

PAdES B-LT e B-LTA (material de validação do DSS e o laço de carimbo de tempo de arquivamento) e a custódia de chaves em HSM PKCS#11 são oferecidos nas edições Pro e Enterprise. Esta receita cobre apenas B-B e B-T; os níveis superiores são capacidades distintas, verificadas separadamente, e estão fora do escopo aqui.