Issues (61)

app/Services/Daemon/CertificateService.php (1 issue)

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

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