1<?php 2 3class CertificateGenerator 4{ 5 const CONFIG = __DIR__. DIRECTORY_SEPARATOR . 'openssl.cnf'; 6 7 /** @var OpenSSLCertificate */ 8 private $ca; 9 10 /** @var resource */ 11 private $caKey; 12 13 /** @var resource|null */ 14 private $lastCert; 15 16 /** @var resource|null */ 17 private $lastKey; 18 19 public function __construct() 20 { 21 if (!extension_loaded('openssl')) { 22 throw new RuntimeException( 23 'openssl extension must be loaded to generate certificates' 24 ); 25 } 26 $this->generateCa(); 27 } 28 29 /** 30 * @param int|null $keyLength 31 * @return resource 32 */ 33 private static function generateKey($keyLength = null) 34 { 35 if (null === $keyLength) { 36 $keyLength = 2048; 37 } 38 39 return openssl_pkey_new([ 40 'private_key_bits' => $keyLength, 41 'private_key_type' => OPENSSL_KEYTYPE_RSA, 42 'encrypt_key' => false, 43 ]); 44 } 45 46 private function generateCa() 47 { 48 $this->caKey = self::generateKey(); 49 $dn = [ 50 'countryName' => 'GB', 51 'stateOrProvinceName' => 'Berkshire', 52 'localityName' => 'Newbury', 53 'organizationName' => 'Example Certificate Authority', 54 'commonName' => 'CA for PHP Tests' 55 ]; 56 57 $this->ca = openssl_csr_sign( 58 openssl_csr_new( 59 $dn, 60 $this->caKey, 61 [ 62 'x509_extensions' => 'v3_ca', 63 'config' => self::CONFIG, 64 ] 65 ), 66 null, 67 $this->caKey, 68 2 69 ); 70 } 71 72 public function getCaCert() 73 { 74 $output = ''; 75 openssl_x509_export($this->ca, $output); 76 77 return $output; 78 } 79 80 public function saveCaCert($file) 81 { 82 openssl_x509_export_to_file($this->ca, $file); 83 } 84 85 public function saveNewCertAsFileWithKey( 86 $commonNameForCert, $file, $keyLength = null, $subjectAltName = null 87 ) { 88 $dn = [ 89 'countryName' => 'BY', 90 'stateOrProvinceName' => 'Minsk', 91 'localityName' => 'Minsk', 92 'organizationName' => 'Example Org', 93 ]; 94 if ($commonNameForCert !== null) { 95 $dn['commonName'] = $commonNameForCert; 96 } 97 98 $subjectAltNameConfig = 99 $subjectAltName ? "subjectAltName = $subjectAltName" : ""; 100 $configCode = <<<CONFIG 101[ req ] 102distinguished_name = req_distinguished_name 103default_md = sha256 104 105[ req_distinguished_name ] 106 107[ v3_req ] 108basicConstraints = CA:FALSE 109keyUsage = nonRepudiation, digitalSignature, keyEncipherment 110$subjectAltNameConfig 111 112[ usr_cert ] 113basicConstraints = CA:FALSE 114$subjectAltNameConfig 115CONFIG; 116 $configFile = $file . '.cnf'; 117 file_put_contents($configFile, $configCode); 118 119 try { 120 $config = [ 121 'config' => $configFile, 122 'req_extensions' => 'v3_req', 123 'x509_extensions' => 'usr_cert', 124 ]; 125 126 $this->lastKey = self::generateKey($keyLength); 127 $this->lastCert = openssl_csr_sign( 128 openssl_csr_new($dn, $this->lastKey, $config), 129 $this->ca, 130 $this->caKey, 131 /* days */ 2, 132 $config, 133 ); 134 if (!$this->lastCert) { 135 throw new Exception('Failed to create certificate'); 136 } 137 138 $certText = ''; 139 openssl_x509_export($this->lastCert, $certText); 140 141 $keyText = ''; 142 openssl_pkey_export($this->lastKey, $keyText); 143 144 file_put_contents($file, $certText . PHP_EOL . $keyText); 145 } finally { 146 unlink($configFile); 147 } 148 } 149 150 public function getCertDigest($algo) 151 { 152 return openssl_x509_fingerprint($this->lastCert, $algo); 153 } 154} 155