Skip to content
getnextpdf.com

Enterprise edition

Hardware security module signing (PKCS#11)

NextPDF Enterprise signs a PDF with a key held inside a hardware security module (HSM). You point the signer at a PKCS#11 token — a smart card, a Universal Serial Bus (USB) token, or a network-attached HSM — and the signing operation runs on the device. The private key never leaves the token boundary. This page is behaviour-level: it states what the signer does, what you provide, and where key custody stops being NextPDF’s responsibility.

The HSM signer resolves through the Core signer contract, so your application depends on the contract, not on the concrete Enterprise type. It expands the same Cryptographic Message Syntax (CMS) signing path that Core uses, except the cryptographic operation is delegated to the token.

Prerequisites are stated in the front matter and repeated under Prerequisites so you are not surprised mid-task.

This capability ships in NextPDF Enterprise (nextpdf/enterprise) and activates with an Enterprise-tier license envelope. A deployment without that entitlement does not load the capability’s classes. Compare editions and get a license.

NextPDF Core ships a software CMS signer that holds the key in process or accepts one through the Core signing-strategy contract; NextPDF Pro adds remote and cloud key-management-service (KMS) signing strategies. Hardware key custody through PKCS#11 is an Enterprise capability, not provided by Core or Pro.

A PKCS#11 token exposes cryptographic objects — certificates and private keys — behind a vendor shared library. The Enterprise signer adapts that library:

  1. It opens the token’s shared library once per process and caches the module handle, because PKCS#11 requires the module to be initialised exactly once per process.
  2. It opens a session on the configured slot and logs in with the supplied PIN. The login authenticates the user before any private-key operation, per PKCS#11 v3.1 §5.6.8.
  3. It locates the signing certificate on the token by label, reads the certificate in Distinguished Encoding Rules (DER) form, and detects the public-key algorithm.
  4. At signing time it locates the private key by label — which may differ from the certificate label on some tokens — and asks the token to compute the signature. The data to sign is passed in; the key stays on the device.

The signer supports RSA with PKCS#1 v1.5 padding (SHA-256, SHA-384, SHA-512), RSA with Probabilistic Signature Scheme (PSS) padding where the salt length equals the digest length, and Elliptic Curve Digital Signature Algorithm (ECDSA) with SHA-256, SHA-384, and SHA-512. The ECDSA curve and digest are paired conventionally — P-256 with SHA-256, P-384 with SHA-384, P-521 with SHA-512 — following the recommended pairing in RFC 5480. A token returns an ECDSA signature as a raw concatenation of the two integers; the signer converts it to the DER-encoded form that PDF and OpenSSL expect.

For signature generation, an RSA key of at least 2048 bits and an ECDSA curve order of at least 224 bits are the acceptable minimums per NIST SP 800-131A Rev.2 §3. Provision your token key at or above those sizes.

An alternative OpenSSL-engine path exists for engine-backed tokens. On OpenSSL 3.x the PHP OpenSSL extension does not expose the engine application programming interface (API), so the engine class is deprecated; the supported engine-backed route runs the OpenSSL command-line binary. Prefer the direct PKCS#11 path where your token has a PKCS#11 library.

The load-bearing decision is that the private key never leaves the token. So the signer delegates the cryptographic operation to the device and moves only the data-to-sign across the PKCS#11 seam. It never reads or reconstructs key material in PHP memory. It resolves through the Core HsmSignerInterface contract rather than a concrete Enterprise type, so signing code is identical whether the key lives in software, a cloud KMS, or a hardware token. It caches the module handle once per process because PKCS#11 initialises each module exactly once per process, then converts the token’s raw ECDSA output to DER so validators see the encoding they expect. Custody, not convenience, drives the shape: the trust boundary stays at the device edge.

Design background: HSM-backed signing.

Before you sign with an HSM, confirm each item:

  1. Install NextPDF Core and the Enterprise package: composer require nextpdf/core:^3 and composer require nextpdf/enterprise.
  2. Hold an active NextPDF Enterprise license; resolve the package against your license credentials on Private Packagist.
  3. Install the token vendor’s PKCS#11 shared library on the host (for example a .so on Linux or a .dll on Windows) and note its absolute path, the slot number, and the object labels.
  4. Load the ext-pkcs11 PHP extension. It is not bundled with standard PHP and must be installed separately. The signer constructor raises a typed operation error when the extension is absent.

Supply these inputs to the signer:

  • Library path — the absolute path to the vendor PKCS#11 shared library.
  • Slot identifier — the token slot number, typically 0.
  • PIN — the token PIN. Treat it as a secret: supply it from your secret manager, never from source or logs. The signer marks the PIN parameter sensitive so it is excluded from stack traces and serialisation.
  • Certificate label — the label of the certificate object on the token.
  • Key label — the label of the private-key object, when it differs from the certificate label.
  • Chain — optional intermediate certificates in DER form, when the token does not hold them.

Check token availability before you construct the signer. Construction reads the certificate from the token, so a misconfigured slot or label fails fast with a typed error rather than at signing time.

  1. Confirm the runtime supports PKCS#11 by checking extension availability. Do not construct the signer when the extension is absent.
  2. Read the PIN from your secret manager into a variable that is never logged.
  3. Construct the HSM signer with the library path, slot, PIN, and labels. Construction logs in and reads the certificate.
  4. Pass the signer to the Core signing orchestrator through HsmSignerInterface. The orchestrator computes the byte range, builds the CMS signed attributes, hands the data to the token, and assembles the signed PDF.
  5. Catch the most specific failure, log a structural message without the PIN, and rethrow.
examples/contracts/hsm-signer-availability.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Contracts\HsmSignerInterface;
/**
* Build a hardware-token signer only when the runtime supports it.
*
* The concrete PKCS#11 signer is resolved through the Core contract so the
* caller depends on the interface, not the Enterprise implementation type.
* The PIN arrives from a secret resolver; it is never written to source.
*
* @param callable(): bool $pkcs11Available Reports ext-pkcs11 availability.
* @param callable(): HsmSignerInterface $signerFactory Builds the configured token signer.
*
* @throws \RuntimeException When the PKCS#11 extension is not loaded.
*
* @return HsmSignerInterface The token signer, ready for the Core orchestrator.
*/
function resolveHsmSigner(callable $pkcs11Available, callable $signerFactory): HsmSignerInterface
{
if ($pkcs11Available() !== true) {
throw new \RuntimeException(
'PKCS#11 signing requires the ext-pkcs11 extension; install it before signing.',
);
}
return $signerFactory();
}

The production wiring — the exact constructor argument list and the typed exception types — is documented in the HSM deep reference.

examples/contracts/hsm-sign-guarded.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Contracts\HsmSignerInterface;
use NextPDF\Exception\NextPdfException;
use Psr\Log\LoggerInterface;
final readonly class HsmSigningService
{
public function __construct(
private HsmSignerInterface $signer,
private LoggerInterface $logger,
) {}
/**
* Sign data on the token through the Core HSM contract.
*
* The byte range is computed by the engine, never accepted from the
* caller. The token performs the signing operation; the private key
* does not leave the device.
*
* @param string $data The bytes the orchestrator hands to the token.
* @param string $algorithm The OpenSSL-style signing algorithm identifier.
*
* @throws NextPdfException When the token operation fails.
*
* @return string The raw signature bytes returned by the token.
*/
public function sign(string $data, string $algorithm): string
{
try {
return $this->signer->sign($data, $algorithm);
} catch (NextPdfException $e) {
// Structural message only — never the PIN or key material.
$this->logger->error('HSM signing failed', ['reason' => $e->getMessage()]);
throw $e;
}
}
}

Confirm the result the way a verifier would:

  1. Read back the signer certificate and chain in DER form from the signer and confirm they match the certificate provisioned on the token.
  2. Open the signed PDF in a validator configured with your trust anchors and confirm the signature is reported as cryptographically intact. A produced signature is not a verified signature; the trust decision belongs to the verifier and its trust anchors, not to the producer.
  3. For an ECDSA signature, confirm the embedded signature is DER-encoded — the signer converts the token’s raw output for you, so a validator that rejects raw concatenated form should still accept the embedded signature.
  4. Confirm no PIN, token label, or key material appears in your application logs.
  • The key stays on the token. The data to sign is handed to the token; the signing operation runs inside the token boundary. The private key is never loaded into PHP memory.
  • The PIN is a secret. It is a sensitive constructor parameter, excluded from logs and serialisation. Supply it from a secret manager. Repeated failed re-authentication can lock the PIN at the token; the token, not NextPDF, enforces that policy.
  • Fail-closed. A token or HSM error raises a typed exception. The signer does not produce an unsigned or partly signed result and never substitutes a weaker algorithm.
  • Algorithm strength. Provision RSA keys of at least 2048 bits and ECDSA curves of at least 224-bit order, the acceptable minimums for signature generation per NIST SP 800-131A Rev.2 §3.
  • Post-quantum signing is experimental and off by default. A post-quantum path exists behind an explicit opt-in flag. Standard PDF Advanced Electronic Signatures (PAdES) long-term archival profiles do not yet recognise post-quantum suites, and most viewers reject them at validation. Do not enable it for production PAdES signatures.

This page concerns cryptographic signing and hardware-security-module integration. Every normative source is paraphrased; no normative text is reproduced. ### Key-custody boundary

NextPDF Enterprise integrates with a PKCS#11 token or HSM. It does not store, generate, or guarantee the security of the signing key. Key security depends on the token or HSM, the deployment, and the operator — not on NextPDF Enterprise alone. You are responsible for token provisioning, PIN handling, slot configuration, and network protection of a network-attached HSM.

  • Extension absent. Constructing the PKCS#11 signer raises a typed operation exception when ext-pkcs11 is not loaded. Check availability first.
  • Certificate or key not found by label. Construction or signing raises a typed exception that names the missing object. Confirm the label and slot.
  • Already logged in. When several signer instances share a cached module for the same slot, the signer logs out and logs back in to provide a fresh PIN verification — required by personal-identity-verification tokens with a “PIN every time” policy.
  • Unsupported algorithm. Requesting an algorithm the signer does not map raises an argument error rather than signing with a substitute.
  • Network HSM unreachable. A network or device error raises a typed exception; the signer never silently produces an unsigned document.

This page documents externally observable behavior and the supported public API surface only. Internal namespace paths, helper classes, mechanism tables, runbook filenames, and ticket prefixes are out of scope.