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                'config' => self::CONFIG,
71            ]
72        );
73    }
74
75    public function getCaCert()
76    {
77        $output = '';
78        openssl_x509_export($this->ca, $output);
79
80        return $output;
81    }
82
83    public function saveCaCert($file)
84    {
85        openssl_x509_export_to_file($this->ca, $file);
86    }
87
88    private function generateCertAndKey(
89        $commonNameForCert, $file, $keyLength = null, $subjectAltName = null
90    ) {
91        $dn = [
92            'countryName' => 'BY',
93            'stateOrProvinceName' => 'Minsk',
94            'localityName' => 'Minsk',
95            'organizationName' => 'Example Org',
96        ];
97        if ($commonNameForCert !== null) {
98            $dn['commonName'] = $commonNameForCert;
99        }
100
101        $subjectAltNameConfig =
102            $subjectAltName ? "subjectAltName = $subjectAltName" : "";
103        $configCode = <<<CONFIG
104[ req ]
105distinguished_name = req_distinguished_name
106default_md = sha256
107default_bits = 1024
108
109[ req_distinguished_name ]
110
111[ v3_req ]
112basicConstraints = CA:FALSE
113keyUsage = nonRepudiation, digitalSignature, keyEncipherment
114$subjectAltNameConfig
115
116[ usr_cert ]
117basicConstraints = CA:FALSE
118$subjectAltNameConfig
119CONFIG;
120        $configFile = $file . '.cnf';
121        file_put_contents($configFile, $configCode);
122
123        $config = [
124            'config' => $configFile,
125            'req_extensions' => 'v3_req',
126            'x509_extensions' => 'usr_cert',
127        ];
128
129        $this->lastKey = self::generateKey($keyLength);
130        $csr = openssl_csr_new($dn, $this->lastKey, $config);
131        $this->lastCert = openssl_csr_sign(
132            $csr,
133            $this->ca,
134            $this->caKey,
135            /* days */ 2,
136            $config,
137        );
138
139        return $config;
140    }
141
142    public function saveNewCertAsFileWithKey(
143        $commonNameForCert, $file, $keyLength = null, $subjectAltName = null
144    ) {
145        $config = $this->generateCertAndKey($commonNameForCert, $file, $keyLength, $subjectAltName);
146
147        $certText = '';
148        openssl_x509_export($this->lastCert, $certText);
149
150        $keyText = '';
151        openssl_pkey_export($this->lastKey, $keyText, null, $config);
152
153        file_put_contents($file, $certText . PHP_EOL . $keyText);
154
155        unlink($config['config']);
156    }
157
158    public function saveNewCertAndKey(
159        $commonNameForCert, $certFile, $keyFile, $keyLength = null, $subjectAltName = null
160    ) {
161        $config = $this->generateCertAndKey($commonNameForCert, $certFile, $keyLength, $subjectAltName);
162
163        openssl_x509_export_to_file($this->lastCert, $certFile);
164        openssl_pkey_export_to_file($this->lastKey, $keyFile, null, $config);
165
166        unlink($config['config']);
167    }
168
169    public function getCertDigest($algo)
170    {
171        return openssl_x509_fingerprint($this->lastCert, $algo);
172    }
173}
174