Skip to main content
Version: 4.0

X.509

X.509 certificates are basically wrappers around public keys with meta information that's interpreted in application specific contexts.

Reading Certificates

use phpseclib4\File\X509;

$x509 = X509::load(file_get_contents('google.crt'));

print_r($x509);
(download google.crt)

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

tbsCertificate
version
phpseclib4\File\ASN1\Types\Integer
v3
serialNumber
phpseclib4\File\ASN1\Types\Integer
105827261859531100510423749949966875981
signature
algorithm
phpseclib4\File\ASN1\Types\OID
sha1WithRSAEncryption
issuer
rdnSequence
0
0
type
phpseclib4\File\ASN1\Types\OID
id-at-countryName
value
phpseclib4\File\ASN1\Types\PrintableString
ZA
1
0
type
phpseclib4\File\ASN1\Types\OID
id-at-organizationName
value
phpseclib4\File\ASN1\Types\PrintableString
Thawte Consulting (Pty) Ltd.
2
0
type
phpseclib4\File\ASN1\Types\OID
id-at-commonName
value
phpseclib4\File\ASN1\Types\PrintableString
Thawte SGC CA
validity
notBefore
utcTime
phpseclib4\File\ASN1\Types\UTCTime
2011-10-26 00:00:00
notAfter
utcTime
phpseclib4\File\ASN1\Types\UTCTime
2013-09-30 23:59:59
subject
rdnSequence
0
0
type
phpseclib4\File\ASN1\Types\OID
id-at-countryName
value
phpseclib4\File\ASN1\Types\PrintableString
US
1
0
type
phpseclib4\File\ASN1\Types\OID
id-at-stateOrProvinceName
value
phpseclib4\File\ASN1\Types\PrintableString
California
2
0
type
phpseclib4\File\ASN1\Types\OID
id-at-localityName
value
phpseclib4\File\ASN1\Types\TeletexString
Mountain View
3
0
type
phpseclib4\File\ASN1\Types\OID
id-at-organizationName
value
phpseclib4\File\ASN1\Types\TeletexString
Google Inc
4
0
type
phpseclib4\File\ASN1\Types\OID
id-at-commonName
value
phpseclib4\File\ASN1\Types\TeletexString
subjectPublicKeyInfo
phpseclib4\Crypt\RSA\PublicKey
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDetyZDppmFzTinFQm5zw/Jw1WM
iO6MjSgnJEsqXqDYFvphGEvPbWCA0zVAMnLAjxLY5U6PubL22RVeWoYxo7qGqmvI
2XGMzM0nEx6dQl049qes7/pi8xiB1CRGfwF3fMYqiRSZu5g5HagZ+zkARH0blGp4
LWmtwHos+tDaIBKY0wIDAQAB
-----END PUBLIC KEY-----
extensions
0
extnId
phpseclib4\File\ASN1\Types\OID
id-ce-basicConstraints
critical
phpseclib4\File\ASN1\Types\Boolean
true
extnValue
cA
phpseclib4\File\ASN1\Types\Boolean
false
1
extnId
phpseclib4\File\ASN1\Types\OID
id-ce-cRLDistributionPoints
critical
phpseclib4\File\ASN1\Types\Boolean
false
extnValue
0
distributionPoint
fullName
0
uniformResourceIdentifier
phpseclib4\File\ASN1\Types\IA5String
2
extnId
phpseclib4\File\ASN1\Types\OID
id-ce-extKeyUsage
critical
phpseclib4\File\ASN1\Types\Boolean
false
extnValue
0
phpseclib4\File\ASN1\Types\OID
id-kp-serverAuth
1
phpseclib4\File\ASN1\Types\OID
id-kp-clientAuth
2
phpseclib4\File\ASN1\Types\OID
2.16.840.1.113730.4.1
3
extnId
phpseclib4\File\ASN1\Types\OID
id-pe-authorityInfoAccess
critical
phpseclib4\File\ASN1\Types\Boolean
false
extnValue
0
accessMethod
phpseclib4\File\ASN1\Types\OID
id-ad-ocsp
accessLocation
uniformResourceIdentifier
phpseclib4\File\ASN1\Types\IA5String
1
accessMethod
phpseclib4\File\ASN1\Types\OID
id-ad-caIssuers
accessLocation
uniformResourceIdentifier
phpseclib4\File\ASN1\Types\IA5String
signatureAlgorithm
algorithm
phpseclib4\File\ASN1\Types\OID
sha1WithRSAEncryption
signature
phpseclib4\File\ASN1\Types\BitString
0021acd5aeca34895ac2ab52d2b234669d7aabeee67cd57ec25c28bb7400c9101f4213fc698a1e24a00200e9ba5bca1904b2d3af01b27e5f14dba6db52b99af3127f7ca29c3b6f997dea500d762312fff7667329b7950aadd88bb2de20e90a70641108c85af17d9eec69a5a5d582d7271e9e56cdd276d5792bf725431c69f0b8f9

All elements and subelements of the above X509 object can be accessed as array elements vis-a-vis ArrayAccess. As a consequence of this there are two ways to get (for example) the public key. You could do $x509['tbsCertificate']['subjectPublicKeyInfo'] or you could do $x509->getPublicKey(). It's these helper functions that set the X509 class apart from \phpseclib4\File\ASN1\Constructed.

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

getPublicKey()

Returns an instance of \phpseclib4\Crypt\Common\PublicKey.

echo $x509->getPublicKey();

Running the above will output the following:

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

Note that if the key is in an unsupported format then $x509->getPublicKey() will throw a UnexpectedValueException. If you want to see what the public key is, regardless of whether or not it's in a supported format, then you can do $x509['tbsCertificate']['subjectPublicKeyInfo'].

Very rarely, X.509 certificates may not have a public key at all. You can test for this by doing $x509->hasPublicKey(), which returns a boolean based on whether or not the certificate in question has a public key or not.

listExtensions()

"Extensions can convey such data as additional subject identification information, key attribute information, policy information, and certification path constraints." -- RFC5280 pp. 10

print_r($x509->listExtensions());

Running the above will produce an array with all the extensions that are in the certificate:

0
id-ce-basicConstraints
1
id-ce-cRLDistributionPoints
2
id-ce-extKeyUsage
3
id-pe-authorityInfoAccess

getExtension()

Returns an array representing the first instance of an extension even if there are multiple instances. In this array extnId is a string, critical is a boolean and extnValue is an instance of phpseclib4\File\ASN1\Types\BaseType.

If said extension isn't present then null is returned.

print_r($x509->getExtension('id-ce-cRLDistributionPoints'));

Since id-ce-cRLDistributionPoints exists in $x509, running the above will produce an array with three elements:

extnId
id-ce-cRLDistributionPoints
extnValue
0
distributionPoint
fullName
0
uniformResourceIdentifier
phpseclib4\File\ASN1\Types\IA5String
critical
false

You can search by the extension name or the OID for the extension (2.5.29.31, in this case).

If you just want to see if the extension exists you could do getExtension() !== null or you could call hasExtension():

echo $x509->hasExtension('id-ce-cRLDistributionPoints') ? 't' : 'f';

hasExtension() works largely the same way that getExtension() does, except that it returns true or false depending on if the extension exists or not.

getDN() / getDNProps()

An X.509 certificate has a subject and an issuer distinguished name (DN for short). For self signed certs, when the subject and issuer DNs are the same, you can call getDN(), hasDNProp() and getDNProps().

If the subject and issuer DNs are different then calling getDN(), hasDNProp() or getDNProps() will throw an exception and you'll need to call either the subject or issuer versions of those methods instead. eg.

Subject DN Methods:

Issuer DN Methods:

Learn more about how DNs work in phpseclib at Deep Dive: Distinguished Names (DNs).

Creating Certificates

Minimalistic Certificate

Consider the following:

use phpseclib4\File\X509;

$x509 = new X509();
echo $x509;

The certificate that that would produce would look something like this:

-----BEGIN CERTIFICATE-----
MEUwO6ADAgECAgEBMAMGAQAwADAeFw03MDAxMDEwMDAwMDBaFw03MTAxMDEwMDAw
MDBaMAAwCDADBgEAAwEAMAMGAQADAQA=
-----END CERTIFICATE-----

This certificate has no extensions, does not have a subject or an issuer distinguished name, does not have a signature and it does not have a public key.

If you saved this certificate as test.pem and then did openssl x509 -noout -text -in test.pem on that certificate you'd get this:

Certificate:
Data:
Version: 3 (0x2)
Serial Number: 1 (0x1)
Signature Algorithm: 0.0
Issuer:
Validity
Not Before: Jan 1 00:00:00 1970 GMT
Not After : Jan 1 00:00:00 1971 GMT
Subject:
Subject Public Key Info:
Public Key Algorithm: 0.0
Unable to load Public Key
4047192EA37F0000:error:03000072:digital envelope routines:X509_PUBKEY_get0:decode error:../crypto/x509/x_pubkey.c:458:
4047192EA37F0000:error:03000072:digital envelope routines:X509_PUBKEY_get0:decode error:../crypto/x509/x_pubkey.c:458:
Signature Algorithm: 0.0
Signature Value:

If you did print_r($x509) on that certificate you'd get this:

tbsCertificate
version
phpseclib4\File\ASN1\Types\Integer
v3
serialNumber
phpseclib4\Math\BigInteger
1
signature
algorithm
phpseclib4\File\ASN1\Types\OID
0.0
issuer
rdnSequence
validity
notBefore
utcTime
phpseclib4\File\ASN1\Types\UTCTime
1970-01-01 00:00:00
notAfter
utcTime
phpseclib4\File\ASN1\Types\UTCTime
1971-01-01 00:00:00
subject
rdnSequence
subjectPublicKeyInfo
algorithm
algorithm
phpseclib4\File\ASN1\Types\OID
0.0
subjectPublicKey
phpseclib4\File\ASN1\Types\BitString
00
signatureAlgorithm
algorithm
phpseclib4\File\ASN1\Types\OID
0.0
signature
phpseclib4\File\ASN1\Types\BitString
00

That said, even when doing this, there are 3x things that are (probably) going to be different every time:

  1. The serial number. It's set to be a random 160-bit positive number by default.

    If you want to set your own serial number you can do so by calling $x509->setSerialNumber(). That function has one required parameter - $serial - and one optional parameter - $base. $serial can be a string or an instance of phpseclib4\Math\BigInteger. If it's a BigInteger then $base is ignored. If it's a string then that string will be interpreted as a base-256 number by default unless you change the value of $base.

  2. The start date. It's set to the current time by default.

    If you want to set your own start date you can do so by calling $x509->setStartDate(). That function has one required parameter - $date - which can be either an instance of \DateTimeInterface or a string. If it's a string it'll passed to the constructor of \DateTime and parsed by that.

  3. The end date. It's set to one year from the current time by default.

    If you want to set your own start date you can do so by calling $x509->setEndDate(). That function has one required parameter - $date - which can be either an instance of \DateTimeInterface or a string. If it's a string it'll be passed to the constructor of \DateTime and parsed by that.

    If $date is set to lifetime then the end date will be set to December 31, 9999 AD 23:59:59 UTC. Per RFC5280 § 4.1.2.5 Validity that value indicates "that a certificate has no well-defined expiration date".

    Note that the CA/Browser Forum currently recommends server certificates have an end date between 8 and 25 years out.

Setting the Public Key

Let's say you created your public key thusly:

use phpseclib4\Crypt\EC;

$private = EC::createKey('nistp256');
$public = $private->getPublicKey();

At this point you could set the public key for the X.509 certificate using any number of techniques:

$x509 = new X509($public);
echo $x509;
$x509 = new X509();
$x509->setPublicKey($public);
echo $x509;
use phpseclib4\File\CSR;

$csr = new CSR($public);
$x509 = new X509($csr);
echo $x509;

If you set the public key via a CSR then, if the CSR also had a subject DN set and extensions requested, those would also be auto set as well. The section From a CSR elaborates.

You can also remove a public key be calling $x509->removePublicKey().

Setting DNs

An X.509 certificate has a subject and an issuer distinguished name (DN for short). For self signed certs, when the subject and issuer DNs are the same, you can call setDN(), resetDN(), addDNProp(), addDNProps() and removeDNProps()

If the subject and issuer DNs are different then calling setDN(), resetDN(), addDNProp(), addDNProps() or removeDNProps() will throw an exception and you'll need to call either the subject or issuer versions of those methods instead. eg.

Subject DN Methods:

Issuer DN Methods:

Additionally, the subject DN can be set by passing an instance of \phpseclib4\File\CSR to the X509 constructor.

use phpseclib4\File\CSR;

$csr = CSR::load('...');
$x509 = new X509($csr);
echo $x509;

The section From a CSR elaborates.

Likewise, the issuer DN can be set by signing the X.509 certicate with a PFX. eg. $pfx->sign($x509). The Signing section elaborates.

Setting Extensions

Some extensions have built in dedicated functions for manipulating them:

  • id-ce-subjectAltName

    This extension can be modified by calling $x509->addDomains() and $x509->addIPAddresses(). Those functions can accept an arbitrary number of parameters. Each parameter is added as a new domain name or IP address, respectively.

    In the case of $x509->addDomains(), if the subject DN does not already have id-at-commonName set then $x509->addDomains() will set it to the first domain name that you passed to it.

  • id-ce-authorityKeyIdentifier and id-ce-subjectKeyIdentifier

    These extensions can be modified by calling $x509->setAuthorityKeyIdentifier() and $x509->setSubjectKeyIdentifier(), respectively. Both functions have one parameter - $value - that can be either a string or an instance of \phpseclib4\File\ASN1\Types\OctetString.

    RFC5280 § 4.2.1.2. Subject Key Identifier defines two methods for automatically generating id-ce-subjectKeyIdentifier. $x509->createSubjectKeyIdentifier(), by default, uses the first method, however, if you want the second method to be used, you can call $x509->createSubjectKeyIdentifier(2) instead.

    RFC5280 § 4.2.1.1. Authority Key Identifier explains the purpose behind these extensions. In particular, they're used when "an issuer has multiple signing keys (either due to multiple concurrent key pairs or due to changeover)".

  • id-ce-keyUsage and id-ce-basicConstraints

    These extensions can be modified by calling $x509->makeCA() which, in turn, sets the cRLSign, keyCertSign and digitalSignature bits in the KeyUsage bitstring and sets the cA boolean of BasicConstraints to true.

Any extension can be set by calling $x509->setExtension(). $x509->setExtension() has two required parameters - $name (a string) and $value (which can be any number of primitive types or object types) - and one optional parameter - $critical. If $critical isn't explicitly set it'll be either bool(true) or bool(false) based on what RFC5280 says about it. Like some extensions are only ever supposed have critical set to either bool(false) or bool(true) and others can be set to one or the other depending on their specific situation. If you manually set an extension to bool(true) that RFC5280 says should only ever be bool(false) phpseclib will let you do so.

If an extension is present multiple times (which really shouldn't ever happen) then phpseclib will only modify the first occurance of that extension.

To remove all occurances of an extension one need simply call $x509->removeExtension('whatever').

Here's an example of an extension being set:

$x509->setExtension('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',
],
],
]);

Extensions can also be set from CSRs. The section From a CSR elaborates.

Custom Extensions

If you wanted to set a custom proprietary extension that phpseclib didn't have built in support for you can do this:

$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],
],
],
]);

$x509->setExtension($customExtensionName, [
'toggle' => true,
'num' => 5,
'name' => "\0\1\2test",
'list' => [
'hello',
'world',
]
]);

Once an extension is registered you can call X509::getRegisteredExtension($customExtensionName) to see the definition for that extension.

From a CSR

When creating an X.509 certificate from a CSR the following things will be copied from the CSR to the X509 object:

  • the public key
  • the subject DN
  • any requested extensions

Some of the things that are copied over, by default, shouldn't be copied over verbatim. Like if you're a CA you probably don't want to the id-ce-subjectAltName extension or the id-at-commonName to be for an unauthorized domain name.

Here's an example of how some of the questionable fields could be removed and, in one case, full on replaced:

use phpseclib4\File\CSR;

$csr = CSR::load('...');
$x509 = new X509($csr);
$x509->removeSubjectDNProps('id-at-commonName');
$x509->removeExtension('id-ce-subjectAltName');
$x509->removeExtension('id-ce-basicConstraints');
$x509->removeExtension('id-ce-keyUsage');
$x509->removeExtension('id-ce-extKeyUsage');
$x509->removeExtension('id-ce-cRLDistributionPoints');
$x509->removeExtension('id-pe-authorityInfoAccess');
$x509->addDomains($authorizedDomain);
$pfx->sign($x509);
echo $x509;

What the removed extensions ought to be replaced with - if they're to be replaced at all - depends on how the X.509 certificate is going to be used.

$pfx->sign($x509) is discussed in the next section, Signing.

Signing

Certificates can be signed with private key objects or PFX objects.

Here's an example of how you'd create a self signed CA cert with a private key object:

use phpseclib4\Crypt\EC;
use phpseclib4\File\X509;

$CAprivate = EC::createKey('nistp256');
$CApublic = $CAprivate->getPublicKey();

$CAX509 = new X509($CApublic);
$CAX509->makeCA();
$CAX509->setDN('O=whatever');
$CAprivate->sign($CAX509);

Here's an example of how you'd create a CA signed cert with a PFX object that builds on the above:

use phpseclib4\File\PFX;

$pfx = new PFX();
$pfx->add($CAprivate);
$pfx->add($CAX509);

$private = EC::createKey('nistp256');
$public = $private->getPublicKey();

$x509 = new X509($public);
$x509->setDN('O=child organization');
$pfx->sign($x509);

This will sign the certificate and auto-set the issuer DN's to match those of the X.509 that is in the PFX.

Signing the certifcate before the DNs or extensions or whatever have been set will result in the certificate having a bad signature. Like modifications done after the signature was added do not automatically result in the signature being updated to reflect those changes.

Output Format

By default, X509 objects, when cast to a string, are converted to base64-encoded PEMs. To make it so that casting to a string produces a binary encoded DERs one need simply call X509::enableBinaryOutput(). To go back to PEMs calling X509::disableBinaryOutput() is sufficient.

You can also convert an X509 object to binary by doing $x509->toString(['binary' => true]).

Validating Certificates

Signatures

X509::addCA('...');
$x509 = X509::load('...');
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.

validateSignature() takes one optional parameter - $caonly (a boolean that defaults to true) - which controls how self-signed certs are handled:

  • true (default): a self-signed cert returns false unless that cert is also in the CA store. This is what you want for normal verification flows.
  • false: a self-signed cert verifies if its signature is valid against its own public key. Useful for inspecting or accepting self-signed certs without first loading them as CAs.

addCA() can be called more than once to build up a trust store; each call appends a new trust root. It accepts a PEM/DER string, the same nested-array structure that ASN1::decodeBER() returns, a \phpseclib4\File\ASN1\Constructed instance, or an already-loaded X509 object. To inspect or reset the store there's X509::getCAs(), which returns the current store as an array of X509 objects, and X509::clearCAStore(), which removes all loaded CAs (most useful in tests).

validateSignature() takes the date into consideration when validating the signature. If you want to change the date to some date other than the current date you can do so by calling X509::setTargetValidationDate(). That function has one required parameter - $date - which can be either an instance of \DateTimeInterface or a string. If it's a string it'll passed to the constructor of \DateTime and parsed by that.

To retrieve the date currently being used to perform said validation one can call X509::getTargetValidationDate(), which returns either an instance of \DateTimeInterface or a string

When validating a signature it may be necessary to download intermediate certs from URI's. To do so one needs to provide a callback to X509::setURLFetchCallback(). eg.

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.

While it may be tempting to just have that function always return true (it would certainly be simpler!) doing so would enable a maliciously crafted certificate to point to 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.

Additionally, an intermediate certificate that linked to itself would result in an infinite loop so to prevent that we set a recursion limit. A negative number means that there is no recursion limit. That can be done by calling X509::setRecurLimit($count), where $count is an integer that defaults to 5.

To validate a certificate against certificate revocation lists one would need to pass a custom anonymous function to X509::setCRLLookupCallback().

By default the key usage is checked when a signature is validated. Like is the issuer actually authorized to sign children certificates? To disable this check one need simply call X509::ignoreKeyUsage(). To re-enable this check one need simply call X509::checkKeyUsage(). The current state can be queried with X509::isCheckKeyUsageEnabled().

By default the basic constraints are checked when a signature is validated, as well. To disable this check one need simply call X509::ignoreBasicConstraints(). To re-enable this check one need simply call X509::checkBasicConstraints(). The current state can be queried with X509::isCheckBasicConstraintsEnabled().

DN comparison

When validateSignature() walks a chain it needs to match a cert's issuer DN against the subject DN of a candidate CA cert. By default the comparison is strict: two DNs are considered different if any RDN value uses a different ASN.1 string type, so UTF8String "ACME"PrintableString "ACME". This is correct per RFC 5280, but some real-world chains were issued with mixed string types and only validate under loose comparison.

X509::looseDNComparison();           // compare underlying string values only
X509::strictDNComparison(); // require exact ASN.1 type match (default)
X509::isStrictDNComparisonEnabled(); // query the current state

URLs

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

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->getValidationChain(). 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.

isIssuerOf()

$x509->isIssuerOf($subject, $expectedKeyUsage = []) tests whether this X509 (acting as a CA cert) issued $subject. $subject can be another X509, a CRL, a Choice, or an array containing either an issuerAndSerialNumber or a subjectKeyIdentifier. The optional $expectedKeyUsage is an array of key-usage bit names that the issuer cert must have set (eg. ['keyCertSign']).

This is used internally during chain validation; usually you'd just call validateSignature() and let phpseclib walk the chain. isIssuerOf() is useful when you want to ask "did this specific CA cert sign that specific thing" without running a full validation.