Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

X509 Laravel Package

sop/x509

Sop/X509 is a PHP library for X.509 certificate handling, offering an object-oriented API to parse, create, sign, and validate certificates and CSRs. Useful for PKI, TLS, and secure PHP application workflows.

Deep Wiki
Context7

Getting Started

sop/x509 is a pure-PHP, ASN.1-first X.509 library. It does not wrap OpenSSL — it parses DER itself (via sop/asn1) and exposes typed objects for every structure: certificates, CSRs, extensions, attribute certificates, and RFC 5280 path validation. That has two consequences you should internalize on day one: everything is an object with a static factory (fromPEM, fromDER, fromASN1), and key material lives in sop/crypto-types objects, not in OpenSSL resources.

Install and confirm what came with it:

composer require sop/x509
composer show sop/x509 sop/asn1 sop/crypto-types sop/crypto-bridge sop/crypto-encoding

The whole stack is sop/asn1 (a fork of fgrosse/phpasn1) → sop/crypto-types → sop/crypto-bridge (key conversion + sign/verify) → sop/x509.

Where to look first, in this order:

  1. tests/ — this is the real documentation. The README is thin; the test suite shows the exact fluent chains for building certificates and CSRs.
  2. lib/Certificate/Certificate.php — parsing, accessors (subject(), issuer(), validity(), extensions(), tbsCertificate()), toPEM()/toDER().
  3. lib/Certificate/Extension/ — one class per extension (BasicConstraintsExtension, KeyUsageExtension, SubjectAltNameExtension, AuthorityInfoAccessExtension, CRLDistributionPointsExtension, CertificatePoliciesExtension, …).
  4. lib/CertificationPath/ — CertificationPath, PathValidationConfig, PathValidation/PathValidationResult, plus the exception types.

First use case — the one you will actually hit: someone hands you a PEM (uploaded cert, mTLS client cert, ACME output) and you need the SANs and validity window.

use Sop\X509\Certificate\Certificate;
use Sop\X509\Certificate\Extension\SubjectAltNameExtension;

$cert = Certificate::fromPEM($pem);          // or fromDER($der)

$subject = $cert->subject()->firstValueOf('cn')->stringValue();
$notAfter = $cert->validity()->notAfter()->dateTime();
$dnsNames = $cert->extensions()
    ->get(SubjectAltNameExtension::OID)
    ?->names()->dNSNames() ?? [];           // verify accessor names in lib/Certificate/Extension/GeneralNames.php

if ($cert->validity()->notAfter()->dateTime() < new \DateTimeImmutable()) {
    throw new \RuntimeException('Certificate expired');
}

Wrap every fromPEM/fromDER call in try/catch (\Sop\X509\Exception\X509Exception $e) — malformed input is a first-class, expected condition.

Implementation Patterns

Parse once, cache the object, never re-parse per request. A parsed Certificate is immutable-ish and cheap to hold; re-parsing a 3 KB cert on every middleware invocation is pure waste. If you parse a trust store, parse it once per process and keep Certificate objects in a container/static cache, not PEM strings.

Bundles for anything multi-cert. A .pem file containing a chain is not a Certificate; feeding it to fromPEM gives you the first block or an error. Use CertificateBundle:

use Sop\X509\Certificate\CertificateBundle;

$bundle = CertificateBundle::fromFile('/etc/ssl/certs/ca-bundle.pem');
foreach ($bundle as $cert) {
    // filter by subject DN, key id, whatever
}

This is the right type for trust-anchor stores and for "here are the intermediates the client sent".

Path validation workflow. The library validates a path you construct; it does not build one. Assemble end-entity → intermediates in order, then validate against the anchors:

use Sop\X509\CertificationPath\CertificationPath;
use Sop\X509\CertificationPath\PathValidation\PathValidationConfig;
use Sop\X509\CertificationPath\Exception\PathValidationException;

$path = CertificationPath::create($endEntity, ...$intermediates); // order matters: leaf first
$config = PathValidationConfig::defaultConfig()
    ->withMaxLength(5)
    ->withDateTime(new \DateTimeImmutable());

try {
    $result = $path->validate($config, ...$trustAnchors);
    $validated = $result->certificatePath();   // confirm method name in PathValidationResult
} catch (PathValidationException $e) {
    // $e->getMessage() names the failed check (expiry, signature, key usage, policy, name chaining)
}

The exception message is your debugging tool — it is reasonably specific about which RFC 5280 step failed. Log it verbatim; it saves hours versus "chain invalid".

Issuing / signing. Two shapes, both useful:

use Sop\X509\CertificationRequest\CertificationRequestInfo;
use Sop\X509\Certificate\TBSCertificate;

// CSR: subject + SPKI + optional extensions, then sign
$info = new CertificationRequestInfo($subject, $publicKeyInfo, $extensions);
$csr  = $info->sign($privateKey, $signatureAlgorithm);
echo $csr->toPEM();

// Certificate: build the TBS, then sign it with the issuer key
$tbs  = TBSCertificate::create()
    ->withSerialNumber($serial)
    ->withIssuer($issuerName)
    ->withValidity($validity)
    ->withSubject($subject)
    ->withSubjectPublicKeyInfo($spki)
    ->withExtensions($extensions);
$cert = $tbs->sign($issuerPrivateKey, $signatureAlgorithm);

The builder method names vary slightly across 0.x releases — check lib/Certificate/TBSCertificate.php for the chain in your pinned version before writing a fixture generator. This is also the cleanest way to produce deterministic test certificates without shelling out to openssl.

Interop with the rest of PHP. You will constantly have OpenSSL resources, PEM strings, or phpseclib keys and need sop types:

use Sop\CryptoTypes\Asymmetric\PublicKey;
use Sop\CryptoTypes\Asymmetric\PrivateKey;

$pub  = PublicKey::fromPEM($pem);     // also fromDER / fromASN1
$priv = PrivateKey::fromPEM($pem);

For OpenSSL resources specifically, go through sop/crypto-bridge (Sop\CryptoBridge\Crypto::getDefault()), which is the abstraction the library itself uses for sign/verify. openssl_x509_read() output is not directly accepted anywhere.

Extension handling is typed, so treat it as a discriminated union. Extensions::get($oid) returns ?Extension; the concrete class tells you what it is. For anything you don't recognize, you still get an opaque Extension with the raw value — which means you can round-trip certs without losing unknown extensions, but you must carry them over explicitly when re-issuing.

Integration shape. Put all of this in one small service class (CertificateInspector, ChainValidator) behind an interface, and inject it. Path validation config and trust anchors should come from config, not be hardcoded; and validation should be behind a feature flag if you're rolling it out over an existing OpenSSL-based check, because the failure modes differ.

Gotchas and Tips

  • No revocation checking. Path validation does not fetch CRLs or do OCSP. If you need revocation, read CRLDistributionPointsExtension / AuthorityInfoAccessExtension yourself and validate the CRL against the issuer — or use openssl/a dedicated library. Do not assume "validated" means "not revoked". This is the single biggest trap.
  • No path building. There is no "give me a bag of certs and figure out the chain" API. You must order the chain leaf→root and decide how intermediates are selected. Budget real effort for this, especially with cross-signed roots.
  • Trust anchor semantics. Whether the anchor is itself checked for expiry/signature is a policy decision the library exposes via config, not something it silently gets right for you. Test with an expired root and confirm your expected behaviour.
  • Namespace collision with fgrosse/phpasn1. sop/asn1 deliberately uses the FG\ASN1 namespace as a drop-in fork. If another dependency pulls in the original package you get class redefinition or unpredictable autoload wins. Check composer why fgrosse/phpasn1 before shipping.
  • DER strictness. Trailing bytes, wrong string encodings, or a PEM body with stray whitespace/CRLF will throw. Be liberal on input: trim, strip BOM, normalize line endings, and reject early with a clean error rather than letting an X509Exception bubble to a 500.
  • PEM with multiple blocks. Certificate::fromPEM is not a bundle parser. If you accept uploads, normalize with openssl_x509_read-free logic or use CertificateBundle::fromPEM.
  • ECDSA signature encoding. sop encodes ECDSA signatures as DER SEQUENCE { r, s }, per X.509. COSE/JWT/WebAuthn expect raw r||s. Converting between them is on you and is a classic silent-failure bug.
  • Time zones. Validity comparisons should be done in UTC. PathValidationConfig::withDateTime() exists precisely so you can freeze time — use it in tests instead of mocking DateTime globally.
  • Equality. Use the library's equals() semantics rather than == on objects; two instances representing the same certificate may not be identical object graphs.
  • Unknown critical extensions. A real-world cert with a critical extension the library can't interpret may fail validation. Test against your actual certificate population (public CAs, internal PKI, mTLS client certs) before trusting it in production.
  • PHP version drift. The 2024-02-05 release fixed modern-PHP issues, but the 0.x line had PHP 7.2-era constraints and deprecation noise on 8.1+. Pin the version, run your test suite on your target PHP, and read the changelog before bumping.
  • ACME component. Some releases ship an ACME namespace (v1-era). Let's Encrypt dropped ACME v1; treat that module as dead code and use a maintained client instead.
  • Debugging technique. When a parse or validation fails, dump both sides: openssl asn1parse -in cert.pem -i and openssl x509 -in cert.pem -text -noout, then compare with what the library reports via toPEM(). Nine times out of ten the library is right and the input is subtly wrong.
  • Extension points. Extension construction/parsing is dispatched from the extension base classes; for a rare or private OID, subclass the abstract Extension and wire it where the OID map lives (ExtensionFactory/Extensions::fromASN1) rather than scattering raw OIDs through your application code.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings