|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
/* |
|
4
|
|
|
* ****************************************************************************** |
|
5
|
|
|
* Copyright 2011-2017 DANTE Ltd. and GÉANT on behalf of the GN3, GN3+, GN4-1 |
|
6
|
|
|
* and GN4-2 consortia |
|
7
|
|
|
* |
|
8
|
|
|
* License: see the web/copyright.php file in the file structure |
|
9
|
|
|
* ****************************************************************************** |
|
10
|
|
|
*/ |
|
11
|
|
|
|
|
12
|
|
|
/** This file contains the X509 class. |
|
13
|
|
|
* |
|
14
|
|
|
* @author Stefan Winter <[email protected]> |
|
15
|
|
|
* @author Tomasz Wolniewicz <[email protected]> |
|
16
|
|
|
* |
|
17
|
|
|
* @package Developer |
|
18
|
|
|
*/ |
|
19
|
|
|
|
|
20
|
|
|
namespace core\common; |
|
21
|
|
|
|
|
22
|
|
|
use Exception; |
|
23
|
|
|
|
|
24
|
|
|
/** |
|
25
|
|
|
* This class contains handling functions for X.509 certificates |
|
26
|
|
|
* |
|
27
|
|
|
* @author Stefan Winter <[email protected]> |
|
28
|
|
|
* @author Tomasz Wolniewicz <[email protected]> |
|
29
|
|
|
* |
|
30
|
|
|
* @license see LICENSE file in root directory |
|
31
|
|
|
* |
|
32
|
|
|
* @package Developer |
|
33
|
|
|
*/ |
|
34
|
|
|
class X509 { |
|
35
|
|
|
|
|
36
|
|
|
const KNOWN_PUBLIC_KEY_ALGORITHMS = [0 => "rsaEncryption", 1 => "id-ecPublicKey"]; |
|
37
|
|
|
|
|
38
|
|
|
/** |
|
39
|
|
|
* transform PEM formatted certificate to DER format |
|
40
|
|
|
* |
|
41
|
|
|
* @param string $pemData blob of data, which is hopefully a PEM certificate |
|
42
|
|
|
* @return string the DER representation of the certificate |
|
43
|
|
|
* |
|
44
|
|
|
* @author http://php.net/manual/en/ref.openssl.php (comment from 29-Mar-2007) |
|
45
|
|
|
*/ |
|
46
|
|
|
public function pem2der(string $pemData) { |
|
47
|
|
|
$begin = "CERTIFICATE-----"; |
|
48
|
|
|
$end = "-----END"; |
|
49
|
|
|
$pemDataTemp = substr($pemData, strpos($pemData, $begin) + strlen($begin)); |
|
50
|
|
|
if ($pemDataTemp === FALSE) { // this is not allowed to happen, we always have clean input here |
|
|
|
|
|
|
51
|
|
|
throw new Exception("No BEGIN marker found in guaranteed PEM data!"); |
|
52
|
|
|
} |
|
53
|
|
|
$markerPosition = strpos($pemDataTemp, $end); |
|
54
|
|
|
if ($markerPosition === FALSE) { |
|
|
|
|
|
|
55
|
|
|
throw new Exception("No END marker found in guaranteed PEM data!"); |
|
56
|
|
|
} |
|
57
|
|
|
$pemDataTemp2 = substr($pemDataTemp, 0, $markerPosition); |
|
58
|
|
|
if ($pemDataTemp2 === FALSE) { // this is not allowed to happen, we always have clean input here |
|
|
|
|
|
|
59
|
|
|
throw new Exception("Impossible: END marker cutting resulted in an empty string or error?!"); |
|
60
|
|
|
} |
|
61
|
|
|
$der = base64_decode($pemDataTemp2); |
|
62
|
|
|
if ($der === FALSE) { |
|
|
|
|
|
|
63
|
|
|
throw new Exception("Invalid DER data after extracting guaranteed PEM data!"); |
|
64
|
|
|
} |
|
65
|
|
|
return $der; |
|
66
|
|
|
} |
|
67
|
|
|
|
|
68
|
|
|
/** |
|
69
|
|
|
* transform DER formatted certificate to PEM format |
|
70
|
|
|
* |
|
71
|
|
|
* @param string $derData blob of DER data |
|
72
|
|
|
* @return string the PEM representation of the certificate |
|
73
|
|
|
*/ |
|
74
|
|
|
public function der2pem($derData) { |
|
75
|
|
|
$pem = chunk_split(base64_encode($derData), 64, "\n"); |
|
76
|
|
|
$pem = "-----BEGIN CERTIFICATE-----\n" . $pem . "-----END CERTIFICATE-----\n"; |
|
77
|
|
|
return $pem; |
|
78
|
|
|
} |
|
79
|
|
|
|
|
80
|
|
|
/** |
|
81
|
|
|
* prepare PEM and DER formats, MD5 and SHA1 fingerprints and subject of the certificate |
|
82
|
|
|
* |
|
83
|
|
|
* returns an array with the following fields: |
|
84
|
|
|
* <pre> uuid |
|
85
|
|
|
* pem certificate in PEM format |
|
86
|
|
|
* der certificate in DER format |
|
87
|
|
|
* md5 MD5 fingerprint |
|
88
|
|
|
* sha1 SHA1 fingerprint |
|
89
|
|
|
* name certificate subject |
|
90
|
|
|
* root value 1 if root certificate 0 otherwise |
|
91
|
|
|
* ca value 1 if CA certificate 0 otherwise |
|
92
|
|
|
* |
|
93
|
|
|
* </pre> |
|
94
|
|
|
* @param string $cadata certificate in ether PEM or DER format |
|
95
|
|
|
* @return array|false |
|
96
|
|
|
*/ |
|
97
|
|
|
public function processCertificate($cadata) { |
|
98
|
|
|
if ($cadata === FALSE) { // we are expecting a string anyway |
|
|
|
|
|
|
99
|
|
|
return FALSE; |
|
100
|
|
|
} |
|
101
|
|
|
$pemBegin = strpos($cadata, "-----BEGIN CERTIFICATE-----"); |
|
102
|
|
|
if ($pemBegin !== FALSE) { |
|
|
|
|
|
|
103
|
|
|
$pemEnd = strpos($cadata, "-----END CERTIFICATE-----") + 25; |
|
104
|
|
|
if ($pemEnd !== FALSE) { |
|
|
|
|
|
|
105
|
|
|
$cadata = substr($cadata, $pemBegin, $pemEnd - $pemBegin); |
|
106
|
|
|
if ($cadata === FALSE) { |
|
|
|
|
|
|
107
|
|
|
throw new Exception("Impossible: despite having found BEGIN and END markers, unable to cut out substring!"); |
|
108
|
|
|
} |
|
109
|
|
|
} |
|
110
|
|
|
$authorityDer = $this->pem2der($cadata); |
|
111
|
|
|
$authorityPem = $this->der2pem($authorityDer); |
|
112
|
|
|
} else { |
|
113
|
|
|
$authorityDer = $cadata; |
|
114
|
|
|
$authorityPem = $this->der2pem($cadata); |
|
115
|
|
|
} |
|
116
|
|
|
|
|
117
|
|
|
// check that the certificate is OK |
|
118
|
|
|
$myca = openssl_x509_read($authorityPem); |
|
119
|
|
|
if ($myca == FALSE) { |
|
|
|
|
|
|
120
|
|
|
return FALSE; |
|
121
|
|
|
} |
|
122
|
|
|
$mydetails = openssl_x509_parse($myca); |
|
123
|
|
|
if (!isset($mydetails['subject'])) { |
|
124
|
|
|
return FALSE; |
|
125
|
|
|
} |
|
126
|
|
|
$md5 = openssl_digest($authorityDer, 'MD5'); |
|
127
|
|
|
$sha1 = openssl_digest($authorityDer, 'SHA1'); |
|
128
|
|
|
$out = ["pem" => $authorityPem, "der" => $authorityDer, "md5" => $md5, "sha1" => $sha1, "name" => $mydetails['name']]; |
|
129
|
|
|
|
|
130
|
|
|
$out['root'] = 0; // default, unless concinved otherwise below |
|
131
|
|
|
if ($mydetails['issuer'] === $mydetails['subject']) { |
|
132
|
|
|
$out['root'] = 1; |
|
133
|
|
|
$mydetails['type'] = 'root'; |
|
134
|
|
|
} |
|
135
|
|
|
// again default: not a CA unless convinced otherwise |
|
136
|
|
|
$out['ca'] = 0; // we need to resolve this ambiguity |
|
137
|
|
|
$out['basicconstraints_set'] = 0; |
|
138
|
|
|
// if no basicContraints are set at all, this is a problem in itself |
|
139
|
|
|
// is this a CA? or not? Treat as server, but add a warning... |
|
140
|
|
|
if (isset($mydetails['extensions']['basicConstraints'])) { |
|
141
|
|
|
$out['ca'] = preg_match('/^CA:TRUE/', $mydetails['extensions']['basicConstraints']); |
|
142
|
|
|
$out['basicconstraints_set'] = 1; |
|
143
|
|
|
} |
|
144
|
|
|
|
|
145
|
|
|
if ($out['ca'] > 0 && $out['root'] == 0) { |
|
146
|
|
|
$mydetails['type'] = 'interm_ca'; |
|
147
|
|
|
} |
|
148
|
|
|
if ($out['ca'] == 0 && $out['root'] == 0) { |
|
149
|
|
|
$mydetails['type'] = 'server'; |
|
150
|
|
|
} |
|
151
|
|
|
$mydetails['sha1'] = $sha1; |
|
152
|
|
|
// the signature algorithm is available in PHP7 with the property "signatureTypeSN", example "RSA-SHA512" |
|
153
|
|
|
$out['full_details'] = $mydetails; |
|
154
|
|
|
|
|
155
|
|
|
$algoMatch = []; |
|
156
|
|
|
$keyLengthMatch = []; |
|
157
|
|
|
// we are also interested in the type and length of public key, |
|
158
|
|
|
// which ..._parse doesn't tell us :-( |
|
159
|
|
|
openssl_x509_export($myca, $output, FALSE); |
|
160
|
|
|
if (preg_match('/^\s+Public Key Algorithm:\s*(.*)\s*$/m', $output, $algoMatch) && in_array($algoMatch[1], X509::KNOWN_PUBLIC_KEY_ALGORITHMS)) { |
|
161
|
|
|
$out['full_details']['public_key_algorithm'] = $algoMatch[1]; |
|
162
|
|
|
} else { |
|
163
|
|
|
$out['full_details']['public_key_algorithm'] = "UNKNOWN"; |
|
164
|
|
|
} |
|
165
|
|
|
|
|
166
|
|
|
if ((preg_match('/^\s+Public-Key:\s*\((.*) bit\)\s*$/m', $output, $keyLengthMatch)) && is_numeric($keyLengthMatch[1])) { |
|
167
|
|
|
$out['full_details']['public_key_length'] = $keyLengthMatch[1]; |
|
168
|
|
|
} else { |
|
169
|
|
|
$out['full_details']['public_key_length'] = 0; // if we don't know, assume an unsafe key length -> will trigger warning |
|
170
|
|
|
} |
|
171
|
|
|
return $out; |
|
172
|
|
|
} |
|
173
|
|
|
|
|
174
|
|
|
/** |
|
175
|
|
|
* split a certificate file into components |
|
176
|
|
|
* |
|
177
|
|
|
* returns an array containing the PEM format of the certificate (s) |
|
178
|
|
|
* if the file contains multiple certificates it gets split into components |
|
179
|
|
|
* |
|
180
|
|
|
* @param string $cadata certificate in ether PEM or DER format |
|
181
|
|
|
* @return array |
|
182
|
|
|
*/ |
|
183
|
|
|
public function splitCertificate($cadata) { |
|
184
|
|
|
$returnarray = []; |
|
185
|
|
|
// maybe we got no real cert data at all? The code is hardened, but will |
|
186
|
|
|
// produce ugly WARNING level output in the logfiles, so let's avoid at least |
|
187
|
|
|
// the trivial case: if the file is empty, there's no cert in it |
|
188
|
|
|
if ($cadata == "") { |
|
189
|
|
|
return $returnarray; |
|
190
|
|
|
} |
|
191
|
|
|
$startPem = strpos($cadata, "-----BEGIN CERTIFICATE-----"); |
|
192
|
|
|
if ($startPem !== FALSE) { |
|
|
|
|
|
|
193
|
|
|
$cadata = substr($cadata, $startPem); |
|
194
|
|
|
if ($cadata === FALSE) { |
|
|
|
|
|
|
195
|
|
|
throw new Exception("Impossible: despite having found BEGIN marker, unable to cut out substring!"); |
|
196
|
|
|
} |
|
197
|
|
|
$endPem = strpos($cadata, "-----END CERTIFICATE-----") + 25; |
|
198
|
|
|
$nextPem = strpos($cadata, "-----BEGIN CERTIFICATE-----", 30); |
|
199
|
|
|
while ($nextPem !== FALSE) { |
|
200
|
|
|
$returnarray[] = substr($cadata, 0, $endPem); |
|
201
|
|
|
$cadata = substr($cadata, $nextPem); |
|
202
|
|
|
$endPem = strpos($cadata, "-----END CERTIFICATE-----") + 25; |
|
203
|
|
|
$nextPem = strpos($cadata, "-----BEGIN CERTIFICATE-----", 30); |
|
204
|
|
|
} |
|
205
|
|
|
$returnarray[] = substr($cadata, 0, $endPem); |
|
206
|
|
|
} else { |
|
207
|
|
|
// we hand it over to der2pem (no user content coming in from any caller |
|
208
|
|
|
// so we know we work with valid cert data in the first place |
|
209
|
|
|
$returnarray[] = $this->der2pem($cadata); |
|
210
|
|
|
} |
|
211
|
|
|
return array_unique($returnarray); |
|
212
|
|
|
} |
|
213
|
|
|
|
|
214
|
|
|
} |
|
215
|
|
|
|