Security: encryption, crypto-policy, and the signing surface
At a glance
Section titled “At a glance”The Core security module applies 256-bit Advanced Encryption Standard (AES-256) document encryption, routes every algorithm choice through a crypto-policy contract, and exposes integration points for a deployment-managed key-management service. Effective document protection depends on key handling, password strength, the consuming reader, and the deployment environment. This page states those boundaries plainly.
Install
Section titled “Install”composer require nextpdf/core:^3Conceptual overview
Section titled “Conceptual overview”The security module has three surfaces. The encryption surface uses the setEncryption() document entry point to configure the AES-256 Standard security handler. The crypto-policy gate uses CryptoPolicyInterface to decide which hash, signature, cipher, and key strength a deployment permits. The signing surface is referenced here but documented separately; see Signing.
Encryption uses AES-256 as defined in ISO 32000-2:2020 §7.6. The default path is the V=5 / R=6 Standard security handler with the AESV3 crypt filter. The file key is 32 bytes (256 bits), which matches Federal Information Processing Standards (FIPS) 197. An opt-in path adds ISO/TS 32003:2023 V=6 / R=7 AES-256 in Galois/Counter Mode (AES-256-GCM) authenticated encryption. The deep page documents both paths: Encryption.
The crypto-policy gate is a deny-or-allow predicate. Core consults CryptoPolicyInterface before any signing, encryption, or hashing step. If no policy is set, Core allows every algorithm. That open default is suitable for development, not production. A regulated deployment must set an explicit policy. Contracts / Security Policy documents the contract surface.
Permission flags are the most common source of overclaim, so this page is explicit. The permission bitmask is stored in the encrypted /Perms entry and the /P value. A conforming reader is expected to honor those restrictions. The flags are not enforced by cryptography. A processor that ignores the bits can still read, copy, or modify content after it has the decryption key. State this limit to any party that relies on permission flags.
Key-management and Public-Key Cryptography Standards #11 (PKCS#11) integration are contract points. Core ships a local-key path. The KeyMaterial value object wraps a length-checked 256-bit key and resists disclosure in string and debug output. The hardware security module (HSM)/PKCS#11 key-custody path is an Enterprise capability gated behind the same contracts; this page names the integration point but does not document the Enterprise implementation.
API surface
Section titled “API surface”| Type | Kind | Key members | Stability | Since |
|---|---|---|---|---|
Document::setEncryption() | method (concern HasSecurity) | userPassword, ownerPassword, permissions | stable | 1.0.0 |
Document::useAesGcm() | method (concern HasSecurity) | ?bool $enabled — opt-in ISO/TS 32003 V=6/R=7 | stable | 2.18.0 |
Aes256Encryptor | class | encrypt(), decrypt(), buildEncryptionDictionary(), verifyUserPassword(), verifyOwnerPassword(), validatePerms() | stable | 1.0.0 |
Aes256GcmEncryptor | class | encrypt(), decrypt(), encryptStream(), assertWithinSafetyBound(), invocationCount() | stable | 2.18.0 |
KeyMaterial | final readonly class | generate(), exposeKey(), fingerprint() | stable | 2.18.0 |
CryptoPolicyInterface | interface | isHashAlgorithmAllowed(), isSignatureAlgorithmAllowed(), isEncryptionAlgorithmAllowed(), isKeyStrengthAllowed(), getPreferredHashAlgorithm(), getName() | stable | 1.9.0 |
Config::withCryptoPolicy() | method | CryptoPolicyInterface $policy | stable | 1.9.0 |
CryptoCapabilities | final class | hasAesGcm(), detectFipsMode(), assertFipsAvailableForProfile() | stable | 2.0.0 |
Code sample — Quick start
Section titled “Code sample — Quick start”<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Core\Document;
$doc = Document::createStandalone();$doc->setTitle('Encrypted Document — Restricted Permissions');
// Call setEncryption() BEFORE addPage().// Permission bit 3 (value 4) = printing allowed; all other operations denied.$doc->setEncryption( userPassword: 'demo', ownerPassword: 'admin', permissions: 4,);
$doc->addPage();$doc->setFont('helvetica', 'B', 20);$doc->cell(0, 14, 'Encrypted PDF Document', newLine: true);
$doc->save(__DIR__ . '/output/22-protection.pdf');The user password opens the document. The owner password grants full access. Permission flags constrain only a conforming reader.
Code sample — Production
Section titled “Code sample — Production”<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Contracts\CryptoPolicyInterface;use NextPDF\Core\Document;use Psr\Log\LoggerInterface;
final readonly class PolicyGatedEncryption{ public function __construct( private CryptoPolicyInterface $cryptoPolicy, private LoggerInterface $logger, ) {}
/** * Encrypt only when the active policy permits AES-256-CBC. * * @param non-empty-string $userPassword Opens the document. * @param non-empty-string $ownerPassword Grants full access. */ public function protect( Document $doc, string $userPassword, string $ownerPassword, int $permissions, ): void { if (!$this->cryptoPolicy->isEncryptionAlgorithmAllowed('aes-256-cbc')) { $this->logger->error('Encryption refused by crypto policy', [ 'policy' => $this->cryptoPolicy->getName(), ]);
throw new \RuntimeException('AES-256-CBC denied by the active crypto policy.'); }
$doc->setEncryption($userPassword, $ownerPassword, $permissions);
$this->logger->info('Document encrypted', [ 'policy' => $this->cryptoPolicy->getName(), 'algorithm' => 'aes-256-cbc', ]); }}The gate consults the policy before encryption, logs the policy name for the audit trail, and throws a specific exception when the policy denies the cipher.
Edge cases & gotchas
Section titled “Edge cases & gotchas”- Call
setEncryption()beforeaddPage(). A later call does not retroactively encrypt content the writer has already emitted. - PDF/A mode and encryption are mutually exclusive. ISO 19005 prohibits the
Encrypttrailer key in every PDF/A flavor, sosetEncryption()anduseAesGcm()throw when a PDF/A manager is active. - Inside
setEncryption(), an empty owner password falls back to the user password. A document with one shared password gives the user-password holder owner-level access. - When no policy is injected,
CryptoPolicyInterfaceallows every algorithm. Treat the open default as a development convenience, and set an explicit policy in any regulated deployment. - Permission flags are advisory to the reader. Do not describe them as access control that a hostile processor cannot bypass.
Performance
Section titled “Performance”setEncryption() runs an iterated key-derivation routine (Algorithm 2.B, revision 6) during document build. The cost is bounded and constant per document; it does not scale with page count. Per-object encryption performs one AES operation per stream or string. The opt-in AES-256-GCM path adds 28-byte overhead per object (12-byte initialization vector (IV) plus 16-byte tag) and streams large content in 16 MiB chunks. This keeps the streaming pass under the documented 64 MB peak. The performance_budget of 1500 ms wall and 64 MB peak is dominated by document rendering, not encryption.
Security notes
Section titled “Security notes”The threat model is explicit. The crypto-policy gate mitigates algorithm downgrade by refusing weak ciphers, weak hashes, and short keys before any operation. The engine does not silently substitute a weaker primitive when a requested one is absent; it raises an exception so the operator can act. KeyMaterial mitigates key disclosure through logging: its string and debug forms redact the bytes and expose only a non-reversible fingerprint. Ciphertext tampering is detected only on the opt-in AES-256-GCM path, where the authentication tag is verified and a mismatch raises an exception instead of returning plaintext. The default AES-256 Cipher Block Chaining (AES-256-CBC) path is confidentiality-only and does not detect modification by itself. On the GCM path, IV reuse is mitigated by a monotonic counter plus a defense-in-depth collision set, consistent with the unique-IV requirement in NIST SP 800-38D §8.
The boundary is equally explicit. AES-256 encryption is applied as defined in ISO 32000-2:2020 §7.6. Effective protection depends on password strength, key management, the deployment environment, and the consuming reader. Conforming readers honor permission flags, but cryptography does not enforce them. The FIPS-mode probe reports whether the host OpenSSL build has loaded a FIPS provider. The library operates in a FIPS-compatible mode when the host provides a validated module; it does not certify any module. NIST SP 800-57 Part 1 §4 frames key lifetime and cryptoperiod as deployment responsibilities. Core exposes the controls, and the deployment sets the rotation policy.
Data Residency & PII Mitigations
Section titled “Data Residency & PII Mitigations”The encryption surface does not transmit document bytes, including any personally identifiable information (PII) they contain, off-host. Key derivation, encryption, and decryption run in-process. The opt-in GCM path keys an in-memory IV-collision set by a non-reversible key fingerprint, not by key bytes. The security module writes no password or key value to disk. A deployment that routes keys through an external key-management service is responsible for that service’s residency.
Safe Telemetry & Log Scrubbing
Section titled “Safe Telemetry & Log Scrubbing”KeyMaterial::__toString() and __debugInfo() return a redacted placeholder, so an accidental log of a key object yields a fingerprint, not key bytes. Passwords passed to setEncryption() carry the #[\SensitiveParameter] attribute, which redacts them from a stack trace. For audits, use the policy name from CryptoPolicyInterface::getName() and the 8-character key fingerprint as the crypto-operation identifiers. Log those values, never the key or password.
Threat model
Section titled “Threat model”| Threat | Mitigation in Core | Residual boundary |
|---|---|---|
| Algorithm downgrade / weak-cipher substitution | Crypto-policy gate; no silent degradation (raises UnsupportedAlgorithmException) | Effective only when a policy is injected |
| Key disclosure via logs | KeyMaterial redaction; #[\SensitiveParameter] on passwords | A caller that passes exposeKey() to a logger defeats this |
| Ciphertext tampering | GCM tag verification on the opt-in path | Default CBC path is confidentiality-only |
| IV reuse (GCM) | Monotonic counter plus collision set; rollover refuses | — |
| Permission-flag bypass | None; flags are advisory | A non-conforming reader ignores the flags |
| Brute-force on a weak password | SASLprep plus iterated key derivation raises the cost | A weak password remains the dominant risk |
FIPS-mode behavior
Section titled “FIPS-mode behavior”Core is not a FIPS-validated cryptographic module and is not FIPS-certified. CryptoCapabilities::detectFipsMode() is a best-effort runtime probe: it reads an operator override, then the OpenSSL provider list, then the legacy FIPS-mode call, and reports active, absent, or indeterminate. assertFipsAvailableForProfile() fails closed when a FIPS profile is selected on a host that cannot prove a FIPS provider. The library operates in a FIPS-compatible mode when it is configured against a host OpenSSL build that has loaded a FIPS-validated provider. A validated, certified FIPS posture is an Enterprise concern; see the Enterprise documentation.
Conformance
Section titled “Conformance”| Claim | Standard | Clause | Evidence |
|---|---|---|---|
| The GCM path keeps each IV unique for an invocation, consistent with the standard’s uniqueness requirement. | NIST SP 800-38D | §8.2.1 | |
| Core exposes controls for key lifetime and cryptoperiod; the deployment owns the policy. | NIST SP 800-57 Part 1 Rev. 5 | §4 | |
| The AES file key is 256 bits, which matches the standard’s key length. | FIPS 197 | §4.2.1 | |
| Token-resident key generation is the integration point for an external key store. | OASIS PKCS#11 v3.1 | C_GenerateKey |
ISO 32000-2:2020 §7.6 is the normative basis for the Standard security handler. Its text is restricted by license and is paraphrased here, never quoted; this page cites the clause by number. Every point above is paraphrased from the cited standard.
Commercial context
Section titled “Commercial context”Core defines and freezes the crypto-policy contract, ships the AES-256 encryption path, and provides a local-key surface. The Enterprise edition supplies an HSM/PKCS#11 key-custody path and a FIPS-mode crypto-policy profile behind the same CryptoPolicyInterface. The contract surface is identical across editions; the deployment injects a different policy implementation and key-custody backend.
See also
Section titled “See also”- Security / Encryption — the AES-256 and AES-256-GCM deep reference.
- Contracts / Security Policy — the crypto-policy and resource-policy contracts.
- Security / Signing — PDF Advanced Electronic Signatures (PAdES), Cryptographic Message Syntax (CMS), and timestamps.
- Audit — policy-name and operation audit logging.
- Conformance — PDF/A interaction with encryption.