Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/eclipxe13/CfdiUtils/llms.txt

Use this file to discover all available pages before exploring further.

The SAT (Mexico’s tax authority) delivers digital certificates and private keys in binary DER formats that PHP cannot use directly. The CfdiUtils\OpenSSL\OpenSSL class bridges that gap: it converts CER certificates and KEY private keys into the PEM format that PHP’s OpenSSL functions require, and provides helpers for reading, protecting, and unprotecting PEM content. It is not a reimplementation of OpenSSL — it orchestrates calls to the external openssl command when native PHP cannot perform the operation.

File Format Overview

Understanding the three file formats you encounter when working with SAT certificates is essential before using any conversion method.
Files with the .cer extension from the SAT are X.509 certificates encoded in DER (Distinguished Encoding Rules) — a compact binary format. PHP cannot load a DER certificate directly; it requires the PEM encoding. Converting CER to PEM only requires base64-encoding the raw bytes and wrapping them in the standard -----BEGIN CERTIFICATE----- / -----END CERTIFICATE----- delimiters, so this conversion can be done entirely in PHP without calling the external openssl binary.
Files with the .key extension from the SAT are private keys encoded in PKCS#8 DER format. Unlike CER files, converting a PKCS#8 DER private key to PEM is not something PHP can do on its own. The external openssl binary must be called to perform this conversion.
PEM is a text container format used for certificates, public keys, and private keys. Each section is bounded by a -----BEGIN <TYPE>----- / -----END <TYPE>----- header/footer pair, and the content is base64-encoded (64 characters per line). A single PEM file may contain multiple sections.
-----BEGIN CERTIFICATE-----
MIIGUTCCBDmgAwIBAgIUMjAwMDEwMDAwMDAzMDAwMjI4MTYwDQYJKoZI...
-----END CERTIFICATE-----
PEM files can use either Unix (LF) or Windows (CRLF) line endings. CfdiUtils normalises line endings to PHP_EOL when reading PEM content.
Never embed the PEM certificate in a CFDI. PEM content contains newline characters that are not valid inside XML attribute values. Always use the X.509 DER binary format (base64-encoded without line breaks) as specified in Anexo 20 when setting the NoCertificado and Certificado attributes of a Comprobante.

CER to PEM Conversion

Because a CER-to-PEM conversion is just base64 encoding plus header/footer text, CfdiUtils provides two paths: a pure-PHP method and an openssl-backed method. Both produce the same output.
<?php

$openssl = new \CfdiUtils\OpenSSL\OpenSSL();

$cerFile     = 'EKU9003173C9.cer';
$cerContents = file_get_contents($cerFile);

// --- Pure PHP (no external binary required) ---
// Convert DER binary content string → PEM string
$pemCertificate = $openssl->derCerConvertPhp($cerContents);

// --- Using the external openssl binary ---
// Write the PEM certificate directly to a file
$openssl->derCerConvert($cerFile, __DIR__ . '/certificate.pem');

// Get the PEM certificate as a string (file in → string out)
$pemCertificate = $openssl->derCerConvertOut($cerFile);

// Get the PEM certificate as a string (binary content in → string out)
$pemCertificate = $openssl->derCerConvertInOut($cerContents);

Method reference

Converts a DER binary string to a PEM certificate string using only PHP — no external binary is called. Wraps the result in the standard -----BEGIN CERTIFICATE----- / -----END CERTIFICATE----- delimiters with base64 content at 64 characters per line.
derContent
string
required
The raw binary contents of a .cer (X.509 DER) file.
Converts a DER file to a PEM file using the external openssl binary.
derInFile
string
required
Path to the input .cer (X.509 DER) file. Must exist and be non-empty.
pemOutFile
string
required
Path where the PEM certificate will be written. The file must not already exist (or must be zero bytes) and its parent directory must exist.
Convenience wrapper: converts a DER file and returns the resulting PEM string instead of writing a file.
derInFile
string
required
Path to the input .cer file.
Convenience wrapper: accepts the raw DER binary as a string and returns a PEM string. Internally writes a temporary file, calls derCerConvertOut, and removes the temp file.
derContents
string
required
Raw binary content of a DER certificate.

KEY to PEM Conversion

Converting a PKCS#8 DER private key requires the external openssl binary. Passwords are passed via environment variables (not command-line arguments) to minimise exposure.
<?php

$keyDerFile             = 'EKU9003173C9.key';
$keyPemFile             = $keyDerFile . '.pem';
$keyPemFileUnprotected  = $keyDerFile . '.unprotected.pem';
$keyDerPass             = '12345678a';
$keyPemPass             = 'This is my not so strong password';

$openssl = new \CfdiUtils\OpenSSL\OpenSSL();

// Convert DER to unprotected PEM, write to file
$openssl->derKeyConvert($keyDerFile, $keyDerPass, $keyPemFileUnprotected);

// Protect an existing unprotected PEM key with a new password
$openssl->pemKeyProtect($keyPemFileUnprotected, '', $keyPemFile, $keyPemPass);

// One-step: convert DER to password-protected PEM (combines the two steps above)
$openssl->derKeyProtect($keyDerFile, $keyDerPass, $keyPemFile, $keyPemPass);

// Get a PEM key re-encrypted with a new password (useful for services like Finkok)
$keyForFinkok = $openssl->pemKeyProtectOut($keyPemFile, $keyPemPass, 'abc/123-xyz');

// Remove the password from a PEM key and get the result as a string
$unprotected = $openssl->pemKeyUnprotectOut($keyPemFile, $keyPemPass);

Method reference

Converts a PKCS#8 DER private key file to an unprotected PEM file. Calls the external openssl binary.
derInFile
string
required
Path to the input .key (PKCS#8 DER) file.
inPassPhrase
string
required
Password used to decrypt the DER file.
pemOutFile
string
required
Path where the unprotected PEM key will be written.
Like derKeyConvert but returns the PEM content as a string instead of writing a file.
derInFile
string
required
Path to the input .key (PKCS#8 DER) file.
inPassPhrase
string
required
Password used to decrypt the DER file.
Converts a PKCS#8 DER file to a password-protected PEM file in one call. Internally calls derKeyConvert followed by pemKeyProtect.
derInFile
string
required
Path to the input .key (PKCS#8 DER) file.
inPassPhrase
string
required
Password used to decrypt the DER file.
pemOutFile
string
required
Path where the protected PEM key will be written.
outPassPhrase
string
required
New password to apply to the output PEM key.
Like derKeyProtect but returns the password-protected PEM content as a string instead of writing a file.
pemInFile
string
required
Path to the input .key (PKCS#8 DER) file.
inPassPhrase
string
required
Password used to decrypt the DER file.
outPassPhrase
string
required
New password to apply to the output PEM key.
Re-encrypts an existing PEM private key with a new password. If $outPassPhrase is an empty string, delegates to pemKeyUnprotect to produce an unprotected file. This function is non-deterministic — the same key and password will produce different output on each call.
pemInFile
string
required
Path to the input PEM key file.
inPassPhrase
string
required
Current password protecting the PEM key (empty string if unprotected).
pemOutFile
string
required
Path where the re-encrypted PEM key will be written.
outPassPhrase
string
required
New password. Pass an empty string to produce an unprotected output.
pemKeyProtectOut(string $pemInFile, string $inPassPhrase, string $outPassPhrase): string — returns the re-encrypted PEM as a string.pemKeyProtectInOut(string $pemContents, string $inPassPhrase, string $outPassPhrase): string — accepts PEM content directly (no input file path needed) and returns the re-encrypted PEM as a string.
Removes the password from a PEM private key and writes the result to $pemOutFile. Calls the external openssl binary.
pemInFile
string
required
Path to the password-protected PEM key file.
inPassPhrase
string
required
Current password protecting the PEM key.
pemOutFile
string
required
Path where the unprotected PEM key will be written.
pemKeyUnprotectOut(string $pemInFile, string $inPassPhrase): string — returns the unprotected PEM as a string.pemKeyUnprotectInOut(string $pemContents, string $inPassPhrase): string — accepts PEM content directly and returns the unprotected PEM as a string.

PEM File Reading — PemContainer and PemExtractor

CfdiUtils provides two methods for reading PEM files or strings into a typed PemContainer object. These methods also normalise line endings to PHP_EOL and strip any trailing newline from each section.
<?php

$openssl = new \CfdiUtils\OpenSSL\OpenSSL();

// Read from a file
$cerPemFile = 'EKU9003173C9.cer.pem';
$certificate = $openssl->readPemFile($cerPemFile)->certificate();

// Read from a string
$keyContents = file_get_contents('EKU9003173C9.key.pem');
$privateKey  = $openssl->readPemContents($keyContents)->privateKey();

// Inspect what sections a PEM file contains
$pem = $openssl->readPemFile('bundle.pem');

if ($pem->hasCertificate()) {
    echo 'CERTIFICATE:', PHP_EOL, $pem->certificate(), PHP_EOL;
}
if ($pem->hasPublicKey()) {
    echo 'PUBLIC KEY:', PHP_EOL, $pem->publicKey(), PHP_EOL;
}
if ($pem->hasPrivateKey()) {
    echo 'PRIVATE KEY:', PHP_EOL, $pem->privateKey(), PHP_EOL;
}
PemContainer exposes three section pairs:
CheckerAccessorSection header
hasCertificate(): boolcertificate(): stringCERTIFICATE
hasPublicKey(): boolpublicKey(): stringPUBLIC KEY
hasPrivateKey(): boolprivateKey(): stringPRIVATE KEY / RSA PRIVATE KEY / ENCRYPTED PRIVATE KEY
PemExtractor validates that section content contains only Base64 characters ([A-Za-z0-9+/=]) and newlines. Sections that fail this check are silently discarded. It does not verify that the Base64 payload is a valid certificate or key.

PemPrivateKey

CfdiUtils\PemPrivateKey\PemPrivateKey is a convenience wrapper around PHP’s openssl_pkey_get_private and openssl_sign for working with private keys that have already been converted to PEM format.
<?php

use CfdiUtils\PemPrivateKey\PemPrivateKey;

// Load from a file path (prefix with file://)
$key = new PemPrivateKey('file:///path/to/EKU9003173C9.key.pem');

// Load from a PEM string
$pemContents = file_get_contents('EKU9003173C9.key.pem');
$key = new PemPrivateKey($pemContents);

// Open (decrypt) the key with its passphrase
if (! $key->open('12345678a')) {
    throw new RuntimeException('Wrong passphrase');
}

// Check whether the key is currently open
if ($key->isOpen()) {
    // Sign data with SHA-256 (default)
    $signature = $key->sign($dataToSign);

    // Sign with a different algorithm
    $signature = $key->sign($dataToSign, OPENSSL_ALGO_SHA512);
}

// Verify the key belongs to a given certificate (PEM string)
// The key must be open before calling belongsTo()
$pemCert = file_get_contents('EKU9003173C9.cer.pem');
if ($key->belongsTo($pemCert)) {
    echo 'Key and certificate match';
}

// Release the private key resource
$key->close();

PemPrivateKey method reference

__construct(string $key, ?OpenSSL $openSSL = null)
constructor
Accepts either a file://-prefixed path or raw PEM content. Throws UnexpectedValueException if no private key section can be found in the given content or file.
open(string $passPhrase): bool
bool
Opens (decrypts) the private key using the given passphrase. Returns true on success, false if the passphrase is wrong. Must be called before sign() or belongsTo().
isOpen(): bool
bool
Returns true if the key has been successfully opened and has not been closed.
sign(string $data, int $algorithm = OPENSSL_ALGO_SHA256): string
string
Signs $data using the open private key. Returns the raw binary signature string. Throws RuntimeException if the key is not open or signing fails.
belongsTo(string $pemContents): bool
bool
Returns true if the private key belongs to the certificate given as a PEM string. The key must be open before calling this method — a RuntimeException is thrown if the key has not been opened with open().
close(): void
void
Releases the in-memory private key resource. Called automatically on object destruction.

Constructor and Configuration

<?php

// Use the system openssl (found via PATH)
$openssl = new \CfdiUtils\OpenSSL\OpenSSL();

// Use a specific openssl binary
$openssl = new \CfdiUtils\OpenSSL\OpenSSL('/usr/local/opt/openssl@3/bin/openssl');

echo $openssl->getOpenSSLCommand(); // '/usr/local/opt/openssl@3/bin/openssl'
opensslCommand
string
default:"\"\""
Path to the openssl executable. When empty the string "openssl" is used, relying on the system PATH.

General Rules and Security Notes

Before any method that accepts an input file path, CfdiUtils verifies that the path is non-empty, the file exists, is not a directory, and has a size greater than zero. An OpenSSLException is thrown if any check fails.
Before writing an output file, CfdiUtils verifies that the path is non-empty, it is not a directory, its parent directory exists, and (if the file already exists) its size is zero. This prevents accidentally overwriting an existing key.
Passwords are never passed on the command line. They are injected as environment variables (PASSIN, PASSOUT) when invoking openssl, which is the best available mechanism for reducing password exposure on most operating systems.
No argument passed to the external openssl command may contain control characters other than CR (\r) and LF (\n). This prevents shell injection via crafted file paths or passwords.

Exceptions

ExceptionWhen thrown
CfdiUtils\OpenSSL\OpenSSLExceptionGeneral error in any OpenSSL utility method (file checks, empty results, etc.)
CfdiUtils\OpenSSL\OpenSSLCallerExceptionThe external openssl command exited with a non-zero status. Exposes getCallResponse(): CallResponse with the executed command, stdout, stderr, and exit code.

Build docs developers (and LLMs) love