Pular para o conteúdo

Inspecione uma assinatura existente e entenda o limite da confiança

Use o inspetor do Core para detectar se um PDF contém um dicionário de assinatura. O inspetor roda offline e não usa o sidecar Spectrum. Esta receita também deixa claro onde fica o limite de confiança: detectar uma assinatura não é o mesmo que verificá-la. A verificação criptográfica, a validação do caminho de confiança e a checagem de revogação ficam no Premium ou em ferramentas externas.

Terminal window
composer require nextpdf/core:^3

Uma assinatura de PDF é um campo de assinatura cujo valor é um dicionário de assinatura (ISO 32000-2 §12.7.4). A entrada Contents do dicionário contém Cryptographic Message Syntax (CMS) SignedData codificada em DER (ISO 32000-2 §12.8.1). O fallback Quick do Inspector detecta a presença dessa estrutura ao varrer marcadores de assinatura. Ele não analisa o CMS, não recalcula o digest do intervalo de bytes (que exclui o valor da assinatura — ISO 32000-2 §12.8.1), não valida a cadeia de certificados nem verifica a revogação.

Chame new Inspector() e, em seguida, ->inspect(string $pdfData, InspectConfig $config). Use InspectConfig::quick() para o fallback PHP offline. InspectDepth::Standard/Full exigem o sidecar Spectrum e falham de forma fechada (INSPECT-SIDECAR-001) quando ele está ausente. O resultado é um objeto de valor InspectResult. Para este fluxo de trabalho, use $hasSigned para indicar a presença da assinatura, além de $isEncrypted e $pdfVersion.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Inspect\InspectConfig;
use NextPDF\Inspect\Inspector;
$pdfData = file_get_contents(__DIR__ . '/incoming.pdf');
if ($pdfData === false || $pdfData === '') {
fwrite(STDERR, "Cannot read incoming.pdf\n");
exit(1);
}
$result = (new Inspector())->inspect($pdfData, InspectConfig::quick());
// hasSigned reports the PRESENCE of a signature dictionary.
// It does NOT mean the signature verifies.
echo $result->hasSigned
? "A signature is present — NOT verified.\n"
: "No signature found.\n";

Este programa autocontido roda no harness do cookbook. Ele espelha examples/37-inspect-existing-signature.php. Ele inspeciona uma amostra do corpus conhecida por estar assinada e um documento sem assinatura recém-criado, para que você veja as duas ramificações do flag de presença. Em seguida, encaminha o veredito para a próxima etapa. A presença é uma entrada de roteamento, nunca um veredito de confiança. O arquivo é passado a um verificador criptográfico (Pro ou externo). Ele não é tratado como confiável aqui.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
use NextPDF\Inspect\InspectConfig;
use NextPDF\Inspect\Inspector;
$inspector = new Inspector();
// --- A known-signed input ---
// The repository corpus carries synthetic PAdES samples. In your
// application this is simply the incoming PDF you received.
$signedPath = __DIR__ . '/tests/Corpus/pades/pades-b-b-bytepattern-synthetic.pdf';
if (is_file($signedPath)) {
$signed = (string) file_get_contents($signedPath);
$r = $inspector->inspect($signed, InspectConfig::quick());
echo "Signed sample:\n";
echo ' Signature present : ' . ($r->hasSigned ? 'yes' : 'no') . "\n";
echo ' Encrypted : ' . ($r->isEncrypted ? 'yes' : 'no') . "\n";
echo ' PDF version : ' . ($r->pdfVersion ?? 'unknown') . "\n";
echo " Verdict : presence detected — NOT verified.\n";
if ($r->hasSigned) {
// Presence detected. This is routing input, not a trust verdict.
// Hand the file to a cryptographic verifier (Pro or external)
// before relying on it. (Pseudo-queue shown; wire your own.)
// $verifierQueue->enqueue($signed);
echo " Next step : run a cryptographic verifier before trusting it.\n";
}
} else {
echo "Signed corpus sample absent; skipping the signed branch.\n";
}
// --- A known-unsigned input ---
$unsigned = Document::createStandalone();
$unsigned->setTitle('Unsigned sample');
$unsigned->addPage();
$unsigned->setFont('helvetica', '', 12);
$unsigned->cell(0, 10, 'This document carries no signature.', newLine: true);
$unsignedBytes = $unsigned->getPdfData();
$ru = $inspector->inspect($unsignedBytes, InspectConfig::quick());
echo "Unsigned sample:\n";
echo ' Signature present : ' . ($ru->hasSigned ? 'yes' : 'no') . "\n";
// The harness sets NEXTPDF_COOKBOOK_OUTPUT and runs this script under the
// semantic profile; emit the unsigned document to the side-channel.
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT');
file_put_contents($out !== false && $out !== '' ? $out : __DIR__ . '/inspected.pdf', $unsignedBytes);

STDOUT esperado (a ramificação assinada é pulada se a amostra do corpus estiver ausente):

Signed sample:
Signature present : yes
Encrypted : no
PDF version : <version>
Verdict : presence detected — NOT verified.
Next step : run a cryptographic verifier before trusting it.
Unsigned sample:
Signature present : no
  • Presença não é validade. $hasSigned indica que existe um dicionário de assinatura. Ele não verifica a estrutura do CMS, o digest do intervalo de bytes, o certificado de assinatura, a cadeia ou a revogação. Um arquivo adulterado ainda pode resultar em hasSigned = true. Nunca trate a presença como prova de integridade ou autoria.
  • O que a verificação completa exige. Uma decisão completa recalcula o digest do intervalo de bytes (ISO 32000-2 §12.8.1), valida o CMS SignedData, constrói e verifica o caminho X.509 até uma âncora confiável e checa a revogação por meio do Online Certificate Status Protocol (OCSP) ou de uma certificate revocation list (CRL). Um carimbo de tempo de assinatura, quando presente, também é verificado contra o próprio imprint sobre os octetos do valor da assinatura (ETSI EN 319 122-1 §5.3). Essas operações são executadas por trás dos contratos de assinatura. As implementações de produção são fornecidas nas edições Pro e Enterprise. Um validador externo é o outro caminho com suporte.
  • Profundidade da inspeção. InspectConfig::quick() é a única profundidade que roda sem o sidecar Spectrum. Standard/Full lançam INSPECT-SIDECAR-001 quando o sidecar está indisponível.
  • Entrada vazia. Uma string vazia lança uma exceção de inspeção com “PDF data must not be empty”. Proteja a leitura.
  • Várias assinaturas / carimbos de tempo. O flag de presença não conta as assinaturas nem distingue uma assinatura de aprovação de um carimbo de tempo de documento (que também é transportado em unsignedAttrs conforme a RFC 5652 §5.3). Use um verificador dedicado quando a contagem ou o veredito por assinatura importar.

O fallback Quick realiza uma varredura limitada nos bytes do documento. Ele não analisa o grafo de objetos completo. Use-o para triar rapidamente arquivos recebidos antes de encaminhá-los a um verificador mais pesado.

O inspetor é uma ferramenta de triagem, não um limite de confiança. Um hasSigned positivo nunca deve, por si só, embasar uma decisão de confiança.

A inspeção é executada inteiramente no processo. Nenhum byte do documento sai do host. O fallback Quick lê apenas marcadores estruturais, não o texto do documento; por isso, não extrai nem transmite informações de identificação pessoal (PII).

Inspector aceita um logger PSR-3 opcional. Ele registra o caminho escolhido (“Spectrum unavailable, using PHP fallback”), não o conteúdo do documento. Não registre os bytes do PDF inspecionado nem o InspectResult inteiro se o documento for sensível.

Considerados: um arquivo adulterado que apresenta um dicionário de assinatura sintaticamente válido (o inspetor informa a presença; ele explicitamente não afirma a integridade) e um arquivo sem assinatura (corretamente informado como ausente). Não se afirma: que qualquer assinatura detectada seja criptograficamente válida, confiável ou não revogada — isso é trabalho do verificador.

O fallback Quick não realiza nenhuma criptografia; portanto, o modo Federal Information Processing Standards (FIPS) não é relevante para esta receita. A verificação criptográfica (Premium/externa) é onde a cadeia de provedores FIPS importa.

DeclaraçãoEspecificaçãoCláusulareference_id
O valor de um campo de assinatura é um dicionário de assinatura.ISO 32000-2§12.7.4
Contents contém CMS SignedData em DER; um Contents de carimbo de tempo de documento contém um TimeStampToken.ISO 32000-2§12.8.1
A verificação recalcula o digest sobre o intervalo de bytes, excluindo o valor da assinatura.ISO 32000-2§12.8.1
Um imprint de carimbo de tempo de assinatura é feito sobre os octetos do valor da assinatura do SignerInfo.ETSI EN 319 122-1§5.3
Um carimbo de tempo é transportado em unsignedAttrs do SignerInfo.RFC 5652§5.3

Esta receita detecta uma assinatura. Ela não afirma que qualquer assinatura seja válida, confiável ou não revogada. Essa decisão cabe a um verificador criptográfico.

A verificação criptográfica de CMS, a validação do caminho X.509 e a checagem de revogação por OCSP/CRL são fornecidas por trás dos contratos de assinatura nas edições Pro e Enterprise. O inspetor do Core cobre apenas a detecção de presença.