X.509 certificates & chains
X509\Certificate parses a certificate eagerly, so a getter can never throw a parse error later. Input is capped at 64 KiB before OpenSSL sees a byte, and it answers the factual questions about a certificate:
use RoundlyConsulting\Crypto\Facades\Crypto;
use RoundlyConsulting\Crypto\Hash\HashAlgorithm;
$certificate = Crypto::x509()->fromPem($pem); // also fromDer(), fromBase64() for an x5c entry
$certificate->fingerprint();
$chain = Crypto::x509()->chain()->fromX5c($x5c); // also fromPems(), fromPemBundle(), fromCertificates()
$chain->isLinked(); // the math — the trust call stays yours
$chain->fingerprints(HashAlgorithm::Sha1);
// Shortcuts for the common cases:
Crypto::certificate($pem);
Crypto::chainFromX5c($x5c);
Crypto::chainFromPemBundle($bundle);Or call the classes the facade fronts directly:
use RoundlyConsulting\Crypto\X509\Certificate;
$certificate = Certificate::fromPem($pem); // also fromDer(), fromBase64() for an x5c entry
$certificate->commonName(); // 'app.example'
$certificate->dnsNames(); // ['app.example', '*.api.example'] — SAN dNSNames, read from the DER
$certificate->fingerprint(); // lower-case sha256 hex, as openssl emits it
$certificate->notAfter(); // CarbonImmutable
$certificate->publicKey(); // RsaKey | EcKey — policy-checked
$certificate->isSignedBy($issuer); // an ALGORITHM questionfromDer() and fromBase64() take exactly one certificate: bytes after it are refused, so two different x5c strings can never be the same certificate. fromPem() takes PEM text, never a file:// path. dnsNames() walks the subjectAltName GeneralNames in the DER and returns each dNSName verbatim — never OpenSSL’s comma-joined text, where one name containing “, DNS:victim.example” would read as two.
More certificate facts
$certificate->subject()->toString(); // 'CN=leaf.example, O=Acme, C=US'
$certificate->issuer()->commonName; // DistinguishedName properties
$certificate->serialNumber(); // upper-case hex
$certificate->signatureAlgorithm(); // e.g. 'ecdsa-with-SHA256'
$certificate->version(); // 1, 2 or 3
$certificate->base64(); // the x5c form (standard base64 DER)
$certificate->isSelfSigned(); // the signature math, not a subject == issuer compare
$certificate->equals($other); // constant-time DER comparison
$extension = $certificate->extension('1.2.840.113635.100.8.2'); // ?Extension
$extension?->critical; // bool
$extension?->der; // raw, uninterpreted extension bytes
$certificate->extensions(); // every extension, keyed by OIDextension() returns the raw DER inside the extension plus its criticality flag — walked from the certificate’s own DER, because openssl_x509_parse() pretty-prints unknown extensions into lossy text. Decode those bytes with the ASN.1 / DER decoder.
Validity dates
Validity is reported as dates, with a symmetric clock-skew leeway you own. The instant defaults to now (honouring Carbon::setTestNow) and accepts any DateTimeInterface, so you can evaluate against a token’s own signed-at time:
$certificate->isValidAt(); // now (honours Carbon::setTestNow)
$certificate->isValidAt($token->signedAt, leewaySeconds: 60);
$certificate->isExpiredAt(leewaySeconds: 60); // distinct from a bad signature
$certificate->isNotYetValidAt(); // a negative leeway throwsA negative leeway throws InvalidLeewayException. Separate isExpiredAt() and isNotYetValidAt() let you raise distinct errors — an expired certificate must never look like a bad signature. Dates gate nothing else: an expired certificate still hands over its public key and facts.
Chains
X509\Chain is an ordered leaf → root list of up to 10 certificates (Chain::MAX_CERTIFICATES). Build it from a JOSE x5c header, a PEM bundle or a list of PEMs:
use RoundlyConsulting\Crypto\Hash\HashAlgorithm;
use RoundlyConsulting\Crypto\X509\Chain;
// A JOSE x5c chain (leaf first), a concatenated PEM bundle, or a list of PEMs:
$chain = Chain::fromX5c($x5c); // ≤ 10 certificates, strict base64, typed errors
$chain = Chain::fromPemBundle($bundle);
$chain = Chain::fromPems([$leafPem, $intermediatePem, $rootPem]);
$chain->isLinked(); // every cert is signed by the next one up
$chain->fingerprints(HashAlgorithm::Sha1); // leaf → root
$chain->leaf()->publicKey();
$chain->root(); // the LAST certificate — not "a trusted root"
$chain->get(1); // by index
count($chain); // Countable; foreach works too
$chain->pemBundle();isLinked() is pure math — every certificate is signed by its successor, and a one-certificate chain is trivially linked. fromPemBundle() counts PEM blocks before parsing any of them, so an oversized bundle costs a regex, not eleven parses.
Certificate reference
| Method | Returns |
|---|---|
fromPem() / fromDer() / fromBase64() | A Certificate. fromBase64() takes an x5c entry — standard padded base64 of the DER, not base64url. DER input must be exactly one certificate; PEM input is text, never a path. |
pem() / der() / base64() | The PEM, DER and x5c (base64) encodings. |
fingerprint($algorithm = Sha256) | Lower-case hex without colons, byte-identical to openssl_x509_fingerprint(). |
publicKey() | An RsaKey or EcKey, policy-checked. Ed25519 certificates are not supported. |
subject() / issuer() | A DistinguishedName (CN, O, OU, C, ST, L) with toString(). |
commonName() / dnsNames() | The subject CN; the dNSName entries of subjectAltName, read from the DER — verbatim (wildcards included), in encoding order. |
extension($oid) / extensions() | ?Extension (oid, critical, der); every extension keyed by OID, in encoding order. |
version() / serialNumber() / signatureAlgorithm() | 1, 2 or 3; upper-case hex; the signature algorithm name. |
notBefore() / notAfter() | CarbonImmutable. |
isValidAt() / isExpiredAt() / isNotYetValidAt() | Date predicates with an optional instant and leeway in seconds. |
isSignedBy($issuer) / isSelfSigned() | The signature math — isSelfSigned() is not a subject == issuer string compare. |
equals($other) | A constant-time comparison of the DER. |
The trust boundary
Crypto proves the math; deciding what to trust is yours. There is no isTrusted(), no pinning, no root store, no revocation and no hostname matching — by design, and enforced by an architecture test. root() is simply the last certificate, not an authority you have heard of. Pin your own anchors, and decide what an expired certificate means.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.