Skip to content

Security: encryption, crypto-policy, and the signing surface

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.

Terminal window
composer require nextpdf/core:^3

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.

TypeKindKey membersStabilitySince
Document::setEncryption()method (concern HasSecurity)userPassword, ownerPassword, permissionsstable1.0.0
Document::useAesGcm()method (concern HasSecurity)?bool $enabled — opt-in ISO/TS 32003 V=6/R=7stable2.18.0
Aes256Encryptorclassencrypt(), decrypt(), buildEncryptionDictionary(), verifyUserPassword(), verifyOwnerPassword(), validatePerms()stable1.0.0
Aes256GcmEncryptorclassencrypt(), decrypt(), encryptStream(), assertWithinSafetyBound(), invocationCount()stable2.18.0
KeyMaterialfinal readonly classgenerate(), exposeKey(), fingerprint()stable2.18.0
CryptoPolicyInterfaceinterfaceisHashAlgorithmAllowed(), isSignatureAlgorithmAllowed(), isEncryptionAlgorithmAllowed(), isKeyStrengthAllowed(), getPreferredHashAlgorithm(), getName()stable1.9.0
Config::withCryptoPolicy()methodCryptoPolicyInterface $policystable1.9.0
CryptoCapabilitiesfinal classhasAesGcm(), detectFipsMode(), assertFipsAvailableForProfile()stable2.0.0
examples/22-protection.php
<?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.

examples/security/policy-gated-encryption.php
<?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.

  • Call setEncryption() before addPage(). 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 Encrypt trailer key in every PDF/A flavor, so setEncryption() and useAesGcm() 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, CryptoPolicyInterface allows 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.

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.

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.

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.

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.

ThreatMitigation in CoreResidual boundary
Algorithm downgrade / weak-cipher substitutionCrypto-policy gate; no silent degradation (raises UnsupportedAlgorithmException)Effective only when a policy is injected
Key disclosure via logsKeyMaterial redaction; #[\SensitiveParameter] on passwordsA caller that passes exposeKey() to a logger defeats this
Ciphertext tamperingGCM tag verification on the opt-in pathDefault CBC path is confidentiality-only
IV reuse (GCM)Monotonic counter plus collision set; rollover refuses
Permission-flag bypassNone; flags are advisoryA non-conforming reader ignores the flags
Brute-force on a weak passwordSASLprep plus iterated key derivation raises the costA weak password remains the dominant risk

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.

ClaimStandardClauseEvidence
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.1C_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.

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.