caiquemcz / ssl-converter
SSL certificate converter (PEM/DER/PKCS12), CA bundle and private keys in PHP.
Requires
- php: >=7.4
Requires (Dev)
- phpunit/phpunit: ^9.6
- squizlabs/php_codesniffer: ^4.0
Suggests
- ext-zip: Required to export all formats as a ZIP archive.
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-28 16:20:36 UTC
README
A PHP library for converting SSL certificates between different formats (PEM, PFX/PKCS12, JKS), including CA bundles and private keys.
Features
- Convert certificates to PEM format with full chain
- Convert certificates to PFX/PKCS12 format
- Convert certificates to JKS (Java KeyStore) format
- Export CRT, PEM, PFX, P12 and JKS in a temporary ZIP named after the certificate common name
- Support for encrypted private keys
- CA bundle handling
- Legacy algorithm support for older systems
- Clean, SOLID architecture
- Fully tested with PHPUnit
Requirements
- PHP >= 7.4
- OpenSSL extension
- Symfony Process component
- Java keytool (for JKS conversion)
- ZIP extension (
ext-zip, for ZIP export only)
Installation
composer require caiquemcz/ssl-converter
Usage
Quick Start with Fluent API (Recommended)
The simplest way to convert certificates is using the fluent SslConverter API:
use CaiqueMcz\SslConverter\SslConverter; // Convert to PEM $response = (new SslConverter($certificate)) ->withCaBundle($caBundle) ->withPrivateKey($privateKey, $password) // password is optional ->toPem(); // Convert to PFX $response = (new SslConverter($certificate)) ->withCaBundle($caBundle) // optional ->withPrivateKey($privateKey, $password) // password is optional ->toPfx('pfx-password', $useLegacy); // useLegacy is optional (default: false) // Convert to JKS $response = (new SslConverter($certificate)) ->withCaBundle($caBundle) // optional ->withPrivateKey($privateKey, $password) // password is optional ->toJks('jks-password', 'alias', $useLegacy); // alias and useLegacy are optional // Save the converted file file_put_contents( $response->virtualFile()->getName(), $response->virtualFile()->getContent() );
Export All Formats as a ZIP
use CaiqueMcz\SslConverter\SslConverter; $converter = (new SslConverter($certificate)) ->withCaBundle($caBundle) ->withPrivateKey($privateKey, $inputKeyPassword); // optional input key password $zipPath = $converter->toZip('SharedPassword123!'); // To also encrypt every ZIP entry with that same password: // $zipPath = $converter->toZip('SharedPassword123!', true); // To generate legacy PFX/P12 files (the second argument still controls ZIP encryption): // $zipPath = $converter->toZip('SharedPassword123!', false, true); // $zipPath = $converter->toZip('SharedPassword123!', true, true); // also encrypt the ZIP try { // Send or copy the archive using $zipPath. } finally { unlink($zipPath); rmdir(dirname($zipPath)); }
For a certificate with CN example.com, the filename is example.com_<12 random digits>.zip.
When a private key is supplied, the format directories sit directly at the root of the archive:
crt/
ca-bundle.crt
certificate.crt
private.key
jks/
alias.txt
certificate.jks
password.txt
p12/
certificate.p12
password.txt
pem/
ca-bundle.pem
certificate.pem
fullchain.pem
private.pem
pfx/
certificate.pfx
password.txt
Without a private key, omit withPrivateKey():
$zipPath = (new SslConverter($certificate)) ->withCaBundle($caBundle) ->toZip('SharedPassword123!');
The archive then contains only the certificate files and CA bundle in crt and pem:
crt/
ca-bundle.crt
certificate.crt
pem/
ca-bundle.pem
certificate.pem
fullchain.pem
This export requires no keytool or OpenSSL CLI. ZIP encryption remains optional through
the second argument, and the legacy flag has no effect because no PKCS#12 files are generated.
The CA bundle, certificate CN and existing password requirements still apply. A supplied
private key must be valid and match the certificate; invalid key material raises an error.
toZip(string $password, bool $encryptZip = false, bool $useLegacyAlgorithm = false): string
requires a parseable certificate with a single nonempty CN, a CA bundle and ext-zip.
Supplying a matching private key also enables PFX, P12 and JKS; JKS requires Java keytool.
It returns an absolute
path such as /tmp/ssl_converter_<random>/example.com_123456789012.zip, under sys_get_temp_dir().
Each filename uses a fresh cryptographically random 12-digit suffix; repeated calls create
separate archives. The caller must delete the ZIP and its containing temporary directory
when finished. On POSIX systems, the directory has mode 0700 and the
completed ZIP has mode 0600. Conversion failures remove partially generated files.
PFX and P12 contain the same encrypted PKCS#12 data under their respective filenames.
The crt directory reuses the PEM-encoded certificate and CA bundle with .crt filenames;
private.key contains the same PEM private key as pem/private.pem.
The JKS directory includes alias.txt with the normalized alias actually used by the keystore.
Each JKS, P12 and PFX directory includes password.txt containing the shared output password.
These text files contain the exact values, without an appended newline.
The shared output password protects PFX/P12 and the JKS keystore and key entry. It must have
at least six characters, as required by keytool. The CRT/PEM private key is encrypted with
AES-256-CBC only when a nonempty password is supplied through withPrivateKey(); that same
password opens the exported key. Omitting the input key password or passing '' or null
exports an unencrypted CRT/PEM private key. By default, the ZIP container has no password.
In that mode, the included password.txt files expose the password needed to open the keystores.
Pass true as the second argument to encrypt every entry with AES-256 using the shared password.
This requires ext-zip built with libzip >= 1.2.0 and an extractor that supports AES-encrypted ZIPs;
if encryption is unavailable or fails, export fails without returning an unprotected archive.
Entry filenames remain visible even when encryption is enabled. See the
PHP ZIP encryption documentation.
When no input key password is supplied, the ZIP includes a plaintext CRT/PEM private key.
The third argument, $useLegacyAlgorithm, controls legacy generation of both PFX and P12.
It defaults to false. Set it to true only for systems requiring older PKCS#12 encryption
algorithms. This option uses the existing OpenSSL CLI pkcs12 -export -legacy conversion
and requires OpenSSL 3 with its legacy provider available. These older algorithms provide
weaker protection; see the OpenSSL PKCS#12 documentation.
ZIP encryption is configured separately with the second argument. Legacy conversion stages
an unencrypted PEM private key in a random temporary file with owner-only permissions on
POSIX systems, then removes the staging files on success or failure.
The ZIP filename prefix preserves the CN's ASCII letters, digits, dots, underscores and hyphens;
other characters (including a wildcard *) become underscores. Leading/trailing dots are
removed, and Windows device names are prefixed with an underscore. For example,
*.example.com becomes _.example.com_<12 random digits>.zip. There is no enclosing CN directory
inside the ZIP. The JKS alias is derived from the original CN
using the alias policy below. PEM retains CA bundle order; PKCS#12 tools can reorder extra
certificates, and keytool constructs the JKS certificate chain from linked certificates.
These rules apply to ZIP export. Existing individual format methods retain their password and PEM key handling behavior.
Real-World Examples
Example 1: Convert PEM to PFX for Windows servers
use CaiqueMcz\SslConverter\SslConverter; $certificate = file_get_contents('certificate.pem'); $privateKey = file_get_contents('private.key'); $caBundle = file_get_contents('ca-bundle.pem'); $response = (new SslConverter($certificate)) ->withCaBundle($caBundle) ->withPrivateKey($privateKey) ->toPfx('SecurePassword123!'); file_put_contents('certificate.pfx', $response->virtualFile()->getContent());
Example 2: Convert to JKS for Java applications
use CaiqueMcz\SslConverter\SslConverter; $certificate = file_get_contents('certificate.crt'); $privateKey = file_get_contents('private.key'); $response = (new SslConverter($certificate)) ->withPrivateKey($privateKey) ->toJks('keystore-password', 'myapp'); file_put_contents('keystore.jks', $response->virtualFile()->getContent());
Example 3: Handle encrypted private keys
use CaiqueMcz\SslConverter\SslConverter; $certificate = file_get_contents('certificate.pem'); $encryptedKey = file_get_contents('encrypted-private.key'); $response = (new SslConverter($certificate)) ->withPrivateKey($encryptedKey, 'key-encryption-password') ->toPfx('pfx-password'); file_put_contents('certificate.pfx', $response->virtualFile()->getContent());
Example 4: Use legacy algorithm for older systems
use CaiqueMcz\SslConverter\SslConverter; $certificate = file_get_contents('certificate.pem'); $privateKey = file_get_contents('private.key'); $response = (new SslConverter($certificate)) ->withPrivateKey($privateKey) ->toPfx('password', true); // true = use legacy algorithm file_put_contents('certificate.pfx', $response->virtualFile()->getContent());
Advanced Usage with Converters
For more control, use the converter classes directly:
Converting to PEM Format
use CaiqueMcz\SslConverter\Converter; use CaiqueMcz\SslConverter\Formats\PemFormat; use CaiqueMcz\SslConverter\ValueObjects\CertificateData; use CaiqueMcz\SslConverter\ValueObjects\PrivateKeyData; $certificateData = new CertificateData( $certificate, new PrivateKeyData($privateKey, $privateKeyPassword), $caBundle ); $pemFormat = new PemFormat($certificateData); $converter = new Converter(); $response = $converter->convert($pemFormat); // Get the main file (fullchain.pem) $mainFile = $response->virtualFile(); echo $mainFile->getName(); // fullchain.pem echo $mainFile->getContent(); // Certificate content // Get extra files (ca-bundle.pem, private.pem) foreach ($response->extraVirtualFile() as $file) { echo $file->getName(); echo $file->getContent(); }
Converting to PFX Format
use CaiqueMcz\SslConverter\Formats\PfxFormat; $certificateData = new CertificateData( $certificate, new PrivateKeyData($privateKey), $caBundle ); $pfxFormat = new PfxFormat($certificateData); $pfxFormat->setPfxPassword('your-password'); // Optional: Use legacy algorithm for older systems $pfxFormat->withLegacyAlgorithm(); $converter = new Converter(); $response = $converter->convert($pfxFormat); $pfxFile = $response->virtualFile(); file_put_contents('certificate.pfx', $pfxFile->getContent());
Converting to JKS Format
use CaiqueMcz\SslConverter\Formats\JksFormat; $certificateData = new CertificateData( $certificate, new PrivateKeyData($privateKey), $caBundle ); $jksFormat = new JksFormat($certificateData); $jksFormat->setJksPassword('your-password'); // Optional: Use legacy algorithm $jksFormat->withLegacyAlgorithm(); $converter = new Converter(); $response = $converter->convert($jksFormat); $jksFile = $response->virtualFile(); file_put_contents('certificate.jks', $jksFile->getContent());
Working with Encrypted Private Keys
use CaiqueMcz\SslConverter\ValueObjects\PrivateKeyData; // Private key with password $privateKeyData = new PrivateKeyData($privateKey, 'key-password'); $certificateData = new CertificateData( $certificate, $privateKeyData, $caBundle ); // Use as normal $pfxFormat = new PfxFormat($certificateData); $pfxFormat->setPfxPassword('pfx-password');
Getting All Files
$response = $converter->convert($format); // Get all files including main and extras $allFiles = $response->getAllVirtualFiles(); foreach ($allFiles->get() as $file) { echo sprintf( "File: %s (Size: %d bytes)\n", $file->getName(), $file->getSize() ); file_put_contents($file->getName(), $file->getContent()); }
Format Support
PEM Format
- Generates
fullchain.pem(certificate + CA bundle) - Optional
ca-bundle.pem(CA certificates) - Optional
private.pem(private key) - Requires CA bundle
PFX/PKCS12 Format
- Generates
certificate.pfx - ZIP export also includes the same PKCS#12 container as
p12/certificate.p12 - Requires private key
- Requires password
- Supports legacy algorithm for compatibility
- Optional CA bundle
JKS Format
- Generates
certificate.jks - Requires private key
- Requires password
- Requires Java keytool installed
- Supports legacy algorithm
- Optional CA bundle
- Custom alias support
JKS aliases, including custom aliases passed to toJks(), are normalized to lowercase ASCII
letters and digits (a-z, 0-9), with other characters removed, and truncated to 32 characters.
For example, *.Example.COM becomes examplecom. An alias with no remaining letters or
digits raises ConversionException. This is the library's interoperability policy, rather
than a Java-wide 32-character restriction. Use the normalized alias when accessing the entry
with keytool. Direct JksFormat callers can configure it with setAlias().
Architecture
The library follows SOLID principles with a clean architecture:
- Value Objects: Immutable data containers (
CertificateData,PrivateKeyData,VirtualFile) - Formats: Strategy pattern for different certificate formats
- Generators: Factory pattern for complex file generation
- Converters: Facade for simple API
- Collections: Type-safe collections for virtual files
Testing
Development dependency resolution targets PHP 7.4.0 through Composer's config.platform.php,
matching the library's minimum supported PHP version. This allows the same development
dependencies to be tested on PHP 7.4 and newer. Run composer check-platform-reqs to check
the actual interpreter and extensions; that command does not use the simulated platform.
See Composer's platform configuration.
If an existing local installation selected PHPUnit helper dependencies requiring a newer PHP, refresh them before running the suite on PHP 7.4 or 8.0:
composer update doctrine/instantiator myclabs/deep-copy --with-dependencies composer check-platform-reqs
# Run tests composer test # Run tests with coverage composer test-coverage # Code style check composer lint # Fix code style composer lint-fix
Error Handling
All conversion errors throw ConversionException:
use CaiqueMcz\SslConverter\Exceptions\ConversionException; try { $response = $converter->convert($format); } catch (ConversionException $e) { echo "Conversion failed: " . $e->getMessage(); }
License
MIT License. See LICENSE file for details.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Credits
Developed by Caique
Support
If you encounter any issues or have questions, please open an issue on GitHub.