Enterprise edition
Security — Deep Reference (HSM, PKCS#11, FIPS-mode)
At a glance
Section titled “At a glance”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.
Availability & licensing
Section titled “Availability & licensing”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.
Public API surface
Section titled “Public API surface”composer require nextpdf/enterprise:^3The 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.
| Symbol | Parameters | Default behavior | Returns | Throws or fails with | Notes |
|---|---|---|---|---|---|
Pkcs11Signer::__construct() | string $libraryPath, int $slotId, string $pin, string $certLabel, ?string $keyLabel = null, array $chainDer = [], bool $enablePostQuantum = false, ?FipsSignatureEnforcer $fipsEnforcer = null | Opens the vendor library, logs into the slot, loads certificate and key-algorithm metadata | — | HsmOperationException when ext-pkcs11 is absent or token access fails | PIN and labels are #[SensitiveParameter]; one module handle is cached per library path per process |
Pkcs11Signer::isAvailable() | None | Reports whether ext-pkcs11 is loaded | bool | None | Static; check before construction |
Pkcs11Signer::sign() | string $data, string $algorithm = 'sha256WithRSAEncryption' | Signs on the token; raw ECDSA output is converted to DER ECDSA-Sig-Value | string raw signature bytes | HsmOperationException (key not found, token failure); InvalidArgumentException (unmapped algorithm); FipsViolationException / FipsModuleErrorStateException before signing when an enforcer is wired | Closed algorithm set; see Behavior contract |
Pkcs11Signer::signPqs() | string $data, Pkcs11PqsAlgorithm $algorithm, string $context = '', bool $randomized = true | Refused unless $enablePostQuantum was set; dispatches the provisional PKCS#11 post-quantum mechanism | string raw signature bytes | HsmOperationException (disabled, token failure, signature-length mismatch); InvalidArgumentException (context over 255 bytes) | Preview; no conformance claim |
Pkcs11Signer accessor surface | None | Read-only construction results | bool / string / array<string> | None | isPostQuantumEnabled, 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 = null | Verifies proc_open, probes the binary, resolves the backend, loads the certificates | — | HsmOperationException (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 default | string raw signature bytes | HsmOperationException (timeout, PIN rejected, key not found, empty output); InvalidArgumentException (unmapped algorithm); FIPS-gate exceptions before signing | Subprocess is killed after $timeoutSeconds; stderr is redacted |
OpenSslCliSigner accessor surface | None | Read-only construction results | string / array<string> / OpenSslCliBackend | None | getCertificateDer, getCertificateChainDer, getPublicKeyAlgorithm, getCertificatePem, getResolvedBackend, getOpensslVersion |
OpenSslCliBackend | — | Enum: Provider, Engine, Auto | — | None | Backend selection for the CLI signer |
Pkcs11PqsAlgorithm | — | Enum of ML-DSA and SLH-DSA parameter sets | — | None | Helpers: isMlDsa, isSlhDsa, mechanismId, parameterSetId, signatureLength, nistCategory |
PqsCapabilityStatus::current() | None | Builds the honest post-quantum posture for the process | PqsCapabilityStatus | None | Every conformance claim boolean is hard-coded false; no flag can flip one on |
HsmSignerProviderAdapter | HsmSignerInterface $hsm, string $providerId, SignatureAlgorithm $algorithm = SignatureAlgorithm::Pkcs1v15 | Exposes an HSM concrete as a unified SignerProviderInterface | Per SPI | KeyManagementException (non-null key version); SignatureFailedException (driver failure, empty signature) | Provider ids: pkcs11-{module-id}, openssl-cli |
HsmOperationException | — | Typed failure for every HSM signing path | — | — | Extends the Core NextPdfException |
FipsCryptoPolicy::strict() / ::standard() | ?FipsSelfTest $selfTest = null | Factory presets; strict is the FIPS 140-3 profile, standard adds AES-128-CBC | FipsCryptoPolicy | None | Immutable allow-lists; see FIPS-mode behavior |
FipsCryptoPolicy predicate surface | string / int inputs | Allow-list membership checks | bool / string | None | isHashAlgorithmAllowed, isSignatureAlgorithmAllowed, isEncryptionAlgorithmAllowed, isKeyStrengthAllowed, getPreferredHashAlgorithm, getName |
FipsCryptoPolicy::assertPreOperational() | None | Runs (or replays) the power-on self-test | void | FipsModuleErrorStateException | Driven by the Core enforcement seam on the first cryptographic operation |
FipsModeGuard::__construct() | CryptoPolicyInterface $policy, ?FipsBootGuard $bootGuard = null, ?FipsAuditLogger $auditLogger = null | Wraps a policy with assert-style boundaries | — | None | Without a boot guard the self-test gate is absent (policy-only) |
FipsModeGuard::assertHashAllowed() | string $algorithm | Deny catalogue first, then the allow-list | void | FipsViolationException; FipsModuleErrorStateException when a boot guard is wired | Audit record precedes any FipsViolationException |
FipsModeGuard assert surface | string / int inputs | Deny catalogue first, then allow-list; audit record before any throw | void | FipsViolationException; FipsModuleErrorStateException (boot guard wired) | assertHashAllowed, assertSignatureAlgorithmAllowed, assertEncryptionAllowed, assertKeyStrengthAllowed, plus getPolicy |
FipsBootGuard::report() / ::rerun() | None | Runs the self-test battery (cached / forced) | FipsSelfTestReport | None | An ERROR report latches the process; a passing re-run never clears the latch |
FipsBootGuard::assertOperational() | None | Asserts the module is OPERATIONAL | void | FipsModuleErrorStateException | Sticky: a process-latched ERROR rejects even a clean instance |
FipsBootGuard::status() | None | Reports the cached status | FipsSelfTestStatus | None | PRE_OPERATIONAL, OPERATIONAL, or ERROR |
FipsSelfTest::run() | None | Executes the full known-answer-test battery; never short-circuits | FipsSelfTestReport | None | Constructor accepts injectable hash and random-bytes providers for deterministic tests |
FipsSelfTestReport / FipsSelfTestResult / FipsSelfTestStatus | — | Report value objects and status enum | — | FipsSelfTestReport::assertOperational() throws FipsModuleErrorStateException | results always lists every outcome for audit evidence |
FipsSignatureEnforcer::assertSignatureGenerationAllowed() | string $algorithm, string $certificatePem | Resolves the signature OID and key strength, then delegates to the guard | void | FipsViolationException (disallowed or unclassifiable, fail-closed) | The chokepoint both signers call at the top of sign() in FIPS mode |
FipsAuditLogger | CryptoPolicyInterface $policy, LoggerInterface $logger | Emits ALLOW (INFO) / DENY (WARNING) records per decision | bool per log call | None | logHashOperation, logSignatureOperation, logEncryptionOperation, logKeyStrengthCheck |
FipsTransitioningAlgorithms | string / int inputs | Static NIST SP 800-131A deny catalogue | bool / array | None | The explicit-deny layer under every guard boundary |
FipsBootstrap::boot() / ::lazy() | ?CryptoPolicyInterface $policy = null, ?FipsSelfTest $selfTest = null, ?LoggerInterface $auditLogger = null | Composes boot guard, policy, and mode guard; boot() runs the self-test at once, lazy() defers it to the first boundary | FipsModeGuard | boot(): FipsModuleErrorStateException on a failed test | Defaults to the strict policy |
FipsBootstrap::signatureEnforcer() | ?CryptoPolicyInterface $policy = null, ?FipsSelfTest $selfTest = null | Boots the module and returns the generation-time gate for the signers | FipsSignatureEnforcer | FipsModuleErrorStateException | Pass the result to a signer’s $fipsEnforcer parameter |
FipsBootstrap::selfTestReport() | ?FipsSelfTest $selfTest = null | Runs the battery on demand and summarizes it | array{status, operational, failed} | None | Intended for health endpoints and the CLI subcommand |
FipsViolationException / FipsModuleErrorStateException | — | Typed FIPS failures | — | — | Expose 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(): boolpublic function sign(string $data, string $algorithm = 'sha256WithRSAEncryption'): stringpublic function signPqs(string $data, Pkcs11PqsAlgorithm $algorithm, string $context = '', bool $randomized = true): stringpublic 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'): stringpublic static function strict(?FipsSelfTest $selfTest = null): selfpublic static function standard(?FipsSelfTest $selfTest = null): selfpublic function assertPreOperational(): voidpublic function __construct(private CryptoPolicyInterface $policy, private ?FipsBootGuard $bootGuard = null, private ?FipsAuditLogger $auditLogger = null)public function assertHashAllowed(string $algorithm): voidpublic function assertSignatureAlgorithmAllowed(string $oid): voidpublic function assertEncryptionAllowed(string $algorithm): voidpublic function assertKeyStrengthAllowed(string $keyType, int $bitLength): voidpublic function getPolicy(): CryptoPolicyInterfacepublic static function boot(?CryptoPolicyInterface $policy = null, ?FipsSelfTest $selfTest = null, ?LoggerInterface $auditLogger = null): FipsModeGuardpublic static function lazy(?CryptoPolicyInterface $policy = null, ?FipsSelfTest $selfTest = null, ?LoggerInterface $auditLogger = null): FipsModeGuardpublic static function signatureEnforcer(?CryptoPolicyInterface $policy = null, ?FipsSelfTest $selfTest = null): FipsSignatureEnforcerpublic static function selfTestReport(?FipsSelfTest $selfTest = null): arrayBehavior contract
Section titled “Behavior contract”- Contract resolution. Both signers implement the Core
HsmSignerInterface; the policy implements the CoreCryptoPolicyInterface. 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.
Pkcs11Signerdelegates the operation to the token;OpenSslCliSignerpasses 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 (
Pkcs11Signeralso acceptsecdsa-raw). Any other identifier raisesInvalidArgumentException; 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-encodedECDSA-Sig-Valueform 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.
FipsSignatureEnforcergoverns 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.
Edge cases & failure modes
Section titled “Edge cases & failure modes”- Constructing
Pkcs11Signerwithoutext-pkcs11raisesHsmOperationExceptionimmediately; the extension is not bundled with standard PHP distributions. - A certificate or private-key label that matches no token object raises
HsmOperationExceptionnaming the missing object class. OpenSslCliSignerrefuses a$keyUricontainingpin-valueat 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
FipsModuleErrorStateExceptioncarrying 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 raisesInvalidArgumentException(FIPS 204 §5.4). A returned signature whose byte length does not match the selected parameter set is rejected before it reaches encoding.
FIPS-mode behavior
Section titled “FIPS-mode behavior”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.
Conformance
Section titled “Conformance”| Claim | Standard | Clause |
|---|---|---|
| Token sign operation, session, and user login semantics | PKCS#11 v3.1 | §5 (sign) |
| PSS salt length equals the digest length | PKCS#11 v3.1 | §5 (PSS sLen) |
| ECDSA signature generation; curve and hash pairing | FIPS 186-5 | §6.3.2; §6.1.1 |
| Minimum RSA key length and signature-generation transition status | NIST SP 800-131A Rev.2 | §3 |
| Self-test category, documentation, conditional trigger, disjoint set | ISO/IEC 19790:2025 | §7.10, §7.10.2, §7.10.3, §7.10.3.p3 |
| AES-GCM initialization-vector uniqueness | NIST SP 800-38D | §5 |
| Key protection and custody responsibilities | NIST SP 800-57 Part 1 Rev.5 | §5.5.2 |
| Post-quantum signing context string limited to 255 bytes | FIPS 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.
Development notes
Section titled “Development notes”- 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’$fipsEnforcerparameter. Non-FIPS deployments passnulland behavior is unchanged. - The
bin/nextpdf-enterprisefips:self-testsubcommand 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@internaland 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.
Publication boundary
Section titled “Publication boundary”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.
See also
Section titled “See also”- Security — NextPDF Enterprise — the capability page for this surface.
- Hardware security module signing (PKCS#11) — setup, configuration, and verification steps.
- FIPS 140 cryptographic policy — the FIPS capability page.
- HSM — Deep Reference — the focused signer reference.
- FIPS 140 — Deep Reference — the focused FIPS-module reference.
- Security — NextPDF Pro — the Pro-tier security surface.
- Security — NextPDF Core — the Core security baseline.