Test Setup Failed
Push — master ( 76a7d8...21c3b6 )
by Nikita
07:04 queued 14s
created

CertificateService::getRootCert()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
c 0
b 0
f 0
dl 0
loc 7
rs 10
cc 2
nc 2
nop 0
1
<?php
2
3
namespace Gameap\Services\Daemon;
4
5
use Carbon\Carbon;
6
use Gameap\Exceptions\GameapException;
7
use Illuminate\Support\Facades\Storage;
8
use phpseclib\Crypt\RSA;
9
use Sop\CryptoEncoding\PEM;
10
use Sop\CryptoTypes\AlgorithmIdentifier\Hash\SHA256AlgorithmIdentifier;
11
use Sop\CryptoTypes\AlgorithmIdentifier\Signature\SignatureAlgorithmIdentifierFactory;
12
use Sop\CryptoTypes\Asymmetric\PrivateKeyInfo;
13
use X501\ASN1\Name;
14
use X509\Certificate\Certificate;
15
use X509\Certificate\Extension\BasicConstraintsExtension;
16
use X509\Certificate\Extension\KeyUsageExtension;
17
use X509\Certificate\Extension\SubjectKeyIdentifierExtension;
18
use X509\Certificate\TBSCertificate;
19
use X509\Certificate\Validity;
20
use X509\CertificationRequest\CertificationRequest;
21
use X509\CertificationRequest\CertificationRequestInfo;
22
23
class CertificateService
24
{
25
    public const ROOT_CA_CERT = 'certs/root.crt';
26
    public const ROOT_CA_KEY  = 'certs/root.key';
27
28
    public const PRIVATE_KEY_BITS = 2048;
29
    
30
    public const CERT_YEARS = 10;
31
    
32
    /**
33
     * Generate CA root key and certificate.
34
     * Write root key and certificate to a Storage.
35
     */
36
    public static function generateRoot(): void
37
    {
38
        $privateKey = (new RSA())->createKey(self::PRIVATE_KEY_BITS)['privatekey'];
39
        
40
        $privateKeyInfo = PrivateKeyInfo::fromPEM(PEM::fromString($privateKey));
41
        
42
        $publicKeyInfo = $privateKeyInfo->publicKeyInfo();
43
        
44
        $name = Name::fromString('CN=GameAP CA, O=GameAP, C=RU');
45
        
46
        $validity = Validity::fromStrings('now', 'now + ' . self::CERT_YEARS . ' years');
47
48
        // create "to be signed" certificate object with extensions
49
        $tbsCert = new TBSCertificate($name, $publicKeyInfo, $name, $validity);
50
51
        $tbsCert = $tbsCert->withRandomSerialNumber()->withAdditionalExtensions(
52
            new BasicConstraintsExtension(true, true),
53
            new SubjectKeyIdentifierExtension(false, $publicKeyInfo->keyIdentifier()),
54
            new KeyUsageExtension(
55
                true,
56
                KeyUsageExtension::DIGITAL_SIGNATURE | KeyUsageExtension::KEY_CERT_SIGN
57
            )
58
        );
59
60
        // sign certificate with private key
61
        $algo = SignatureAlgorithmIdentifierFactory::algoForAsymmetricCrypto(
62
            $privateKeyInfo->algorithmIdentifier(),
63
            new SHA256AlgorithmIdentifier()
64
        );
65
66
        $cert = $tbsCert->sign($algo, $privateKeyInfo);
67
68
        Storage::put(self::ROOT_CA_CERT, $cert);
69
        Storage::put(self::ROOT_CA_KEY, $privateKey);
70
    }
71
72
    public static function getRootKey(): string
73
    {
74
        if (!Storage::exists(self::ROOT_CA_KEY)) {
75
            self::generateRoot();
76
        }
77
78
        return Storage::get(self::ROOT_CA_KEY);
79
    }
80
81
    public static function getRootCert(): string
82
    {
83
        if (!Storage::exists(self::ROOT_CA_CERT)) {
84
            self::generateRoot();
85
        }
86
87
        return Storage::get(self::ROOT_CA_CERT);
88
    }
89
90
    /**
91
     * Generate key and certificate. Sign certificate
92
     *
93
     * @param $certificatePath string   path to certificate in storage
94
     * @param $keyPath string   path to key in storage
95
     *
96
     * @throws GameapException
97
     */
98
    public static function generate($certificatePath, $keyPath): void
99
    {
100
        $privateKey = self::generateKey();
101
102
        $privateKeyInfo = PrivateKeyInfo::fromPEM(
103
            PEM::fromString($privateKey)
104
        );
105
106
        // extract public key from private key
107
        $publicKeyInfo = $privateKeyInfo->publicKeyInfo();
108
109
        // DN of the subject
110
        $subject = Name::fromString('CN=' . gethostname() . ', O=GameAP, C=RU');
111
112
        // create certification request info
113
        $cri = new CertificationRequestInfo($subject, $publicKeyInfo);
114
115
        // sign certificate request with private key
116
        $algo = SignatureAlgorithmIdentifierFactory::algoForAsymmetricCrypto(
117
            $privateKeyInfo->algorithmIdentifier(),
118
            new SHA256AlgorithmIdentifier()
119
        );
120
        
121
        $csr = $cri->sign($algo, $privateKeyInfo);
122
        
123
        $cert = self::signCsr($csr);
124
        
125
        Storage::put($certificatePath, $cert);
126
        Storage::put($keyPath, $privateKey);
127
    }
128
129
    public static function generateKey(): string
130
    {
131
        return (new RSA())->createKey(self::PRIVATE_KEY_BITS)['privatekey'];
132
    }
133
134
    public static function generateCsr(string $key): string
135
    {
136
        $privateKeyInfo = PrivateKeyInfo::fromPEM(
137
            PEM::fromString($key)
138
        );
139
140
        $publicKeyInfo = $privateKeyInfo->publicKeyInfo();
141
142
        $subject = Name::fromString('CN=*, O=GameAP, C=RU');
143
144
        $cri = new CertificationRequestInfo($subject, $publicKeyInfo);
145
146
        $algo = SignatureAlgorithmIdentifierFactory::algoForAsymmetricCrypto(
147
            $privateKeyInfo->algorithmIdentifier(),
148
            new SHA256AlgorithmIdentifier()
149
        );
150
151
        $csr = $cri->sign($algo, $privateKeyInfo);
152
153
        return $csr;
154
    }
155
156
    /**
157
     * @param $csr string   PEM string
158
     *
159
     * @return string  PEM certificate
160
     * @throws GameapException
161
     */
162
    public static function signCsr(string $csr)
163
    {
164
        // load CA's private key
165
        $privateKeyInfo = PrivateKeyInfo::fromPEM(
166
            PEM::fromString(self::getRootKey())
167
        );
168
        
169
        $issuerCert = Certificate::fromPEM(
170
            PEM::fromString(self::getRootCert())
171
        );
172
        
173
        $certificationRequest = CertificationRequest::fromPEM(PEM::fromString($csr));
174
        
175
        if (!$certificationRequest->verify()) {
176
            throw new GameapException('Failed to verify certification request signature.');
177
        }
178
179
        $tbsCert = TBSCertificate::fromCSR($certificationRequest)->withIssuerCertificate($issuerCert);
180
        
181
        $tbsCert = $tbsCert->withRandomSerialNumber();
182
        
183
        $tbsCert = $tbsCert->withValidity(
184
            Validity::fromStrings('now', 'now + ' . self::CERT_YEARS . ' years')
185
        );
186
187
        $tbsCert = $tbsCert->withVersion(0);
188
        
189
        // sign certificate with issuer's private key
190
        $algo = SignatureAlgorithmIdentifierFactory::algoForAsymmetricCrypto(
191
            $privateKeyInfo->algorithmIdentifier(),
192
            new SHA256AlgorithmIdentifier()
193
        );
194
195
        $cert = $tbsCert->sign($algo, $privateKeyInfo);
196
        return $cert;
197
    }
198
199
    /**
200
     * @param $certificatePath
201
     *
202
     * @return string
203
     */
204
    public static function fingerprintString($certificatePath)
205
    {
206
        $fingerprint = openssl_x509_fingerprint(Storage::get($certificatePath), 'sha256');
207
        return strtoupper(implode(':', str_split($fingerprint, 2)));
0 ignored issues
show
Bug introduced by
It seems like str_split($fingerprint, 2) can also be of type true; however, parameter $pieces of implode() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

207
        return strtoupper(implode(':', /** @scrutinizer ignore-type */ str_split($fingerprint, 2)));
Loading history...
208
    }
209
210
    /**
211
     * @param $certificatePath
212
     *
213
     * @return array
214
     */
215
    public static function certificateInfo($certificatePath)
216
    {
217
        $parsed = openssl_x509_parse(Storage::get($certificatePath));
218
219
        return [
220
            'expires' => Carbon::createFromTimestamp($parsed['validTo_time_t'])->toDateTimeString(),
221
222
            'signature_type' => $parsed['signatureTypeSN'],
223
224
            'country'             => $parsed['subject']['C'] ?? '',
225
            'state'               => $parsed['subject']['ST'] ?? '',
226
            'locality'            => $parsed['subject']['L'] ?? '',
227
            'organization'        => $parsed['subject']['O'] ?? '',
228
            'organizational_unit' => $parsed['subject']['OU'] ?? '',
229
            'common_name'         => $parsed['subject']['CN'] ?? '',
230
            'email'               => $parsed['subject']['emailAddress'] ?? '',
231
232
            'issuer_country'             => $parsed['issuer']['C'] ?? '',
233
            'issuer_state'               => $parsed['issuer']['ST'] ?? '',
234
            'issuer_locality'            => $parsed['issuer']['L'] ?? '',
235
            'issuer_organization'        => $parsed['issuer']['O'] ?? '',
236
            'issuer_organizational_unit' => $parsed['issuer']['OU'] ?? '',
237
            'issuer_common_name'         => $parsed['issuer']['CN'] ?? '',
238
            'issuer_email'               => $parsed['issuer']['emailAddress'] ?? '',
239
        ];
240
    }
241
}
242