1<?php
2
3class CertificateGenerator
4{
5    const CONFIG = __DIR__. DIRECTORY_SEPARATOR . 'openssl.cnf';
6
7    /** @var resource */
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