Search by

caiquemcz / ssl-converter

Caique1996

SSL certificate converter (PEM/DER/PKCS12), CA bundle and private keys in PHP.

Package info

github.com/CaiqueMcz/ssl-converter

pkg:composer/caiquemcz/ssl-converter

Statistics

Installs: 38

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v3.1.1 2026-10-28 16:20 UTC

README

License: MIT PHP Version

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.