Skip to main content
Version: 4.0

PFX

PFX, also known as PKCS12, is a container format that can contain multiple X509 certificates and private keys.

So let's say you wanted to sign an X.509 certificate. You have $CAprivate, $CAX509 and $pfx as existent variables.

If you didn't use the $pfx variable you would have to do this:

$x509->setIssuerDN($CAX509->getSubjectDN());
$x509->setAuthorityKeyIdentifier(
$CAX509->getExtension('id-ce-subjectKeyIdentifier')['extnValue']
);
$CAprivate->sign($x509);

Or you could just do this:

$pfx->sign($x509);

Reading PFXs

use phpseclib4\File\PFX;

$pfx = PFX::load(file_get_contents('pfx.der'), 'password');

print_r($pfx);
(download pfx.der)

password is optional. If you omit it, phpseclib tries null first and then falls back to the empty string, so a PFX encrypted with an empty password loads without your having to know that in advance. You only need to pass the argument when the PFX was encrypted with an actual password.

$pfx, in this case, is a PFX object which, in turn, is basically a thin wrapper around an instance of \phpseclib4\File\ASN1\Constructed. Passing $pfx through print_r() or var_dump() triggers the __debugInfo() magic method which, in turn, produces the following output:

version
phpseclib4\File\ASN1\Types\Integer
v3
authSafe
contentType
phpseclib4\File\ASN1\Types\OID
id-data
content
0
contentType
phpseclib4\File\ASN1\Types\OID
id-data
content
0
bagId
phpseclib4\File\ASN1\Types\OID
PKCS8ShroudedKeyBag
bagValue
phpseclib4\Crypt\RSA\PrivateKey
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIopDCssdOZfMCAggA
MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAECBBD729euUHXakGszQ221YkFnBIIE
0KyIIde2XfAPXwcDbRpODXQdUfN/jYhgVRPr7Chr1+od1B1/asBv08uoQd110oQk
7Bqt5foSTlCiWfaPFWP78GKJwQCgAZ2eAv4ESPHdLiOQEmtY2MAcVCxhJ9tP71nb
MeZ/+mvrVbSWQl5T8tUKfbrpS+2QQw7EEQdP5NPnJ9URSdZLfxbQPwrrX/LGZxLY
sMiqZ4gUkPbVSSmyA1bizvD/kHN8qC1ns5InFtxqt+pxRgM0ahtYRdsmXb8oFAGx
6BfIOpSDWDXrTbKEN5d3gRwKdJGKH0xJhKjgoWa3hU1xsZrj/uYwbFuQS18XcGml
ePXbWDq7w+JHEh/bFawX3Uo21TFl0FXn6DhOa1lkqWe88Y1bVZXdGFnmXU5ee5Gh
8lxXD2Yo7hWs+4ECk8Xz/gPbCRyJTPYEhxaocvPNJg44CDth2c+2KRbhcZcaH1xR
J0cSb1R/Uo6eCta7YlaPFrVBecqNBH0VkMMWcTkC8BAggIAyAKUlTF3YCJp4n1nT
Nn3s/lybFqdoSJT1Vu1vVQt+9n1WkYq9cCCypX3NYjO+6C6OymMH7OZI1JPS5eSc
Tf2OOy36UYXYeJ2MFLM5Gjf1hQXZbX0lentrRs+fg1mjVRx7EbASKkx+xafstcuu
Ph5XfN+t3Xn9R7JXxH968Bv+ycI03X6h5IlpPZ5SKuMOchGHqhvlxWeG+KY5I7yS
w9HkTpmA/Keh14/h2GpgSXqBMx/m/YbKmCuAKkKEWxGkc51ZGIbNSi6azbwLE213
hWeqD/pvy0xY6xNhtivI6WDhlBRI+xZighKkCh0wRSIT6BGSy7GDjrSqYwVZ/UvN
LT5/X3c81hx8yKpOe8tgV4CGhmbl7QvC9Gt1jCWmuwLowxAGDZ9szTr7KVWuu80d
f0tdet57DGGDbJWCn1skyabLy6wKsaOsLj7uG03U225+WEJKaKQMaPclHvVO7QEi
rObQEehN3SVCN/TgAFQpXUMau/med3ezS4Zur8zRTvOZnMB5qr7KGctDVei0ZLx/
Ww87QD5Rqhu7Cw/qQttnSkn30wvXXT+B46ZtZgcnrKS4u8yw7ieh41U5fRskBnVP
YUy4Kbtj8zRwK3UYlGYLpbWfiEdmssEACu99p66mNAVxeeKFvEAkhg7l6OU3FrC9
jeqYq1tgMYrP+SaQjD8oWfJj/PoLdjxeUuRv6fvacWbgON5+ylOc8QAeR2eL9Kaf
2rTGqX4ro6txTeV9zduTFA17VkNKlpkWbeH6qEaKG/FEqmxhblG7sqdBRSO8Ru7E
J+hIUCtpHzDnZrbWdIeLUHmonJ87U8ht3d7K8Z2QBdvPsQ0Jx/Zb8BfFm20SJ9wT
H4eKd7YH1r4pmR6w7yXgGstXynZi0dbW/72IUsbkyeXEr2aoHOb3YUNnykiheoUb
li4AunXhQcyXwctkrXkMQWugEztv3hAtGIYEAPbzCKjfTOhQnkpP9lq7oJyXk07m
TdvU4pX4jBidl7EpU0hQt3KhmH3bWaAO2e19c+kywpqCw5R7ogXlqe6392O5gn1v
8Usf2xoS2mL3t9DJ65pEjBO+zZTDfY9p1TzaElFNXWpbKNQAaCXKLaQD61DaqRfu
E4822jl03MFpD8RNfpEB345v6v4TaaG8qHZTDjm0zZy8
-----END ENCRYPTED PRIVATE KEY-----
1
contentType
phpseclib4\File\ASN1\Types\OID
id-encryptedData
content
0
bagId
phpseclib4\File\ASN1\Types\OID
CertBag
bagValue
certId
phpseclib4\File\ASN1\Types\OID
x509Certificate
certValue
phpseclib4\File\X509
-----BEGIN CERTIFICATE-----
MIICpDCCAYygAwIBAgIUW0q6DA7BwTq4pAGlsLIBl3AAxaowDQYJKoZIhvcNAQEL
BQAwDDEKMAgGA1UEAwwBYTAeFw0yNTA2MTQxOTUzMDhaFw0yNjA2MTQxOTUzMDha
MAwxCjAIBgNVBAMMAWEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDc
wCuDQsrlaP1LeFWUWHa68LY9hJp/v0gDoR+1OJ9/nmg1X34LPakXJ3DTvqqo+Tg+
8fc12LtYduwDXRU+wuf+fetfcgLk61bXa7oZPG9uYu7oYf/YYjXNpWB7miGbTwKZ
hguMrs6iKil/5XHHKZTruoTqW+imbpTENBy89uF2S7YVWK2wMibEci7DYDzIWZCX
cg+cEVcYPuT6CYLmlzocw1BStyREGjPa4vToSj/kXI6S9mos2Ji1d7oMjxru4bM9
lGwb84tK3fxu/2u2KL6gr9zS2P7iO0vDiUTkVovRlg9wfp91ShSAgbYyz82osEuZ
m0h8Pu7UAs4UWSforzyjAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAF6LngMrlwrp
jgp5j2oHGg59ozyXAoQNibi/0X47zPoZZzUaXK2MleC48OC1KgyeaySB+4IMjeXf
3yRcd94QCqGmhRriahOiT2rR7kzrEm5Uull5lblS2m77IY+oM6M6nbt1SHTnrfL4
u738iaah6W3WPR08hizfgzRH3n7ypcZ/rOREeUbMWcawR2C3eeEf9JelpGNlYOWI
2vE7jCk9eEy3Ov8aF8Tosod+3JDqeYO6SigyG5qsSxtuygIYt8sjWY9rBbSOsDMP
EvRg6P8U2fhIbseSJ7kAxqonLuB0i5o4ddDiwPoQb6NRwS1MosQ1KIyss7kf879i
tsH0sNSpwgI=
-----END CERTIFICATE-----
macData
mac
digestAlgorithm
algorithm
phpseclib4\File\ASN1\Types\OID
id-sha256
digest
phpseclib4\File\ASN1\Types\OctetString
dab02d9c7f577fcd5ddb4094b50f397455c1b79474d5fcabafb950d3fc610712
macSalt
phpseclib4\File\ASN1\Types\OctetString
dc71b16a001622d4b57b2a7d31b1d9cbf00fcdf69256bd7ac214652cd0419fa1
iterations
phpseclib4\File\ASN1\Types\Integer
2048

An unencrypted PFX (eg. pfx2.der) looks like this:

version
phpseclib4\File\ASN1\Types\Integer
v3
authSafe
contentType
phpseclib4\File\ASN1\Types\OID
id-data
content
0
contentType
phpseclib4\File\ASN1\Types\OID
id-data
content
0
bagId
phpseclib4\File\ASN1\Types\OID
KeyBag
bagValue
phpseclib4\Crypt\RSA\PrivateKey
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDcwCuDQsrlaP1L
eFWUWHa68LY9hJp/v0gDoR+1OJ9/nmg1X34LPakXJ3DTvqqo+Tg+8fc12LtYduwD
XRU+wuf+fetfcgLk61bXa7oZPG9uYu7oYf/YYjXNpWB7miGbTwKZhguMrs6iKil/
5XHHKZTruoTqW+imbpTENBy89uF2S7YVWK2wMibEci7DYDzIWZCXcg+cEVcYPuT6
CYLmlzocw1BStyREGjPa4vToSj/kXI6S9mos2Ji1d7oMjxru4bM9lGwb84tK3fxu
/2u2KL6gr9zS2P7iO0vDiUTkVovRlg9wfp91ShSAgbYyz82osEuZm0h8Pu7UAs4U
WSforzyjAgMBAAECggEAWfVKBiSq8FAXoLC6F0jxoX7ElU87csGthCUd+cjIXsZb
6xwuDkCjbpcZCIETpOZiwGv0dRIMTqGvzjMZnEjrJ6+CwpOAkG9DJWh2HyCcvFw+
xZ5iF3j2OgahydzmwxawlYWloT//XuBVpxQGiMXuNR3+Wk74ARLhNw+RgoQNHxo7
l5Vo1Mo4dEIr1wv/bHLi9AuuQ8KR85v9qAaSuMR6KRAEfrLGu3u+PNP7x4St1zKv
SUoM5EIAHS/o9ADTcZ3WpMVaNEUyxpOAGROa9W6IQInVDcSSxOqdNRfauHeGfJ3c
nZCOFiaSjRqHM6mgGqQwhiouUbVGdjE1iPc1VtWxoQKBgQD0SJj8tFxVj7kz0H7o
vJTVNPnBPiC1yExYVlFmpxQqG2AV30sgZzjQDbCa1DwlxvX/QDjelzUf1SDjqulV
AbjaK5TM4BXefQy0N5r4HTAO7Z+CnXwfCBX2pNblEJJIcvCIdVfuH3n3VUsHimx5
kLp+WeKvTjh8HxUJcwp4ZM2YcQKBgQDnVqBrYalZBLF228LyhM+svRhEGL5qGRv+
mfV9HaxZFIJMluBIeS1UMN1XgS5/YJSTQOYIXZpOC+ezp2wradhWOOzffTTYWmLh
INwWfkLyv9ctw6nDoLI8+hHqMNvTJCvzvmCfCg/etniUZEP3iwMa1xQ4tl0ZcSwJ
wlKB3lvQUwKBgFU2Yc985tyd1v/hEfQDnHNHGHw1PNnMdf02lwyU6a7Kf9kBXEaA
W0wQCnPn5tdSsQsKDwxueISTJvbj7qLNh/rqyuwwmtBSlLTk1LUKEXKTp1m7r8lv
W6H+3EtpXW3SUl93d2XEKxqA8Tw/KBm7x6I9npqCUafSe9cg/x0CJMmxAoGAA8fP
oVGcQd8ow0d+HRZ8ilcgwWQiIHu+kF+VunC0s1P5eSTyD10jl3+g7mAnEV/TrcBe
wf49mk7pf28Z/m+hYG9Xt6NFLM/1U7aEwj1X/L2T16Sd7pt+ri5sAoa/4Ohza21O
COtSLRKQT+R/k9kKuPKQ2whn5xWJYbNyQ7sNhw8CgYEA7mEyCSKggWsYkJmm09M6
wP87jsQGwsMlwejPpPZ/bY9Z/rmTxvtYO2XGsRuDgcI/xUkjEGP6JYgZBzJ+MPfb
/UXBEnFEPkT5CeWp2opooGKZ/A/oNgnAJ9w0xQdlszLhwkluDspuoGd8iu86cTHH
MTZEWXUYik1q3Yc8FcgMN38=
-----END PRIVATE KEY-----
1
contentType
phpseclib4\File\ASN1\Types\OID
id-data
content
0
bagId
phpseclib4\File\ASN1\Types\OID
CertBag
bagValue
certId
phpseclib4\File\ASN1\Types\OID
x509Certificate
certValue
phpseclib4\File\X509
-----BEGIN CERTIFICATE-----
MIICpDCCAYygAwIBAgIUW0q6DA7BwTq4pAGlsLIBl3AAxaowDQYJKoZIhvcNAQEL
BQAwDDEKMAgGA1UEAwwBYTAeFw0yNTA2MTQxOTUzMDhaFw0yNjA2MTQxOTUzMDha
MAwxCjAIBgNVBAMMAWEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDc
wCuDQsrlaP1LeFWUWHa68LY9hJp/v0gDoR+1OJ9/nmg1X34LPakXJ3DTvqqo+Tg+
8fc12LtYduwDXRU+wuf+fetfcgLk61bXa7oZPG9uYu7oYf/YYjXNpWB7miGbTwKZ
hguMrs6iKil/5XHHKZTruoTqW+imbpTENBy89uF2S7YVWK2wMibEci7DYDzIWZCX
cg+cEVcYPuT6CYLmlzocw1BStyREGjPa4vToSj/kXI6S9mos2Ji1d7oMjxru4bM9
lGwb84tK3fxu/2u2KL6gr9zS2P7iO0vDiUTkVovRlg9wfp91ShSAgbYyz82osEuZ
m0h8Pu7UAs4UWSforzyjAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAF6LngMrlwrp
jgp5j2oHGg59ozyXAoQNibi/0X47zPoZZzUaXK2MleC48OC1KgyeaySB+4IMjeXf
3yRcd94QCqGmhRriahOiT2rR7kzrEm5Uull5lblS2m77IY+oM6M6nbt1SHTnrfL4
u738iaah6W3WPR08hizfgzRH3n7ypcZ/rOREeUbMWcawR2C3eeEf9JelpGNlYOWI
2vE7jCk9eEy3Ov8aF8Tosod+3JDqeYO6SigyG5qsSxtuygIYt8sjWY9rBbSOsDMP
EvRg6P8U2fhIbseSJ7kAxqonLuB0i5o4ddDiwPoQb6NRwS1MosQ1KIyss7kf879i
tsH0sNSpwgI=
-----END CERTIFICATE-----

If an encrypted PFX is loaded and neither null nor the empty string can decrypt it, \phpseclib4\Exception\PasswordNeededException will be thrown. If the password is wrong then \phpseclib4\Exception\BadDecryptionException will be thrown instead. The two are distinct so that you can prompt the user for a password in the first case and report a credential mismatch in the second.

All elements and subelements of the above PFX objects can be accessed as array elements vis-a-vis ArrayAccess.

To learn more about the capabilities afforded to the PFX class by virtue of it being a thin wrapper around \phpseclib4\File\ASN1\Constructed please refer to Deep Dive: ASN1\Constructed Objects.

getAll()

Returns an array containing all private key and X509 objects that are contained within the PFX.

This method is slightly more complicated than one might think because the X.509 certificates and private keys don't live next to one another. Here are the respective locations of the private key and the certificate for both the encrypted and unencrypted PFX:

  • $pfx['authSafe']['content'][0]['content'][0]['bagValue']
  • $pfx['authSafe']['content'][1]['content'][0]['bagValue']['certValue']

getCertificates()

Returns an array containing all the X.509 objects that are contained within the PFX.

getPrivateKeys()

Returns an array containing all the private objects that are contained within the PFX.

getFriendlyNames()

Returns an array containing all private key and X509 pkcs-9-at-friendlyName values.

getLocalKeyIDs()

Returns an array containing all private key and X509 pkcs-9-at-localKeyId values.

pluckByFriendlyName()

Has one parameter - $value, which can be a string or any \phpseclib4\File\ASN1\Types\BaseString instance (use BaseString when you need to pin a specific ASN.1 string type for matching).

Returns an array containing all private key and X509 objects that are contained within the PFX that have a pkcs-9-at-friendlyName matching $value.

pluckByLocalKeyID()

Has one parameter - $value, which can be a string or any \phpseclib4\File\ASN1\Types\BaseString instance.

Returns an array containing all private key and X509 objects that are contained within the PFX that have a pkcs-9-at-localKeyId matching $value.

A common idiom when bundling a cert with its matching private key is to give both bags the same localKeyID so that they can be paired up later:

$keyId = random_bytes(16);
$pfx->add($x509, friendlyName: 'production', localKeyID: $keyId);
$pfx->add($privKey, friendlyName: 'production', localKeyID: $keyId);

This is what Windows and Apple Keychain use to figure out which key belongs to which cert. pluckByLocalKeyID($keyId) will then return both objects together.

pfxFromFriendlyName()

Has one parameter - $value, which can be a string or any \phpseclib4\File\ASN1\Types\BaseString instance.

Returns a PFX object containing all private key and X509 objects that are contained within the PFX that have a pkcs-9-at-friendlyName matching $value.

pfxFromLocalKeyID()

Has one parameter - $value, which can be a string or any \phpseclib4\File\ASN1\Types\BaseString instance.

Returns a PFX object containing all private key and X509 objects that are contained within the PFX that have a pkcs-9-at-localKeyId matching $value.

Both pfxFrom* methods give the new PFX the same password as the one it came from, so a subset extracted from an encrypted PFX re-exports encrypted. The pkcs-9-at-friendlyName and pkcs-9-at-localKeyId attributes are not carried over, however — the objects are re-added without them. If you need the labels preserved, use the corresponding pluckBy* method and re-add() them yourself.

Creating PFXs

Minimalistic PFX

Consider the following:

use phpseclib4\File\PFX;

$pfx = new PFX();
echo $pfx;

Since PFXs are only available in binary format their output is not being included here.

This PFX has no X509 certificate and no private key, but it does have a password — the empty string — and therefore a MAC.

If you did print_r($pfx) on that PFX you'd get this:

version
phpseclib4\File\ASN1\Types\Integer
v3
authSafe
contentType
phpseclib4\File\ASN1\Types\OID
id-data
content

Making Additions

Here's how you'd add an X.509 certificate and a private key:

$pfx->add($x509);
$pfx->add($privKey);

If you wanted them to be added with a pkcs-9-at-friendlyName attribute you could do this:

$pfx->add($x509, friendlyName: 'whatever');
$pfx->add($privKey, friendlyName: 'whatever');

If you wanted them to be added with a pkcs-9-at-localKeyId attribute you could do this:

$pfx->add($x509, localKeyID: 'whatever');
$pfx->add($privKey, localKeyID: 'whatever');

Setting Password

Passwords can be set (or changed) on a PFX by calling $pfx->setPassword('password'). By default, phpseclib uses the empty string as the password, which yields an encrypted, MAC'd PFX — this is what most other software means by a "passwordless" PFX.

Passwords can be removed from a PFX by calling $pfx->removePassword(). Note that $pfx->setPassword(null) and a bare $pfx->setPassword() do the same thing; to get an empty password you have to write $pfx->setPassword('') explicitly. Password-less PFXs do not have a MAC (as MACs require a password) and are not widely supported by other softwares. 1

Set the password before calling add() where you can, since add() bag-types things according to the password state at the time: private keys go into a PKCS8ShroudedKeyBag when a password is set and a plain KeyBag when one isn't. Setting the password afterwards re-bags everything and works fine, but going from no password to a password is the more involved path.

Setting MAC Parameters

MAC Parameters can be set globally thusly:

PFX::setHashAlgorithm('sha256');
PFX::setIterationCount(1024);
PFX::setSaltLength(32);

Or they can be set on a per key basis thusly:

echo $key
->withPassword('demo')
->toString('PKCS8', [
'hashAlgorithm' => 'sha256',
'iterationCount' => 1024,
'saltLength' => 32
]);

PFX::toString() accepts the same three options, overriding the global defaults for that one call without changing them:

echo $pfx->toString([
'hashAlgorithm' => 'sha256',
'iterationCount' => 1024,
'saltLength' => 32
]);

These apply when phpseclib writes a PFX. Reading one uses whatever parameters the file itself specifies.

Valid hash algorithms are:

  • md2
  • md5
  • sha1
  • sha224
  • sha256
  • sha384
  • sha512/224
  • sha512/256

Creating signatures

Signatures can be created from strings or from objects implementing the phpseclib4\File\Common\Signable interface.

Example:

$pfx->sign($x509);

To sign with a PFX, the PFX must contain either:

  • Exactly one private key, no cert. Behaves like signing with a bare PrivateKey — no auto-fill of issuer info.
  • Exactly one private key and exactly one matching X509. The X509's public key must match the private key's public side. In this case sign() additionally copies the CA cert's subject DN as the target's issuer DN, and the CA cert's subjectKeyIdentifier as the target's authorityKeyIdentifier.

A PFX with more than one cert/key pair, or with non-matching cert and key, will throw \phpseclib4\Exception\InvalidArgumentException when you try to sign with it. If you have a PFX containing multiple identities, use pfxFromFriendlyName() or pfxFromLocalKeyID() to extract the specific pair you want to sign with first.

Footnotes

  1. KeyStore Explorer 5.6.1, for instance, cannot open them at all. See also this Feb 2022 feature request for OpenSSL or this Aug 2016 issue for .NET.