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);
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
authSafe
contentType
phpseclib4\File\ASN1\Types\OID
content
0
contentType
phpseclib4\File\ASN1\Types\OID
content
0
bagId
phpseclib4\File\ASN1\Types\OID
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
content
0
bagId
phpseclib4\File\ASN1\Types\OID
bagValue
certId
phpseclib4\File\ASN1\Types\OID
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
digest
phpseclib4\File\ASN1\Types\OctetString
macSalt
phpseclib4\File\ASN1\Types\OctetString
iterations
phpseclib4\File\ASN1\Types\Integer
An unencrypted PFX (eg. pfx2.der) looks like this:
version
phpseclib4\File\ASN1\Types\Integer
authSafe
contentType
phpseclib4\File\ASN1\Types\OID
content
0
contentType
phpseclib4\File\ASN1\Types\OID
content
0
bagId
phpseclib4\File\ASN1\Types\OID
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
content
0
bagId
phpseclib4\File\ASN1\Types\OID
bagValue
certId
phpseclib4\File\ASN1\Types\OID
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
authSafe
contentType
phpseclib4\File\ASN1\Types\OID
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'ssubjectKeyIdentifieras the target'sauthorityKeyIdentifier.
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
-
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. ↩