Ir al contenido
getnextpdf.com

Enterprise edición

Validación de firmas por lotes

NextPDF Enterprise valida firmas digitales en muchos documentos PDF con una sola llamada. NextPDF\Enterprise\Signature\BatchSignatureValidator::validate() toma una lista de documentos y devuelve un BatchValidationReport. Cada firma pasa por la misma tubería a prueba de fallos: autenticación criptográfica CMS sobre el rango de bytes firmado, validación de la cadena de certificados con anclas de confianza y comprobación de revocación OCSP/CRL. El informe incluye detalle por documento y por firma —CertChainStatus, RevocationStatus, TimestampStatus— de modo que las herramientas de cumplimiento pueden volver a derivar cada veredicto a partir de la evidencia registrada.

El modelo de veredicto es deliberadamente estricto. Una firma es Valid únicamente cuando toda la evidencia queda establecida de forma afirmativa. La ausencia de evidencia de revocación produce Indeterminate, nunca Valid. Esta página cubre el orquestador por lotes y sus tipos de resultado. La verificación AdES de documento único se documenta en Verificación de firmas. La incrustación de material de validación a largo plazo se documenta en Archivo.

Esta capacidad se distribuye en NextPDF Enterprise (nextpdf/enterprise) y se activa con un sobre de licencia de nivel Enterprise. Un despliegue sin esa titularidad no carga las clases de la capacidad. Compare ediciones y obtenga una licencia.

Ventana de terminal
composer require nextpdf/enterprise

El metapaquete nextpdf/premium también resuelve el paquete Enterprise. La activación usa su sobre de licencia de Enterprise; consulte Licencias y activación. Los tipos por lotes se cargan automáticamente bajo NextPDF\Enterprise\Signature. No se requiere ninguna extensión de PHP más allá de la base del motor.

Una llamada a validate() procesa una lista de valores DocumentSignatureInput. Cada entrada incluye un identificador de documento, los bytes PDF sin procesar y, opcionalmente, anclas de confianza codificadas en PEM. El validador extrae los diccionarios de firma de cada documento y ejecuta tres etapas por firma.

Etapa 1 — autenticación criptográfica. El blob CMS/PKCS#7 separado de /Contents se verifica sobre los bytes que cubre /ByteRange. El verificador vuelve a calcular por sí mismo el resumen del contenido y lo compara con el atributo firmado messageDigest. Nunca confía en un resumen suministrado por el productor (RFC 5652 §5.6). El valor de la firma debe verificarse y el certificado de firma debe estar vinculado al CMS. Un /Contents o /ByteRange ausente o malformado, un CMS no analizable, un resumen que no coincide o una comprobación de firma fallida fallan todos a prueba de fallos. Una firma que se verifica bajo SHA-1 se trata como débil y nunca constituye una aprobación completa.

Etapa 2 — validación de la cadena y anclaje de confianza. La cadena del firmante recuperada del CMS se valida como la ruta de certificación prospectiva. Los trustedCerts que usted suministra son la entrada de ancla de confianza, en el sentido de RFC 5280 §6.1.1: el extremo de la cadena debe coincidir con un ancla suministrada por huella DER SHA-256. Una cadena estructuralmente coherente cuyo extremo no sea un ancla configurada nunca se reporta como confiable. Sin anclas utilizables, solo se reporta el veredicto estructural, y CertChainStatus::$trusted permanece en false.

Etapa 3 — revocación. La revocación se ejecuta sobre la cadena recuperada tras la autenticación, reflejando el modelo de ETSI EN 319 102-1 donde la comprobación de revocación sigue a la validación de ruta exitosa (cláusula 5.2.6.2). OCSP es primario: solo cuenta una respuesta verificada criptográficamente, como Good o Revoked. La ruta CRL es la alternativa y da fe de la frescura de la lista. Cuando no se configura ningún cliente, el estado es unavailable.

El veredicto por firma es un SignatureValidationStatus. La taxonomía refleja el modelo de estados de ETSI EN 319 102-1 (TOTAL-PASSED / TOTAL-FAILED / INDETERMINATE) con granularidad por firma:

EvidenciaVeredicto
Certificado confirmado como revocadoInvalid (decisivo, con independencia de otras comprobaciones)
Falló la autenticación CMS, no se recuperó material del firmanteError
Falló la autenticación CMS, hay material del firmante presenteInvalid
Autenticado, pero la cadena no validaInvalid (o Error sin cadena)
Autenticado y con cadena válida, pero sin ancla de confianza confirmadaIndeterminate
Autenticado, con cadena válida y confiable, pero sin no-revocación concluyenteIndeterminate
Todo lo anterior establecido de forma afirmativaValid

La regla de no-revocación concluyente. «No probado como revocado» no es lo mismo que «probado como no revocado». Un veredicto Valid requiere al menos un resultado de revocación Good. Una respuesta OCSP verificada como correcta es la forma concluyente: afirma el estado propio del certificado del firmante. Una CRL aceptada criptográficamente y fresca también satisface la condición en esta implementación, pero solo como una atestación de frescura e integridad: la ruta no analiza las entradas por número de serie, de modo que no proporciona ninguna garantía de revocación por número de serie ni jamás un veredicto positivo de revoked. Configure OCSP allí donde importe la detección positiva de revocación: un despliegue solo con CRL no expondrá un certificado revocado como Invalid. Cuando los resultados de OCSP y de CRL son ambos Unknown o Unavailable, el estado de revocación queda indeterminado, y el veredicto es Indeterminate. Esto sigue a ETSI EN 319 102-1: la información de estado de revocación no disponible resulta en INDETERMINATE, nunca en una aprobación (cláusula 5.1.3, TRY_LATER). Se trata de un endurecimiento del comportamiento en 3.1.0 con impacto en la compatibilidad hacia atrás: versiones anteriores podían reportar Valid sin evidencia de revocación concluyente. Los despliegues que no configuran ningún cliente OCSP o CRL ahora suelen ver Indeterminate donde antes veían Valid.

Dos límites enmarcan esta capacidad con honestidad. Primero, el validador por lotes no evalúa los tokens de sello de tiempo incrustados: TimestampStatus en los resultados por lotes es siempre el estado de ausencia. La evaluación de sellos de tiempo RFC 3161 corresponde a la verificación de documento único; consulte Verificación de firmas. Segundo, esta página trata de validación de solo lectura. La incrustación de material DSS/VRI para validez a largo plazo es la capacidad de Archivo.

La decisión de fondo es un productor de veredictos a prueba de fallos. Valid se acuña únicamente a partir de evidencia afirmativa en los tres ejes: autenticación criptográfica, una cadena con ancla de confianza y no-revocación concluyente. Cualquier elemento no establecido degrada a Indeterminate en lugar de recurrir por defecto a una aprobación, que es la postura de EN 319 102-1 ante la falta de material de revocación. El rendimiento por lotes nunca compensa el rigor: la capa por lotes es orquestación sobre el mismo verificador CMS auditado que se usa para un solo documento, de modo que una ejecución de 1.000 documentos aplica idéntica criptografía. El informe también separa la evidencia del veredicto —CertChainStatus y RevocationStatus registran las entradas sobre las que descansa cada veredicto— para que un auditor pueda volver a derivarlo más adelante.

Trasfondo de diseño: Firmar a escala, sin concesiones.

Todos los símbolos siguientes son API pública en nextpdf/enterprise 3.1.0.

final class BatchSignatureValidator
{
public function __construct(
?SignatureExtractor $extractor = null,
?CertificateChainValidator $chainValidator = null,
private readonly ?OcspClient $ocspClient = null,
private readonly ?CrlFetcher $crlFetcher = null,
?CmsSignatureDataExtractor $cmsExtractor = null,
private readonly ClockInterface $clock = new SystemClock(),
)
public function validate(array $inputs): BatchValidationReport
}

Lanza o falla con: validate() lanza \InvalidArgumentException si la lista de entrada está vacía, y \OverflowException cuando el lote supera los 1.000 documentos. Un documento que no sea un PDF analizable no lanza; se convierte en un resultado Error por documento. El $clock es un Psr\Clock\ClockInterface PSR-20 usado para la decisión de frescura de la CRL, de modo que los veredictos son deterministas bajo un reloj de prueba congelado.

final readonly class DocumentSignatureInput
{
public string $documentId;
public function __construct(
string $documentId,
public string $pdfData,
public array $trustedCerts = [],
)
}

Lanza o falla con: \InvalidArgumentException si $documentId es una cadena vacía. $trustedCerts es una lista de certificados de ancla de confianza codificados en PEM.

final readonly class BatchValidationReport
{
public function __construct(
public array $documents,
public int $totalDocuments,
public int $totalSignatures,
public int $totalValid,
public int $totalInvalid,
public float $durationMs,
)
public function allValid(): bool
public function hasDocumentsWithoutSignatures(): bool
public function toJson(?CertPiiGuard $piiGuard = null): string
}

Lanza o falla con: toJson() lanza \JsonException si la codificación falla. allValid() es true solo cuando hay firmas y ninguna es no válida. Por defecto, toJson() aplica un NextPDF\Enterprise\Signature\Eidas\CertPiiGuard con privacidad por defecto, que enmascara el nombre del firmante, el emisor raíz, el nombre de la TSA y los diagnósticos de problemas de cadena; consulte Niveles de garantía eIDAS para conocer la API del guard.

DocumentValidationResult y DocumentValidationStatus

Sección titulada «DocumentValidationResult y DocumentValidationStatus»
final readonly class DocumentValidationResult
{
public function __construct(
public string $documentId,
public DocumentValidationStatus $status,
public array $signatures,
public int $validCount,
public int $invalidCount,
)
public function hasSignatures(): bool
public function totalSignatures(): int
}
enum DocumentValidationStatus: string
{
case AllValid = 'all_valid';
case SomeInvalid = 'some_invalid';
case AllInvalid = 'all_invalid';
case NoSignatures = 'no_signatures';
case Error = 'error';
}

Lanza o falla con: nada. Objeto de valor inmutable y enum respaldado.

SignatureValidationResult y SignatureValidationStatus

Sección titulada «SignatureValidationResult y SignatureValidationStatus»
final readonly class SignatureValidationResult
{
public function __construct(
public SignatureValidationStatus $status,
public CertChainStatus $certChain,
public TimestampStatus $timestamp,
public RevocationStatus $revocation,
public string $signer,
public string $level = '',
public string $subFilter = '',
public string $reason = '',
)
public function isValid(): bool
}
enum SignatureValidationStatus: string
{
case Valid = 'valid';
case Invalid = 'invalid';
case Indeterminate = 'indeterminate';
case Error = 'error';
}

Lanza o falla con: nada. $signer es el sujeto del certificado verificado por CMS cuando la autenticación fue satisfactoria, y de lo contrario la cadena vacía. $level es una etiqueta derivada de SubFilter (por ejemplo B-B para ETSI.CAdES.detached), no una determinación de conformidad AdES.

final readonly class CertChainStatus
{
public function __construct(
public bool $valid,
public bool $trusted,
public int $chainLength,
public string $rootIssuer,
public array $issues = [],
)
public function hasIssues(): bool
}

Lanza o falla con: nada. $trusted se establece solo ante un acierto confirmado de pertenencia a un ancla de confianza, nunca a partir de la no vacuidad de la lista de anclas.

final readonly class RevocationStatus
{
public function __construct(
public RevocationCheckResult $ocspStatus,
public RevocationCheckResult $crlStatus,
public bool $isRevoked,
public ?DateTimeImmutable $revocationDate = null,
)
public static function unavailable(): self
public function hasConclusiveGood(): bool
}
enum RevocationCheckResult: string
{
case Good = 'good';
case Revoked = 'revoked';
case Unknown = 'unknown';
case Unavailable = 'unavailable';
}

Lanza o falla con: nada de los miembros mostrados. La clase también expone fábricas estáticas con evidencia comprobada (good(), revoked(), fromResults()), que lanzan \InvalidArgumentException cuando el estado reclamado contradice la evidencia de OCSP/CRL: un resultado revocado nunca puede acuñarse como no revocado, ni viceversa. hasConclusiveGood() es true solo para un estado no revocado en el que al menos una comprobación sea Good.

final readonly class TimestampStatus
{
public function __construct(
public bool $present,
public bool $valid,
public ?DateTimeImmutable $timestampTime = null,
public string $tsaName = '',
public array $issues = [],
)
public static function absent(): self
}

Lanza o falla con: nada. En los resultados por lotes este es siempre el estado absent(); consulte Casos límite y trampas.

Valide un documento y lea el informe. Este ejemplo usa un PDF sin firmar, de modo que la salida es determinista.

batch-quick-start.php
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use NextPDF\Enterprise\Signature\BatchSignatureValidator;
use NextPDF\Enterprise\Signature\DocumentSignatureInput;
// A minimal, unsigned PDF: the validator reports it as no_signatures.
$unsigned = "%PDF-1.7\n1 0 obj\n<< /Type /Catalog >>\nendobj\ntrailer\n<< /Root 1 0 R >>\n%%EOF\n";
$validator = new BatchSignatureValidator();
try {
$report = $validator->validate([
new DocumentSignatureInput(documentId: 'doc-001', pdfData: $unsigned),
]);
} catch (\InvalidArgumentException $e) {
// Empty input list, or an empty documentId.
echo 'Rejected: ' . $e->getMessage() . "\n";
exit(1);
}
echo 'Documents: ' . $report->totalDocuments . "\n";
echo 'Signatures: ' . $report->totalSignatures . "\n";
foreach ($report->documents as $doc) {
echo $doc->documentId . ': ' . $doc->status->value . "\n";
}
echo 'All valid: ' . ($report->allValid() ? 'yes' : 'no') . "\n";
echo 'Unsigned documents: ' . ($report->hasDocumentsWithoutSignatures() ? 'yes' : 'no') . "\n";

Salida esperada:

Documents: 1
Signatures: 0
doc-001: no_signatures
All valid: no
Unsigned documents: yes

Observe que allValid() reporta aquí no: requiere al menos una firma y ningún resultado no válido, de modo que un conjunto de firmas vacío nunca pasa en silencio.

Valide un directorio de contratos firmados con clientes de revocación, anclas de confianza, fragmentación en lotes y un informe JSON protegido frente a PII.

batch-validate-contracts.php
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use NextPDF\Enterprise\Security\Ltv\CrlFetcher;
use NextPDF\Enterprise\Security\Ltv\OcspClient;
use NextPDF\Enterprise\Security\Ltv\OcspResponseCache;
use NextPDF\Enterprise\Signature\BatchSignatureValidator;
use NextPDF\Enterprise\Signature\DocumentSignatureInput;
use NextPDF\Enterprise\Signature\SignatureValidationStatus;
// Any PSR-18 client works; Guzzle shown here.
$httpClient = new \GuzzleHttp\Client(['timeout' => 10]);
// Revocation clients make a conclusive non-revoked (Good) result reachable.
// Without them, every verdict tops out at Indeterminate. The response cache
// lets repeat signers across the batch resolve without extra network calls.
$validator = new BatchSignatureValidator(
ocspClient: new OcspClient($httpClient, cache: new OcspResponseCache()),
crlFetcher: new CrlFetcher($httpClient),
);
// Trust anchors are an input: the chain terminus must match one of these.
$anchors = [(string) file_get_contents('/etc/nextpdf/trust/enterprise-root.pem')];
$inputs = [];
foreach (glob('/var/contracts/signed/*.pdf') ?: [] as $path) {
$inputs[] = new DocumentSignatureInput(
documentId: basename($path),
pdfData: (string) file_get_contents($path),
trustedCerts: $anchors,
);
}
$exit = 0;
// One call is capped at 1,000 documents; chunk larger runs.
foreach (array_chunk($inputs, 1000) as $batch) {
try {
$report = $validator->validate($batch);
// Signer PII is redacted by default in the serialized report.
file_put_contents('/var/log/nextpdf/batch-report.jsonl', $report->toJson() . PHP_EOL, FILE_APPEND); // one JSON document per line
} catch (\InvalidArgumentException | \OverflowException $e) {
fwrite(STDERR, 'Batch rejected: ' . $e->getMessage() . "\n");
exit(2);
} catch (\JsonException $e) {
fwrite(STDERR, 'Report encoding failed: ' . $e->getMessage() . "\n");
exit(3);
}
foreach ($report->documents as $doc) {
foreach ($doc->signatures as $sig) {
if ($sig->status !== SignatureValidationStatus::Valid) {
$exit = 1;
fwrite(STDERR, sprintf(
"%s: %s (chain trusted: %s, revoked: %s)\n",
$doc->documentId,
$sig->status->value,
$sig->certChain->trusted ? 'yes' : 'no',
$sig->revocation->isRevoked ? 'yes' : 'no',
));
}
}
}
}
exit($exit);

Salida esperada (stderr, para un documento cuya evidencia de revocación no estaba disponible; otras líneas varían según sus entradas):

contract-0042.pdf: indeterminate (chain trusted: yes, revoked: no)

El informe JSON serializa los campos de identidad del firmante a través del CertPiiGuard por defecto, de modo que una entrada por firma tiene este aspecto (extracto ilustrativo):

{
"status": "indeterminate",
"signer": "[REDACTED]",
"level": "B-B",
"subFilter": "ETSI.CAdES.detached"
}
  • Una lista de entrada vacía lanza \InvalidArgumentException; más de 1.000 documentos en una sola llamada lanza \OverflowException. Fragmente las ejecuciones más grandes, como en el ejemplo de producción.
  • Actualización desde versiones anteriores: sin ningún cliente OCSP o CRL configurado, la revocación es unavailable, de modo que ninguna firma puede alcanzar Valid. Las versiones anteriores reportaban Valid aquí; 3.1.0 reporta Indeterminate (consulte Panorama conceptual).
  • Los contadores a nivel de documento son estrictos: solo Valid incrementa validCount. Invalid, Indeterminate y Error incrementan todos invalidCount. Un documento cuya única firma es Indeterminate reporta por tanto all_invalid. Condicione sobre el status por firma cuando la distinción importe.
  • La comprobación OCSP se ejecuta solo cuando la cadena recuperada tiene al menos dos certificados, porque la consulta necesita el emisor. Una cadena de un solo certificado cae hacia la ruta CRL o a unavailable.
  • crlStatus nunca reporta revoked en los resultados por lotes. La alternativa CRL solo da fe de la frescura de la lista; un resultado revocado autoritativo proviene de OCSP.
  • timestamp es siempre absent() en los resultados por lotes. El validador por lotes no evalúa los tokens RFC 3161 incrustados; use Verificación de firmas para la evaluación de sellos de tiempo.
  • signer está vacío cuando la autenticación falla. Cuando se establece, es el CN (u O) del sujeto del certificado verificado por CMS, nunca la cadena /Name no autenticada del diccionario de firma.
  • Las entradas de trustedCerts deben ser certificados PEM. Una lista de anclas vacía o malformada produce un veredicto de cadena solo estructural con trusted: false, limitando el veredicto a Indeterminate.
  • Los bytes que no comienzan con una cabecera PDF producen un estado error por documento con cero firmas, sin excepción.
  • toJson() redacta la PII por defecto. Pase new CertPiiGuard(disclosePii: true) solo allí donde disponga de una base legal documentada para procesar la identidad del firmante.
  • Productor de veredictos a prueba de fallos. Valid requiere todo lo siguiente: autenticación CMS verificada sobre el resumen del /ByteRange, una cadena válida, pertenencia a un ancla de confianza confirmada y un estado de no-revocación concluyente. Toda comprobación no establecida degrada el veredicto; nada recurre por defecto a una aprobación.
  • Sin blanqueo de identidad. El firmante reportado es el sujeto del certificado vinculado criptográficamente. La entrada /Name es metadato controlado por el atacante y nunca se expone como el firmante.
  • Los algoritmos débiles nunca pasan. Una firma SHA-1 que se verifica sigue reportándose como no válida; la validez criptográfica bajo un resumen débil no se blanquea en una aprobación completa.
  • La confianza es una entrada, no una inferencia. Las anclas que usted suministra se cotejan con el extremo de la cadena por huella DER SHA-256 (RFC 5280 §6.1.1). La coherencia interna de una cadena, o una lista de anclas no vacía por sí sola, nunca establece confianza.
  • La revocación es decisiva. Una declaración de revocación verificada fuerza Invalid con independencia de cualquier otra comprobación; la evidencia no disponible fuerza Indeterminate.
  • Privacidad por defecto en la salida serializada. toJson() enmascara el CN del firmante, el DN del emisor raíz, el nombre de la TSA y los diagnósticos de problemas de cadena a menos que se desactive, implementando la minimización de datos del artículo 5(1)(c) del RGPD en el límite de serialización.
  • Tiempo determinista. La decisión de frescura de la CRL lee el reloj PSR-20 inyectado, no el reloj de pared del anfitrión, de modo que los veredictos de revocación son reproducibles bajo pruebas.

NextPDF Enterprise implementa un comportamiento informado por ETSI EN 319 102-1 (el modelo de estado de validación de tres valores y la regla de que la información de revocación no disponible produce INDETERMINATE), RFC 5652 §5.6 (recálculo del resumen del lado del verificador) y RFC 5280 §6.1 (las anclas de confianza como entradas de la parte que confía a la validación de ruta). El soporte no es conformidad, y la conformidad no es certificación. NextPDF no ostenta ninguna certificación ni la otorga. El validador por lotes no es un servicio de validación cualificado, y sus estados son veredictos de ingeniería alineados con la taxonomía de EN 319 102-1, no indicaciones TOTAL-PASSED/TOTAL-FAILED/INDETERMINATE de un proceso completo de validación de la cláusula 5. En particular, el modo por lotes no realiza prueba de existencia ni procesamiento de sellos de tiempo; la verificación de documento único cubre ese terreno.

El validador por lotes no consulta ninguna política de modo FIPS, y habilitar el modo FIPS no cambia los veredictos por lotes. Su manejo de algoritmos del lado de la verificación es fijo y a prueba de fallos: las firmas débiles (SHA-1) nunca se reportan como Valid, con o sin modo FIPS. La política de modo FIPS de Enterprise controla el lado de firma/generación, documentado en FIPS 140 — Referencia detallada. El soporte de FIPS 140 es una declaración de capacidad, no una afirmación de validación ni de certificación.

  • validate() lanza \InvalidArgumentException para una lista vacía y \OverflowException por encima de 1.000 documentos. Los documentos malformados nunca lanzan; producen resultados error por documento.
  • Valid requiere la conjunción: CMS verificado criptográficamente, cadena válida, pertenencia a un ancla de confianza confirmada y RevocationStatus::hasConclusiveGood() verdadero.
  • Un certificado confirmado como revocado es decisivo: el veredicto es Invalid con independencia de toda otra evidencia.
  • Ambas comprobaciones de revocación en Unknown/Unavailable significan Indeterminate, nunca Valid (endurecimiento de 3.1.0, con impacto en la compatibilidad hacia atrás).
  • Una firma autenticada y con cadena válida sin un ancla de confianza confirmada es Indeterminate: auténtica, con la confianza no establecida.
  • signer es el sujeto verificado por CMS o la cadena vacía; la entrada /Name nunca se utiliza.
  • timestamp es siempre el estado de ausencia en los resultados por lotes.
  • validCount cuenta solo Valid; todos los demás estados cuentan en invalidCount, y el estado del documento se agrega a partir de esos contadores.
  • toJson() aplica el CertPiiGuard con privacidad por defecto a menos que se pase un guard explícitamente.
  • Los totales del informe son sumas exactas sobre los resultados por documento; durationMs es el tiempo de pared medido para el lote.

El módulo Seguridad / Firma de NextPDF Core es el lado productor: crea firmas CMS, aplica sellos de tiempo RFC 3161 y valida cadenas y revocación para el material que incrusta en el momento de la firma. Core no incluye ningún orquestador por lotes del lado de la verificación: ni informe multidocumento, ni taxonomía de estado agregada, ni veredictos de revocación OCSP/CRL para documentos de terceros, ni serialización de informe protegida frente a PII. Solo con Core, tendría que extraer y verificar cada firma por su cuenta y construir su propia generación de informes. La verificación de documento único de Enterprise (Verificación de firmas) y este orquestador por lotes proporcionan esa capa.

Esta página documenta únicamente el comportamiento observable externamente y la superficie de API pública soportada. Las rutas de espacio de nombres internas, las clases auxiliares, las tablas de mecanismos, los nombres de archivo de runbook y los prefijos de tickets quedan fuera de alcance.