Skip to content
getnextpdf.com

Enterprise edition

Security — Deep Reference (HSM, PKCS#11, FIPS-mode)

This page is the combined deep reference for the NextPDF Enterprise security surface. It covers hardware-token signing over PKCS#11, subprocess signing through the OpenSSL command-line interface (CLI), the FIPS crypto-policy presets, the runtime FIPS guard, and the power-on self-test guard. Two focused companions exist: HSM — Deep Reference for the signer detail and FIPS 140 — Deep Reference for the FIPS module detail. The post-quantum signing path is a preview.

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.

Terminal window
composer require nextpdf/enterprise:^3

The signing types live in NextPDF\Enterprise\Security\Signature\Hsm; the FIPS types live in NextPDF\Enterprise\Security\Fips; the composition root lives in NextPDF\Enterprise\Bootstrap. Both signers implement the Core NextPDF\Contracts\HsmSignerInterface contract. The policy implements the Core NextPDF\Contracts\CryptoPolicyInterface and NextPDF\Contracts\PreOperationalSelfTestInterface contracts.

SymbolParametersDefault behaviorReturnsThrows or fails withNotes
Pkcs11Signer::__construct()string $libraryPath, int $slotId, string $pin, string $certLabel, ?string $keyLabel = null, array $chainDer = [], bool $enablePostQuantum = false, ?FipsSignatureEnforcer $fipsEnforcer = nullOpens the vendor library, logs into the slot, loads certificate and key-algorithm metadataHsmOperationException when ext-pkcs11 is absent or token access failsPIN and labels are #[SensitiveParameter]; one module handle is cached per library path per process
Pkcs11Signer::isAvailable()NoneReports whether ext-pkcs11 is loadedboolNoneStatic; check before construction
Pkcs11Signer::sign()string $data, string $algorithm = 'sha256WithRSAEncryption'Signs on the token; raw ECDSA output is converted to DER ECDSA-Sig-Valuestring raw signature bytesHsmOperationException (key not found, token failure); InvalidArgumentException (unmapped algorithm); FipsViolationException / FipsModuleErrorStateException before signing when an enforcer is wiredClosed algorithm set; see Behavior contract
Pkcs11Signer::signPqs()string $data, Pkcs11PqsAlgorithm $algorithm, string $context = '', bool $randomized = trueRefused unless $enablePostQuantum was set; dispatches the provisional PKCS#11 post-quantum mechanismstring raw signature bytesHsmOperationException (disabled, token failure, signature-length mismatch); InvalidArgumentException (context over 255 bytes)Preview; no conformance claim
Pkcs11Signer accessor surfaceNoneRead-only construction resultsbool / string / array<string>NoneisPostQuantumEnabled, getCertificateDer, getCertificateChainDer, getPublicKeyAlgorithm
OpenSslCliSigner::__construct()string $keyUri, string $certPath, string $pin, array $extraCertPaths = [], OpenSslCliBackend $backend = OpenSslCliBackend::Auto, string $opensslBinary = 'openssl', int $timeoutSeconds = 30, ?string $modulePath = null, ?string $configPath = null, bool $legacyPinDelivery = false, ?FipsSignatureEnforcer $fipsEnforcer = nullVerifies proc_open, probes the binary, resolves the backend, loads the certificatesHsmOperationException (proc_open disabled, missing module/config/certificate file, no backend); InvalidArgumentException (pin-value inside $keyUri)Auto prefers the OpenSSL 3.x provider, then the engine
OpenSslCliSigner::sign()string $data, string $algorithm = 'sha256WithRSAEncryption'Signs in an openssl subprocess; PIN travels through an ephemeral 0600 pin-source file by defaultstring raw signature bytesHsmOperationException (timeout, PIN rejected, key not found, empty output); InvalidArgumentException (unmapped algorithm); FIPS-gate exceptions before signingSubprocess is killed after $timeoutSeconds; stderr is redacted
OpenSslCliSigner accessor surfaceNoneRead-only construction resultsstring / array<string> / OpenSslCliBackendNonegetCertificateDer, getCertificateChainDer, getPublicKeyAlgorithm, getCertificatePem, getResolvedBackend, getOpensslVersion
OpenSslCliBackendEnum: Provider, Engine, AutoNoneBackend selection for the CLI signer
Pkcs11PqsAlgorithmEnum of ML-DSA and SLH-DSA parameter setsNoneHelpers: isMlDsa, isSlhDsa, mechanismId, parameterSetId, signatureLength, nistCategory
PqsCapabilityStatus::current()NoneBuilds the honest post-quantum posture for the processPqsCapabilityStatusNoneEvery conformance claim boolean is hard-coded false; no flag can flip one on
HsmSignerProviderAdapterHsmSignerInterface $hsm, string $providerId, SignatureAlgorithm $algorithm = SignatureAlgorithm::Pkcs1v15Exposes an HSM concrete as a unified SignerProviderInterfacePer SPIKeyManagementException (non-null key version); SignatureFailedException (driver failure, empty signature)Provider ids: pkcs11-{module-id}, openssl-cli
HsmOperationExceptionTyped failure for every HSM signing pathExtends the Core NextPdfException
FipsCryptoPolicy::strict() / ::standard()?FipsSelfTest $selfTest = nullFactory presets; strict is the FIPS 140-3 profile, standard adds AES-128-CBCFipsCryptoPolicyNoneImmutable allow-lists; see FIPS-mode behavior
FipsCryptoPolicy predicate surfacestring / int inputsAllow-list membership checksbool / stringNoneisHashAlgorithmAllowed, isSignatureAlgorithmAllowed, isEncryptionAlgorithmAllowed, isKeyStrengthAllowed, getPreferredHashAlgorithm, getName
FipsCryptoPolicy::assertPreOperational()NoneRuns (or replays) the power-on self-testvoidFipsModuleErrorStateExceptionDriven by the Core enforcement seam on the first cryptographic operation
FipsModeGuard::__construct()CryptoPolicyInterface $policy, ?FipsBootGuard $bootGuard = null, ?FipsAuditLogger $auditLogger = nullWraps a policy with assert-style boundariesNoneWithout a boot guard the self-test gate is absent (policy-only)
FipsModeGuard::assertHashAllowed()string $algorithmDeny catalogue first, then the allow-listvoidFipsViolationException; FipsModuleErrorStateException when a boot guard is wiredAudit record precedes any FipsViolationException
FipsModeGuard assert surfacestring / int inputsDeny catalogue first, then allow-list; audit record before any throwvoidFipsViolationException; FipsModuleErrorStateException (boot guard wired)assertHashAllowed, assertSignatureAlgorithmAllowed, assertEncryptionAllowed, assertKeyStrengthAllowed, plus getPolicy
FipsBootGuard::report() / ::rerun()NoneRuns the self-test battery (cached / forced)FipsSelfTestReportNoneAn ERROR report latches the process; a passing re-run never clears the latch
FipsBootGuard::assertOperational()NoneAsserts the module is OPERATIONALvoidFipsModuleErrorStateExceptionSticky: a process-latched ERROR rejects even a clean instance
FipsBootGuard::status()NoneReports the cached statusFipsSelfTestStatusNonePRE_OPERATIONAL, OPERATIONAL, or ERROR
FipsSelfTest::run()NoneExecutes the full known-answer-test battery; never short-circuitsFipsSelfTestReportNoneConstructor accepts injectable hash and random-bytes providers for deterministic tests
FipsSelfTestReport / FipsSelfTestResult / FipsSelfTestStatusReport value objects and status enumFipsSelfTestReport::assertOperational() throws FipsModuleErrorStateExceptionresults always lists every outcome for audit evidence
FipsSignatureEnforcer::assertSignatureGenerationAllowed()string $algorithm, string $certificatePemResolves the signature OID and key strength, then delegates to the guardvoidFipsViolationException (disallowed or unclassifiable, fail-closed)The chokepoint both signers call at the top of sign() in FIPS mode
FipsAuditLoggerCryptoPolicyInterface $policy, LoggerInterface $loggerEmits ALLOW (INFO) / DENY (WARNING) records per decisionbool per log callNonelogHashOperation, logSignatureOperation, logEncryptionOperation, logKeyStrengthCheck
FipsTransitioningAlgorithmsstring / int inputsStatic NIST SP 800-131A deny cataloguebool / arrayNoneThe explicit-deny layer under every guard boundary
FipsBootstrap::boot() / ::lazy()?CryptoPolicyInterface $policy = null, ?FipsSelfTest $selfTest = null, ?LoggerInterface $auditLogger = nullComposes boot guard, policy, and mode guard; boot() runs the self-test at once, lazy() defers it to the first boundaryFipsModeGuardboot(): FipsModuleErrorStateException on a failed testDefaults to the strict policy
FipsBootstrap::signatureEnforcer()?CryptoPolicyInterface $policy = null, ?FipsSelfTest $selfTest = nullBoots the module and returns the generation-time gate for the signersFipsSignatureEnforcerFipsModuleErrorStateExceptionPass the result to a signer’s $fipsEnforcer parameter
FipsBootstrap::selfTestReport()?FipsSelfTest $selfTest = nullRuns the battery on demand and summarizes itarray{status, operational, failed}NoneIntended for health endpoints and the CLI subcommand
FipsViolationException / FipsModuleErrorStateExceptionTyped FIPS failuresExpose policyName / violatingItem / reason and failedResults respectively
public function __construct(private readonly string $libraryPath, private readonly int $slotId, #[SensitiveParameter] private readonly string $pin, #[SensitiveParameter] private readonly string $certLabel, #[SensitiveParameter] private readonly ?string $keyLabel = null, array $chainDer = [], private readonly bool $enablePostQuantum = false, ?FipsSignatureEnforcer $fipsEnforcer = null)
public static function isAvailable(): bool
public function sign(string $data, string $algorithm = 'sha256WithRSAEncryption'): string
public function signPqs(string $data, Pkcs11PqsAlgorithm $algorithm, string $context = '', bool $randomized = true): string
public function __construct(private string $keyUri, string $certPath, #[SensitiveParameter] private string $pin, array $extraCertPaths = [], private OpenSslCliBackend $backend = OpenSslCliBackend::Auto, private string $opensslBinary = 'openssl', private int $timeoutSeconds = 30, private ?string $modulePath = null, private ?string $configPath = null, private bool $legacyPinDelivery = false, private ?FipsSignatureEnforcer $fipsEnforcer = null)
public function sign(string $data, string $algorithm = 'sha256WithRSAEncryption'): string
public static function strict(?FipsSelfTest $selfTest = null): self
public static function standard(?FipsSelfTest $selfTest = null): self
public function assertPreOperational(): void
public function __construct(private CryptoPolicyInterface $policy, private ?FipsBootGuard $bootGuard = null, private ?FipsAuditLogger $auditLogger = null)
public function assertHashAllowed(string $algorithm): void
public function assertSignatureAlgorithmAllowed(string $oid): void
public function assertEncryptionAllowed(string $algorithm): void
public function assertKeyStrengthAllowed(string $keyType, int $bitLength): void
public function getPolicy(): CryptoPolicyInterface
public static function boot(?CryptoPolicyInterface $policy = null, ?FipsSelfTest $selfTest = null, ?LoggerInterface $auditLogger = null): FipsModeGuard
public static function lazy(?CryptoPolicyInterface $policy = null, ?FipsSelfTest $selfTest = null, ?LoggerInterface $auditLogger = null): FipsModeGuard
public static function signatureEnforcer(?CryptoPolicyInterface $policy = null, ?FipsSelfTest $selfTest = null): FipsSignatureEnforcer
public static function selfTestReport(?FipsSelfTest $selfTest = null): array
  • Contract resolution. Both signers implement the Core HsmSignerInterface; the policy implements the Core CryptoPolicyInterface. Calling code depends on the contracts, so an edition upgrade changes composition, not call sites.
  • Key custody. The private key never leaves the token boundary. Pkcs11Signer delegates the operation to the token; OpenSslCliSigner passes a PKCS#11 URI key reference to the subprocess. NextPDF does not store, generate, or guarantee the security of the signing key. Key protection is the operator’s custody responsibility (NIST SP 800-57 Part 1 Rev.5 §5.5.2).
  • Session and login. The token sign operation, the session, and the user login follow PKCS#11 v3.1 §5. The certificate label and the private-key label may differ; the constructor accepts a separate key label for such tokens.
  • Closed algorithm set. The signers accept exactly: RSA PKCS#1 v1.5 with SHA-256/384/512, RSASSA-PSS with SHA-256/384/512, and ECDSA with SHA-256/384/512 (Pkcs11Signer also accepts ecdsa-raw). Any other identifier raises InvalidArgumentException; no substitute algorithm is ever signed.
  • PSS salt binding. For every PSS variant the salt length equals the digest length — 32, 48, or 64 bytes — and the hash and mask-generation parameters match the chosen digest (PKCS#11 v3.1 §5).
  • ECDSA conversion. Token ECDSA mechanisms return a raw signature; sign() converts it to the DER-encoded ECDSA-Sig-Value form for PDF and OpenSSL interoperability. Signature generation follows FIPS 186-5 §6.3.2.
  • Preset contents. The strict preset allows SHA-256/384/512; RSA and ECDSA signature OIDs with those hashes; RSASSA-PSS; AES-256-CBC and AES-256-GCM; minimum RSA 2048 and EC 256. The standard preset additionally allows AES-128-CBC for legacy interoperability. Any AES-GCM use requires a unique initialization vector per key (NIST SP 800-38D §5).
  • Two-layer enforcement. Every guard boundary consults the explicit NIST SP 800-131A deny catalogue first, then the policy allow-list. The deny layer produces the audit-clear “disallowed” signal; the allow-list remains authoritative.
  • Power-on self-test. The battery covers SHA-256/384/512, HMAC-SHA-256, AES-256-CBC, AES-256-GCM, an ECDSA P-256 pairwise-consistency test, and a random-bit health check. The first cryptographic operation under the policy on the Core path runs it once per process, fail-closed. A failure puts the module in the ERROR state; cryptographic services are refused until reset. This follows ISO/IEC 19790:2025 §7.10, §7.10.2, §7.10.3, and §7.10.3.p3.
  • Sticky ERROR state. An observed ERROR latches for the whole process. Constructing a fresh policy or boot guard cannot launder it; a passing re-run does not clear it. Only a process restart — a true power cycle — resets the state.
  • Generation gate only. FipsSignatureEnforcer governs producing new signatures. Validation of pre-existing signatures is legacy use and never routes through the enforcer.
  • Audit trail. When a guard is composed with an audit logger, every boundary emits an ALLOW or DENY record before permitting or rejecting the operation. The logger consults the same policy the guard enforces, so the recorded decision cannot diverge.
  • Constructing Pkcs11Signer without ext-pkcs11 raises HsmOperationException immediately; the extension is not bundled with standard PHP distributions.
  • A certificate or private-key label that matches no token object raises HsmOperationException naming the missing object class.
  • OpenSslCliSigner refuses a $keyUri containing pin-value at construction, fail-closed; the PIN travels through the secure pin-source path instead.
  • In FIPS mode, an algorithm identifier that cannot be mapped to a known signature OID is denied fail-closed; so is a certificate whose public-key strength cannot be determined.
  • An unknown key type is denied by default; the policy never falls back to a weaker algorithm.
  • A failed known-answer test raises FipsModuleErrorStateException carrying the failed results; every later boundary in the process repeats the failure until restart.
  • A guard constructed without a boot guard enforces the allow-lists but provides no self-test gate; production FIPS composition supplies one through the bootstrap.
  • signPqs() refuses to run unless the constructor opt-in was set. A context string over 255 bytes raises InvalidArgumentException (FIPS 204 §5.4). A returned signature whose byte length does not match the selected parameter set is rejected before it reaches encoding.

FIPS-allowed in strict mode: SHA-256/384/512; RSA PKCS#1 v1.5 and RSA-PSS with those hashes; ECDSA with those hashes; AES-256-CBC and AES-256-GCM; RSA at least 2048 bits, EC at least 256 bits. FIPS-rejected in strict mode: weaker or legacy hashes, non-approved signature OIDs, AES-128 (allowed only in the standard preset), and any key below the minimum strength. The minimum RSA key length and the transition status follow NIST SP 800-131A Rev.2 §3. The ECDSA curve and hash pairing follows FIPS 186-5 §6.1.1. The path is fail-closed and never substitutes a weaker algorithm.

NextPDF Enterprise is not a FIPS-validated cryptographic module and makes no FIPS certification claim. NextPDF Enterprise operates in a FIPS-compatible mode only when it is configured with a FIPS-validated cryptographic provider — for example a FIPS-validated OpenSSL provider — or a FIPS-validated HSM. The FIPS-mode policy assists compliance; it is not a certification. No FIPS certification artifact exists in this repository.

ClaimStandardClause
Token sign operation, session, and user login semanticsPKCS#11 v3.1§5 (sign)
PSS salt length equals the digest lengthPKCS#11 v3.1§5 (PSS sLen)
ECDSA signature generation; curve and hash pairingFIPS 186-5§6.3.2; §6.1.1
Minimum RSA key length and signature-generation transition statusNIST SP 800-131A Rev.2§3
Self-test category, documentation, conditional trigger, disjoint setISO/IEC 19790:2025§7.10, §7.10.2, §7.10.3, §7.10.3.p3
AES-GCM initialization-vector uniquenessNIST SP 800-38D§5
Key protection and custody responsibilitiesNIST SP 800-57 Part 1 Rev.5§5.5.2
Post-quantum signing context string limited to 255 bytesFIPS 204§5.4

All clauses are paraphrased; no normative text is reproduced. These are capability claims about NextPDF code, not certifications. Whether a produced signature verifies is the verifier’s decision against its own trust configuration. The FIPS-mode policy is a compliance-assistance feature, not a legal opinion; consult your own compliance and legal advisers. This module concerns cryptographic functionality; treat it as security-sensitive in your own review.

  • Compose FIPS mode through the bootstrap: boot() for a start-up gate, lazy() to defer the battery to the first boundary, and the enforcer factory for the signers’ $fipsEnforcer parameter. Non-FIPS deployments pass null and behavior is unchanged.
  • The bin/nextpdf-enterprise fips:self-test subcommand runs the battery on demand and exits non-zero in the ERROR state; wire it to maintenance jobs or admin-only health endpoints (ISO/IEC 19790:2025 on-demand self-tests).
  • FipsBootGuard::resetProcessErrorLatchForTesting() is @internal and test-only; production code never calls it, because it would defeat the sticky ERROR state.
  • Construct signers once and reuse them; construction logs in and reads the certificate, and the per-library module cache makes repeated construction against the same library safe.
  • Supply the PIN from a secret manager. It is a #[SensitiveParameter], never logged or serialized; do not commit it to configuration.
  • The operator owns token provisioning, PIN handling, slot configuration, network protection of a network-attached HSM, and trust configuration. This page does not expose token PIN policy internals or vendor credential material.
  • Do not enable the post-quantum preview for production AdES signatures. The AdES cryptographic-suites catalogue does not yet recognize post-quantum suites, most PDF viewers reject such signatures, and hardware round-trip validation is not complete. Internal mechanism detail stays in the source repository’s internal documentation and is out of scope for this manual.

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.