spomky-labs/pki-framework
PHP 8.1+ framework for Public Key Infrastructure: X.509 certificates (incl. attribute certs), ASN.1 DER encoding/decoding, X.501/X.520 DN parsing, PEM (RFC 7468) handling, and PKCS-oriented cryptography utilities.
Install the Package
composer require spomky-labs/pki-framework
Ensure gmp or bcmath extensions are enabled for cryptographic operations.
Load a Certificate (PEM or DER)
use SpomkyLabs\PkiFramework\Certificate;
$certificate = Certificate::loadFromFile('path/to/certificate.pem');
// or
$certificate = Certificate::loadFromString($pemContent);
Inspect Certificate Details
echo $certificate->getSubject()->getCommonName(); // Subject CN
echo $certificate->getIssuer()->getOrganization(); // Issuer O
echo $certificate->getValidity()->getNotBefore()->format('Y-m-d'); // Validity
Validate a Certificate
$isValid = $certificate->isValid();
$errors = $certificate->getValidationErrors();
Generate a Self-Signed Certificate (Quick Test)
use SpomkyLabs\PkiFramework\Certificate\CertificateGenerator;
$generator = new CertificateGenerator();
$certificate = $generator
->withSubject(new Subject('CN=Test Certificate'))
->withValidity(new Validity(new DateTime(), new DateTime('+1 year')))
->withPublicKey($publicKey) // Load from key pair
->generate();
use SpomkyLabs\PkiFramework\Certificate;
public function validateClientCertificate(Request $request)
{
$clientCert = $request->getClientCert();
$cert = Certificate::loadFromString($clientCert);
if (!$cert->isValid()) {
throw new \RuntimeException('Invalid certificate: ' . implode(', ', $cert->getValidationErrors()));
}
// Check custom logic (e.g., SANs, issuer, or extensions)
if (!$cert->getSubject()->getCommonName()->equals('Allowed Service')) {
throw new \RuntimeException('Certificate not authorized');
}
return response()->json(['status' => 'valid']);
}
use SpomkyLabs\PkiFramework\Certificate\CertificateGenerator;
use SpomkyLabs\PkiFramework\Certificate\Extension\SubjectAlternativeName;
use SpomkyLabs\PkiFramework\Certificate\Extension\KeyUsage;
public function issueCertificate(string $subjectCN, array $sans = []): string
{
$generator = new CertificateGenerator();
// Configure subject and validity
$generator->withSubject(new Subject("CN=$subjectCN"));
$generator->withValidity(new Validity(
new DateTime(),
new DateTime('+1 year')
));
// Add SANs (e.g., for DNS or IP validation)
if (!empty($sans)) {
$generator->withExtension(new SubjectAlternativeName($sans));
}
// Key usage (e.g., digital signature, key encipherment)
$generator->withExtension(new KeyUsage([
KeyUsage::DIGITAL_SIGNATURE,
KeyUsage::KEY_ENCIPHERMENT
]));
// Generate key pair (RSA 2048-bit)
$keyPair = KeyPair::generate(2048);
// Sign with CA private key (loaded from secure storage)
$caPrivateKey = PrivateKey::loadFromFile('/path/to/ca_private.key');
$generator->withPrivateKey($caPrivateKey);
$certificate = $generator->generate();
return $certificate->toPEM();
}
use SpomkyLabs\PkiFramework\Crl\CrlGenerator;
public function generateCrl(): string
{
$crlGenerator = new CrlGenerator();
// Add revoked certificates (serial number => revocation date)
$crlGenerator->addRevokedCertificate(
new RevokedCertificate(
new SerialNumber('1234567890'), // Serial from issued cert
new DateTime('2023-01-01') // Revocation date
)
);
// Sign with CA private key
$caPrivateKey = PrivateKey::loadFromFile('/path/to/ca_private.key');
$crlGenerator->withPrivateKey($caPrivateKey);
$crl = $crlGenerator->generate();
return $crl->toPEM();
}
use SpomkyLabs\PkiFramework\Ocsp\OcspResponseGenerator;
public function generateOcspResponse(string $certSerial): string
{
$ocspGenerator = new OcspResponseGenerator();
// Configure response status
$ocspGenerator->addSingleResponse(
new SingleResponse(
new SerialNumber($certSerial),
ResponseStatus::GOOD, // or GOOD, REVOKED, UNKNOWN
new DateTime(), // ThisUpdate
new DateTime('+1 day') // NextUpdate
)
);
// Sign with CA private key
$caPrivateKey = PrivateKey::loadFromFile('/path/to/ca_private.key');
$ocspGenerator->withPrivateKey($caPrivateKey);
$ocspResponse = $ocspGenerator->generate();
return $ocspResponse->toPEM();
}
use SpomkyLabs\PkiFramework\Certificate\CertificateChainValidator;
public function validateCertificateChain(array $certificates): bool
{
$validator = new CertificateChainValidator();
// Load trusted root CA
$rootCa = Certificate::loadFromFile('/path/to/root_ca.pem');
$validator->withTrustedCertificates([$rootCa]);
// Validate chain
$chain = [];
foreach ($certificates as $cert) {
$chain[] = Certificate::loadFromString($cert);
}
$result = $validator->validate($chain);
return $result->isValid();
}
// app/Providers/PkiServiceProvider.php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use SpomkyLabs\PkiFramework\Certificate;
use SpomkyLabs\PkiFramework\Certificate\CertificateChainValidator;
class PkiServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton(CertificateChainValidator::class, function ($app) {
$validator = new CertificateChainValidator();
// Load trusted CAs from config
$trustedCAs = config('pki.trusted_cas');
foreach ($trustedCAs as $caPath) {
$validator->withTrustedCertificates([
Certificate::loadFromFile($caPath)
]);
}
return $validator;
});
}
}
// app/Http/Middleware/ValidateClientCertificate.php
namespace App\Http\Middleware;
use Closure;
use SpomkyLabs\PkiFramework\Certificate;
use Illuminate\Http\Request;
class ValidateClientCertificate
{
public function handle(Request $request, Closure $next)
{
if ($request->hasHeader('ssl-client-cert')) {
$clientCert = $request->header('ssl-client-cert');
$cert = Certificate::loadFromString($clientCert);
if (!$cert->isValid()) {
abort(403, 'Invalid client certificate');
}
// Attach certificate to request for downstream use
$request->merge(['client_certificate' => $cert]);
}
return $next($request);
}
}
// app/Console/Commands/IssueCertificate.php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use SpomkyLabs\PkiFramework\Certificate\CertificateGenerator;
class IssueCertificate extends Command
{
protected $signature = 'pki:issue
{--subject= : Subject CN}
{--sans= : Comma-separated SANs (e.g., DNS:example.com,IP:192.168.1.1)}
{--output= : Output file path}';
protected $description = 'Issue a new certificate';
public function handle()
{
$generator = new CertificateGenerator();
$generator->withSubject(new Subject("CN={$this->option('subject')}"));
if ($sans = $this->option('sans')) {
$generator->withExtension(new SubjectAlternativeName(explode(',', $sans)));
}
$keyPair = KeyPair::generate(2048);
$generator->withPublicKey($keyPair->getPublicKey());
// Load CA private key from secure storage
$caPrivateKey = PrivateKey::loadFromFile(config('pki.ca_private_key_path'));
$generator->withPrivateKey($caPrivateKey);
$certificate = $generator->generate();
if ($output = $this->option('output
How can I help you explore Laravel packages today?