Skip to main content
Version: 3.0

X.509

Reading Certificates

use phpseclib3\File\X509;

$x509 = new X509();
$cert = $x509->loadX509(file_get_contents('google.crt'));

var_dump($cert);
(download google.crt)

Running the above will produce an array that looks something like this:

tbsCertificate
version
v3
serialNumber
105827261859531100510423749949966875981
signature
algorithm
sha1WithRSAEncryption
parameters
null
issuer
rdnSequence
0
0
type
id-at-countryName
value
printableString
ZA
1
0
type
id-at-organizationName
value
printableString
Thawte Consulting (Pty) Ltd.
2
0
type
id-at-commonName
value
printableString
Thawte SGC CA
validity
notBefore
utcTime
Wed, 26 Oct 2011 00:00:00 +0000
notAfter
utcTime
Mon, 30 Sep 2013 23:59:59 +0000
subject
rdnSequence
0
0
type
id-at-countryName
value
printableString
US
1
0
type
id-at-stateOrProvinceName
value
printableString
California
2
0
type
id-at-localityName
value
teletexString
Mountain View
3
0
type
id-at-organizationName
value
teletexString
Google Inc
4
0
type
id-at-commonName
value
teletexString
subjectPublicKeyInfo
algorithm
algorithm
rsaEncryption
parameters
null
subjectPublicKey
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDetyZDppmFzTinFQm5zw/Jw1WM
iO6MjSgnJEsqXqDYFvphGEvPbWCA0zVAMnLAjxLY5U6PubL22RVeWoYxo7qGqmvI
2XGMzM0nEx6dQl049qes7/pi8xiB1CRGfwF3fMYqiRSZu5g5HagZ+zkARH0blGp4
LWmtwHos+tDaIBKY0wIDAQAB
-----END PUBLIC KEY-----
extensions
0
extnId
id-ce-basicConstraints
critical
1
extnValue
cA
1
extnId
id-ce-cRLDistributionPoints
critical
extnValue
0
distributionPoint
fullName
0
uniformResourceIdentifier
2
extnId
id-ce-extKeyUsage
critical
extnValue
0
id-kp-serverAuth
1
id-kp-clientAuth
2
2.16.840.1.113730.4.1
3
extnId
id-pe-authorityInfoAccess
critical
extnValue
0
accessMethod
id-ad-ocsp
accessLocation
uniformResourceIdentifier
1
accessMethod
id-ad-caIssuers
accessLocation
signatureAlgorithm
algorithm
sha1WithRSAEncryption
parameters
null
signature
...

getDNProp()

print_r($x509->getDNProp('CN'));

That will produce the following:

An array is returned because each distinguished name property can (in theory) have multiple values

Valid property names are enumerated upon at Distinguished Property Names.

getIssuerDNProp() returns the issuer distinguished name as opposed to the subject distinguished name.

getDN()

print_r($x509->getDN());

getDN() / getIssuerDN() accept several different parameters:

  • X509::DN_ARRAY (the default value) returns an array who's keys are based on the ASN.1 syntax for X.509:

    rdnSequence
    0
    0
    type
    id-at-countryName
    value
    printableString
    US
    1
    0
    type
    id-at-stateOrProvinceName
    value
    printableString
    California
    2
    0
    type
    id-at-localityName
    value
    teletexString
    Mountain View
    3
    0
    type
    id-at-organizationName
    value
    teletexString
    Google Inc
    4
    0
    type
    id-at-commonName
    value
    teletexString
  • X509::DN_STRING returns an OpenSSL-style string:

    C=US, ST=California, L=Mountain View, O=Google Inc, CN=www.google.com
  • X509::DN_OPENSSL returns an OpenSSL-style array:

    C
    US
    ST
    California
    L
    Mountain View
    O
    Google Inc
    CN
  • X509::DN_ASN1 returns a DER encoded binary string

  • X509::DN_CANON returns a "canonicalized" DER encoded binary string wherein SEQUENCE around RDNs and all string values normalized as trimmed lowercase UTF-8 with all spacing as one blank. Constructed RDNs are not canonicalized.

getPublicKey()

echo $x509->getPublicKey();

Returns a \phpseclib3\Crypt\Common\PublicKey object that, by default, gets cast to a PKCS8-encoded public key:

-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDetyZDppmFzTinFQm5zw/Jw1WM
iO6MjSgnJEsqXqDYFvphGEvPbWCA0zVAMnLAjxLY5U6PubL22RVeWoYxo7qGqmvI
2XGMzM0nEx6dQl049qes7/pi8xiB1CRGfwF3fMYqiRSZu5g5HagZ+zkARH0blGp4
LWmtwHos+tDaIBKY0wIDAQAB
-----END PUBLIC KEY-----

Validating Certificates

Signatures

$x509 = new X509();
$x509->loadCA('...');
$cert = $x509->loadX509('...');
echo $x509->validateSignature() ? 'valid' : 'invalid';

Certificate authority certificates can be downloaded from curl - SSL CA Certificates. Parsing that is left as an exercise to the reader.

Note that when the certificate being validated is issued by a CA that isn't already in your trust store, phpseclib will, by default, read any caIssuers URL from the certificate's Authority Information Access (AIA) extension and connect to it to try to download the missing intermediate. Because that URL comes from the certificate itself, a maliciously crafted certificate can point it at an address on the host's own network (eg. loopback, a cloud metadata endpoint such as 169.254.169.254, an internal-only service, etc) turning validation into a server-side request forgery vector.

You can disable this behavior entirely with X509::disableURLFetch(). As of 3.0.54 you can instead keep fetching enabled but restrict where phpseclib will connect, by registering a callback that approves or rejects each destination:

X509::setURLFetchCallback(function (string $host, string $ip, int $port, string $scheme): bool {
if ($scheme !== 'http' && $scheme !== 'https') {
return false;
}

$public = filter_var(
$ip,
FILTER_VALIDATE_IP,
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
);

return $public !== false; // public address -> allow; internal/reserved -> deny
});

The callback receives the resolved IP, host, port, and scheme, and returns true to allow the connection or false to block it. The example above permits only publicly-routable addresses. If instead you run an internal CA (eg. its AIA endpoint lives on 10.x.x.x) you'd invert the logic and allow only your specific internal range.

Self-signed Signatures

$x509 = new X509();
$cert = $x509->loadX509('...');
echo $x509->validateSignature(false) ?
'valid' :
'invalid';

URLs

$x509 = new File_X509();
$cert = $x509->loadX509('...');
echo $x509->validateURL('https://www.domain.tld/path/to/whatever.ext') ?
'valid' :
'invalid';

Dates

$x509 = new X509();
$cert = $x509->loadX509('...');
echo $x509->validateDate() ? 'valid' : 'invalid';

By default validateDate checks to see if the current date is within the date range. If, however, you want to use a custom date you may do so by either passing a string (eg. validateDate('January 1, 2001')) or a DateTime object (eg. validateDate(new DateTime('January 1, 2001'))).

Chains

Let's say you have a certificate chain that has at least one intermediate cert and let's further say that you wanted to see all the certs in this chain. To do that you could do $x509>getChain(). That'll return an array of X509 objects who's first element is the current cert and who's last element is the CA cert.

If you want to retrieve the array contents of any cert in the chain you'll need to do $x509->getCurrentCert().

Creating Certificates

The General Idea

In a nutshell...

$result = $x509->sign($issuer, $subject); 
$x509->saveX509($result);

Public / Private Keys

$issuer and $subject are, themselves, X509 objects. They need private and public keys, respectively. Here's an example of how to create them from scratch for a brand new self-signed cert:

$privKey = RSA::createKey();
$pubKey = $privKey->getPublicKey();

$issuer->setPrivateKey($privKey);
$subject->setPublicKey($pubKey);

The public key can also be set by $subject->loadCSR('...') or $subject->loadX509('...'). If you're using $subject->loadX509() you'll effectively be re-signing $subject. The issuer DN of $subject will be updated as will some of the other "transactional properties" (see below), if appropriate, but all (well, most) of the extensions will be preserved.

Note that whereas in the 1.0 and 2.0 branches, the signature algorithm was set by using an additional parameter in the sign method (or signCSR, signCRL, etc) in this case it's set based on the key. So if you want to create an rsaEncryption X509 cert you'd need to do $publicKey = $publicKey->withHashing(RSA::SIGNATURE_PKCS1) since, by default, RSA keys use RSA::SIGNATURE_PSS.

Key Identifiers

Sometimes an $issuer or a $subject will have multiple private keys. To disambiguate between them key identifiers are used. Quoting RFC5280 § 4.2.1.1, "the value of the keyIdentifier field SHOULD be derived from the public key used to verify the certificate's signature or a method that generates unique values". An example follows:

$issuer->setKeyIdentifier($issuer->computeKeyIdentifier($pubKey));

Note that computeKeyIdentifier is already called by saveX509() unless you've specifically over-written the key identifier.

Distinguished Names

$issuer and $subject also need to have their distinguished names set. That can be done in a number of ways. The following are all equivalent to one another:

$subject->setDNProp('id-at-organizationName', 'phpseclib demo cert'); 
$subject->setDN(
array(
'rdnSequence' => array(
array(
array(
'type' => 'id-at-organizationName',
'value'=> 'phpseclib demo cert'
)
)
)
)
);
$subject->setDN(array(
'O' => 'phpseclib demo cert'
));
$subject->setDN('/O=phpseclib demo cert');

DN properties can also be removed thusly:

$subject->removeDNProp('id-at-organizationName');

DN properties can be set via $subject->loadCSR('...') and $subject->loadX509('...') as well.

In the case of $subject->setDN().. the value will be assumed to be utf8String unless you explicitely tell phpseclib to set it to another type by doing something like 'value'=> array('ia5String' => 'phpseclib demo cert').

See also: List of DN property names

Domain Names

If your X.509 certificate is supposed to be valid for a single domain name that domain name can be set by either setting the id-at-commonName or CN distinguished name property. If your X.509 certificate is supposed to be valid for multiple domain names an X.509 extension - id-ce-subjectAltName - is required. setDomain() takes care of all of this for you. Here's an example of how to make your certificate valid for two domains:

$subject->setDomain('www.google.com', 'www.yahoo.com');

Transactional Properties

Some properties belong neither to $issuer or $subject. Rather they belong to "transaction". The functions to set those properties are as follows:

$x509->setSerialNumber(...);
$x509->makeCA();
$x509->setStartDate(...);
$x509->setEndDate(...);

$subject->setDomain() isn't considered a transactional property because, in theory, the issuing certificate could have at least one domain too since that's based on a particular distinguished name property.

The start time is, by default, when the cert is created. The current time is converted to UTC time and the fact that it's UTC time is denoted in the cert. Other X.509 decoders (eg. browsers or email clients or whatever) should decode this to their timezone so there's no need to set it to do $x509->setStartDate('-1 day') or anything like that.

The end date, by default, is one year from the current time.

setStartDate() / setEndDate() can accept strings or DateTime objects. If you want the cert to last forever pass 'lifetime' to it.

Function List

Functions affecting properties common to both $issuer and $subject are as follows:

  • setDNProp
  • removeDNProp
  • setDN
  • loadCSR
  • loadX509
  • setKeyIdentifier / computeKeyIdentifier

Functions affecting $issuer only properties:

  • setPrivateKey

Functions affecting $subject only properties:

  • setPublicKey
  • setDomain

Functions affecting $x509 properties:

  • setSerialNumber
  • makeCA
  • setStartDate
  • setEndDate

Minimalistic Example

$subject = new X509(); 
$subject->setPublicKey($pubKey); // $pubKey is a PublicKey object
$subject->setDN('/O=phpseclib demo cert');

$issuer = new X509();
$issuer->setPrivateKey($privKey); // $privKey is a PrivateKey object
$issuer->setDN('/O=phpseclib demo cert');

$x509 = new X509();
$result = $x509->sign($issuer, $subject);
echo $x509->saveX509($result);

For self-signed certs the DN of the subject and issuer will match and the issuer's private key will correspond to the subject's public key.

Example: Self-signed cert

use phpseclib3\File\X509;
use phpseclib3\Crypt\RSA;

// create private key / x.509 cert for stunnel / website
$CAPrivKey = RSA::createKey();
$CAPubKey = $CAPrivKey->getPublicKey();

$CASubject = new X509;
$CASubject->setDNProp('id-at-organizationName', 'phpseclib CA cert');
$CASubject->setPublicKey($CAPubKey);

$CAIssuer = new X509;
$CAIssuer->setPrivateKey($CAPrivKey);
$CAIssuer->setDN($CASubject->getDN());

$x509 = new X509;
$x509->makeCA();
$result = $x509->sign($CAIssuer, $CASubject);
echo "private key for CA cert (can be discarded):\r\n\r\n";
echo $CAPrivKey;
echo "\r\n\r\nCA cert to be imported into browser:\r\n\r\n";
echo $x509->saveX509($result);
echo "\r\n";

The cert that this script creates is a CA cert. If you don't want it to be a CA cert you can comment out the $x509->makeCA() line.

Example: CA-signed cert

The following code requires the previous code to work and should follow the code in the previous section.

$privKey = RSA::createKey();
$pubKey = $privKey->getPublicKey();

$subject = new X509;
$subject->setDNProp('id-at-organizationName', 'phpseclib demo cert');
$subject->setPublicKey($pubKey);

$x509 = new X509;
$result = $x509->sign($CAIssuer, $subject);
echo "\r\nthe stunnel.pem contents are as follows:\r\n\r\n";
echo $privKey;
echo "\r\n";
echo $x509->saveX509($result);
echo "\r\n";

Example: Arbitrary and Custom Extensions

phpseclib can be used to implement add arbitrary extensions as well.

Building off of the previous example (using the phpBB MOD Text Template):

#
#-----[ FIND ]------------------------------------------
#
$result = $x509->sign($issuer, $subject);
#
#-----[ BEFORE, ADD ]-----------------------------------
#
$x509->setExtensionValue('id-pe-authorityInfoAccess', [
[
"accessMethod" => "id-ad-ocsp",
"accessLocation" => [
"uniformResourceIdentifier" => 'https://ocsp.test.ca/',
]
],
[
"accessMethod" => "id-ad-caIssuers",
"accessLocation" => [
"uniformResourceIdentifier" => 'https://crt.test.ca/test.crt',
],
],
], true);

The first two parameters are pretty self explanatory. The third parameter is used to denote if the extension is critical or not. A fourth parameter - $replace - also exists if you want this extension to replace an existant extension with the same id as the one you're adding. This parameter defaults to false (don't replace) but can also be set to true (do replace).

Now let's say you wanted to include not just an arbitrary extension but a custom one as well (eg. one that phpseclib doesn't have pre built-in support for). That can be done thusly:

#
#-----[ FIND ]------------------------------------------
#
use phpseclib3\File\X509;
use phpseclib3\Crypt\RSA;
#
#-----[ AFTER, ADD ]------------------------------------
#
use phpseclib3\File\ASN1;

$customExtensionName = 'cust';
$customExtensionNumber = '2.16.840.1.101.3.4.2.99';

ASN1::loadOIDs([
$customExtensionName => $customExtensionNumber,
]);

X509::registerExtension($customExtensionName, [
'type' => ASN1::TYPE_SEQUENCE,
'children' => [
'toggle' => ['type' => ASN1::TYPE_BOOLEAN],
'num' => ['type' => ASN1::TYPE_INTEGER],
'name' => ['type' => ASN1::TYPE_OCTET_STRING],
'list' => [
'type' => ASN1::TYPE_SEQUENCE,
'min' => 0,
'max' => -1,
'children' => ['type' => ASN1::TYPE_OCTET_STRING],
],
],
]);
#
#-----[ FIND ]------------------------------------------
#
$result = $x509->sign($issuer, $subject);
#
#-----[ BEFORE, ADD ]-----------------------------------
#
$x509->setExtensionValue($customExtensionName, $customExtensionData);