Passed
Push — develop ( caa568...5dcde2 )
by Nikita
06:53
created

CertificateService::signCsr()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 37
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 18
nc 4
nop 1
dl 0
loc 37
rs 9.6666
c 0
b 0
f 0
1
<?php
2
3
namespace Gameap\Services;
4
5
use Gameap\Exceptions\GameapException;
6
use Illuminate\Support\Facades\Storage;
7
use Carbon\Carbon;
8
use phpseclib\Crypt\RSA;
9
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\TBSCertificate;
16
use X509\Certificate\Validity;
17
use X509\Certificate\Extension\BasicConstraintsExtension;
18
use X509\Certificate\Extension\KeyUsageExtension;
19
use X509\Certificate\Extension\SubjectKeyIdentifierExtension;
20
use X509\CertificationRequest\CertificationRequestInfo;
21
use X509\CertificationRequest\CertificationRequest;
22
use X509\Certificate\Certificate;
23
24
25
class CertificateService
26
{
27
    const ROOT_CA_CERT = 'certs/root.crt';
28
    const ROOT_CA_KEY = 'certs/root.key';
29
30
    const PRIVATE_KEY_BITS = 2048;
31
    
32
    const CERT_YEARS = 10;
33
    
34
    /**
35
     * Generate CA root key and certificate.
36
     * Write root key and certificate to a Storage.
37
     */
38
    static public function generateRoot()
39
    {
40
        $privateKey = (new RSA())->createKey(self::PRIVATE_KEY_BITS)['privatekey'];
41
        
42
        $privateKeyInfo = PrivateKeyInfo::fromPEM(PEM::fromString( $privateKey));
43
        
44
        $publicKeyInfo = $privateKeyInfo->publicKeyInfo();
45
        
46
        $name = Name::fromString("CN=GameAP CA, O=GameAP, C=RU");
47
        
48
        $validity = Validity::fromStrings('now', 'now + ' . self::CERT_YEARS . ' years');
49
50
        // create "to be signed" certificate object with extensions
51
        $tbsCert = new TBSCertificate($name, $publicKeyInfo, $name, $validity);
52
53
        $tbsCert = $tbsCert->withRandomSerialNumber()->withAdditionalExtensions(
54
            new BasicConstraintsExtension(true, true),
55
            new SubjectKeyIdentifierExtension(false, $publicKeyInfo->keyIdentifier()),
56
            new KeyUsageExtension(true,
57
                KeyUsageExtension::DIGITAL_SIGNATURE | KeyUsageExtension::KEY_CERT_SIGN));
58
59
        // sign certificate with private key
60
        $algo = SignatureAlgorithmIdentifierFactory::algoForAsymmetricCrypto(
61
            $privateKeyInfo->algorithmIdentifier(),
62
            new SHA256AlgorithmIdentifier()
63
        );
64
65
        $cert = $tbsCert->sign($algo, $privateKeyInfo);
66
67
        Storage::put(self::ROOT_CA_CERT, $cert);
68
        Storage::put(self::ROOT_CA_KEY, $privateKey);
69
    }
70
71
    /**
72
     * Generate key and certificate. Sign certificate
73
     * 
74
     * @param $certificatePath string   path to certificate in storage
75
     * @param $keyPath string   path to key in storage
76
     *
77
     * @throws GameapException
78
     */
79
    static public function generate($certificatePath, $keyPath)
80
    {
81
        if (!Storage::exists(self::ROOT_CA_CERT)) {
82
            self::generateRoot();
83
        }
84
85
        $privateKey = (new RSA())->createKey(self::PRIVATE_KEY_BITS)['privatekey'];
86
87
        $privateKeyInfo = PrivateKeyInfo::fromPEM(
88
            PEM::fromString($privateKey));
89
90
        // extract public key from private key
91
        $publicKeyInfo = $privateKeyInfo->publicKeyInfo();
92
93
        // DN of the subject
94
        $subject = Name::fromString('CN=' . gethostname() . ', O=GameAP, C=RU');
95
96
        // create certification request info
97
        $cri = new CertificationRequestInfo($subject, $publicKeyInfo);
98
99
        // sign certificate request with private key
100
        $algo = SignatureAlgorithmIdentifierFactory::algoForAsymmetricCrypto(
101
            $privateKeyInfo->algorithmIdentifier(), new SHA256AlgorithmIdentifier());
102
        
103
        $csr = $cri->sign($algo, $privateKeyInfo);
104
        
105
        $cert = self::signCsr($csr);
106
        
107
        Storage::put($certificatePath, $cert);
108
        Storage::put($keyPath, $privateKey);
109
    }
110
111
    /**
112
     * @param $csr string   PEM string
113
     *
114
     * @return string  PEM certificate
115
     * @throws GameapException
116
     */
117
    static public function signCsr(string $csr)
118
    {
119
        if (!Storage::exists(self::ROOT_CA_CERT)) {
120
            self::generateRoot();
121
        }
122
123
        // load CA's private key
124
        $privateKeyInfo = PrivateKeyInfo::fromPEM(
125
            PEM::fromString(Storage::get(self::ROOT_CA_KEY))
0 ignored issues
show
Bug introduced by
Illuminate\Support\Facad...:get(self::ROOT_CA_KEY) of type Illuminate\Contracts\Filesystem\Filesystem is incompatible with the type string expected by parameter $str of Sop\CryptoEncoding\PEM::fromString(). ( Ignorable by Annotation )

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

125
            PEM::fromString(/** @scrutinizer ignore-type */ Storage::get(self::ROOT_CA_KEY))
Loading history...
126
        );
127
        
128
        $issuerCert = Certificate::fromPEM(
129
            PEM::fromString(Storage::get(self::ROOT_CA_CERT))
130
        );
131
        
132
        $certificationRequest = CertificationRequest::fromPEM(PEM::fromString($csr));
133
        
134
        if (!$certificationRequest->verify()) {
135
            throw new GameapException('Failed to verify certification request signature.');
136
        }
137
138
        $tbsCert = TBSCertificate::fromCSR($certificationRequest)->withIssuerCertificate($issuerCert);
139
        
140
        $tbsCert = $tbsCert->withRandomSerialNumber();
141
        
142
        $tbsCert = $tbsCert->withValidity(
143
            Validity::fromStrings('now', 'now + ' . self::CERT_YEARS . ' years')
144
        );
145
146
        $tbsCert = $tbsCert->withVersion(0);
147
        
148
        // sign certificate with issuer's private key
149
        $algo = SignatureAlgorithmIdentifierFactory::algoForAsymmetricCrypto(
150
            $privateKeyInfo->algorithmIdentifier(), new SHA256AlgorithmIdentifier());
151
152
        $cert = $tbsCert->sign($algo, $privateKeyInfo);
153
        return $cert;
154
    }
155
156
    /**
157
     * @param $certificatePath
158
     *
159
     * @return string
160
     */
161
    static public function fingerprintString($certificatePath)
162
    {
163
        $fingerprint = openssl_x509_fingerprint(Storage::get($certificatePath), 'sha256');
0 ignored issues
show
Bug introduced by
Illuminate\Support\Facad...::get($certificatePath) of type Illuminate\Contracts\Filesystem\Filesystem is incompatible with the type string expected by parameter $x509 of openssl_x509_fingerprint(). ( Ignorable by Annotation )

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

163
        $fingerprint = openssl_x509_fingerprint(/** @scrutinizer ignore-type */ Storage::get($certificatePath), 'sha256');
Loading history...
164
        return strtoupper(implode(':', str_split($fingerprint, 2)));
165
    }
166
167
    /**
168
     * @param $certificatePath
169
     *
170
     * @return array
171
     */
172
    static public function certificateInfo($certificatePath)
173
    {
174
        $parsed = openssl_x509_parse(Storage::get($certificatePath));
175
176
        return [
177
            'expires' => Carbon::createFromTimestamp($parsed['validTo_time_t'])->toDateTimeString(),
178
179
            'signature_type' => $parsed['signatureTypeSN'],
180
181
            'country' => $parsed['subject']['C'] ?? '',
182
            'state' => $parsed['subject']['ST'] ?? '',
183
            'locality' => $parsed['subject']['L'] ?? '',
184
            'organization' => $parsed['subject']['O'] ?? '',
185
            'organizational_unit' => $parsed['subject']['OU'] ?? '',
186
            'common_name' => $parsed['subject']['CN'] ?? '',
187
            'email' => $parsed['subject']['emailAddress'] ?? '',
188
189
            'issuer_country' => $parsed['issuer']['C'] ?? '',
190
            'issuer_state' => $parsed['issuer']['ST'] ?? '',
191
            'issuer_locality' => $parsed['issuer']['L'] ?? '',
192
            'issuer_organization' => $parsed['issuer']['O'] ?? '',
193
            'issuer_organizational_unit' => $parsed['issuer']['OU'] ?? '',
194
            'issuer_common_name' => $parsed['issuer']['CN'] ?? '',
195
            'issuer_email' => $parsed['issuer']['emailAddress'] ?? '',
196
        ];
197
    }
198
}