Rev 846 | Rev 1042 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed
Rev | Author | Line No. | Line |
---|---|---|---|
827 | daniel-mar | 1 | <?php |
2 | |||
3 | /** |
||
4 | * Pure-PHP ASN.1 Parser |
||
5 | * |
||
6 | * PHP version 5 |
||
7 | * |
||
8 | * ASN.1 provides the semantics for data encoded using various schemes. The most commonly |
||
9 | * utilized scheme is DER or the "Distinguished Encoding Rules". PEM's are base64 encoded |
||
10 | * DER blobs. |
||
11 | * |
||
12 | * \phpseclib3\File\ASN1 decodes and encodes DER formatted messages and places them in a semantic context. |
||
13 | * |
||
14 | * Uses the 1988 ASN.1 syntax. |
||
15 | * |
||
874 | daniel-mar | 16 | * @category File |
17 | * @package ASN1 |
||
827 | daniel-mar | 18 | * @author Jim Wigginton <terrafrost@php.net> |
19 | * @copyright 2012 Jim Wigginton |
||
20 | * @license http://www.opensource.org/licenses/mit-license.html MIT License |
||
21 | * @link http://phpseclib.sourceforge.net |
||
22 | */ |
||
23 | |||
24 | namespace phpseclib3\File; |
||
25 | |||
26 | use DateTime; |
||
27 | use ParagonIE\ConstantTime\Base64; |
||
28 | use phpseclib3\Common\Functions\Strings; |
||
29 | use phpseclib3\File\ASN1\Element; |
||
30 | use phpseclib3\Math\BigInteger; |
||
31 | |||
32 | /** |
||
33 | * Pure-PHP ASN.1 Parser |
||
34 | * |
||
874 | daniel-mar | 35 | * @package ASN1 |
827 | daniel-mar | 36 | * @author Jim Wigginton <terrafrost@php.net> |
874 | daniel-mar | 37 | * @access public |
827 | daniel-mar | 38 | */ |
39 | abstract class ASN1 |
||
40 | { |
||
41 | // Tag Classes |
||
42 | // http://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#page=12 |
||
43 | const CLASS_UNIVERSAL = 0; |
||
44 | const CLASS_APPLICATION = 1; |
||
45 | const CLASS_CONTEXT_SPECIFIC = 2; |
||
46 | const CLASS_PRIVATE = 3; |
||
47 | |||
48 | // Tag Classes |
||
49 | // http://www.obj-sys.com/asn1tutorial/node124.html |
||
50 | const TYPE_BOOLEAN = 1; |
||
51 | const TYPE_INTEGER = 2; |
||
52 | const TYPE_BIT_STRING = 3; |
||
53 | const TYPE_OCTET_STRING = 4; |
||
54 | const TYPE_NULL = 5; |
||
55 | const TYPE_OBJECT_IDENTIFIER = 6; |
||
56 | //const TYPE_OBJECT_DESCRIPTOR = 7; |
||
57 | //const TYPE_INSTANCE_OF = 8; // EXTERNAL |
||
58 | const TYPE_REAL = 9; |
||
59 | const TYPE_ENUMERATED = 10; |
||
60 | //const TYPE_EMBEDDED = 11; |
||
61 | const TYPE_UTF8_STRING = 12; |
||
62 | //const TYPE_RELATIVE_OID = 13; |
||
63 | const TYPE_SEQUENCE = 16; // SEQUENCE OF |
||
64 | const TYPE_SET = 17; // SET OF |
||
65 | |||
66 | // More Tag Classes |
||
67 | // http://www.obj-sys.com/asn1tutorial/node10.html |
||
68 | const TYPE_NUMERIC_STRING = 18; |
||
69 | const TYPE_PRINTABLE_STRING = 19; |
||
70 | const TYPE_TELETEX_STRING = 20; // T61String |
||
71 | const TYPE_VIDEOTEX_STRING = 21; |
||
72 | const TYPE_IA5_STRING = 22; |
||
73 | const TYPE_UTC_TIME = 23; |
||
74 | const TYPE_GENERALIZED_TIME = 24; |
||
75 | const TYPE_GRAPHIC_STRING = 25; |
||
76 | const TYPE_VISIBLE_STRING = 26; // ISO646String |
||
77 | const TYPE_GENERAL_STRING = 27; |
||
78 | const TYPE_UNIVERSAL_STRING = 28; |
||
79 | //const TYPE_CHARACTER_STRING = 29; |
||
80 | const TYPE_BMP_STRING = 30; |
||
81 | |||
82 | // Tag Aliases |
||
83 | // These tags are kinda place holders for other tags. |
||
84 | const TYPE_CHOICE = -1; |
||
85 | const TYPE_ANY = -2; |
||
86 | |||
87 | /** |
||
88 | * ASN.1 object identifiers |
||
89 | * |
||
90 | * @var array |
||
874 | daniel-mar | 91 | * @access private |
827 | daniel-mar | 92 | * @link http://en.wikipedia.org/wiki/Object_identifier |
93 | */ |
||
94 | private static $oids = []; |
||
95 | |||
96 | /** |
||
97 | * ASN.1 object identifier reverse mapping |
||
98 | * |
||
99 | * @var array |
||
874 | daniel-mar | 100 | * @access private |
827 | daniel-mar | 101 | */ |
102 | private static $reverseOIDs = []; |
||
103 | |||
104 | /** |
||
105 | * Default date format |
||
106 | * |
||
107 | * @var string |
||
874 | daniel-mar | 108 | * @access private |
827 | daniel-mar | 109 | * @link http://php.net/class.datetime |
110 | */ |
||
111 | private static $format = 'D, d M Y H:i:s O'; |
||
112 | |||
113 | /** |
||
114 | * Filters |
||
115 | * |
||
116 | * If the mapping type is self::TYPE_ANY what do we actually encode it as? |
||
117 | * |
||
118 | * @var array |
||
874 | daniel-mar | 119 | * @access private |
827 | daniel-mar | 120 | * @see self::encode_der() |
121 | */ |
||
122 | private static $filters; |
||
123 | |||
124 | /** |
||
125 | * Current Location of most recent ASN.1 encode process |
||
126 | * |
||
127 | * Useful for debug purposes |
||
128 | * |
||
129 | * @var array |
||
874 | daniel-mar | 130 | * @access private |
827 | daniel-mar | 131 | * @see self::encode_der() |
132 | */ |
||
133 | private static $location; |
||
134 | |||
135 | /** |
||
136 | * DER Encoded String |
||
137 | * |
||
138 | * In case we need to create ASN1\Element object's.. |
||
139 | * |
||
140 | * @var string |
||
874 | daniel-mar | 141 | * @access private |
827 | daniel-mar | 142 | * @see self::decodeDER() |
143 | */ |
||
144 | private static $encoded; |
||
145 | |||
146 | /** |
||
147 | * Type mapping table for the ANY type. |
||
148 | * |
||
149 | * Structured or unknown types are mapped to a \phpseclib3\File\ASN1\Element. |
||
150 | * Unambiguous types get the direct mapping (int/real/bool). |
||
151 | * Others are mapped as a choice, with an extra indexing level. |
||
152 | * |
||
153 | * @var array |
||
874 | daniel-mar | 154 | * @access public |
827 | daniel-mar | 155 | */ |
156 | const ANY_MAP = [ |
||
157 | self::TYPE_BOOLEAN => true, |
||
158 | self::TYPE_INTEGER => true, |
||
159 | self::TYPE_BIT_STRING => 'bitString', |
||
160 | self::TYPE_OCTET_STRING => 'octetString', |
||
161 | self::TYPE_NULL => 'null', |
||
162 | self::TYPE_OBJECT_IDENTIFIER => 'objectIdentifier', |
||
163 | self::TYPE_REAL => true, |
||
164 | self::TYPE_ENUMERATED => 'enumerated', |
||
165 | self::TYPE_UTF8_STRING => 'utf8String', |
||
166 | self::TYPE_NUMERIC_STRING => 'numericString', |
||
167 | self::TYPE_PRINTABLE_STRING => 'printableString', |
||
168 | self::TYPE_TELETEX_STRING => 'teletexString', |
||
169 | self::TYPE_VIDEOTEX_STRING => 'videotexString', |
||
170 | self::TYPE_IA5_STRING => 'ia5String', |
||
171 | self::TYPE_UTC_TIME => 'utcTime', |
||
172 | self::TYPE_GENERALIZED_TIME => 'generalTime', |
||
173 | self::TYPE_GRAPHIC_STRING => 'graphicString', |
||
174 | self::TYPE_VISIBLE_STRING => 'visibleString', |
||
175 | self::TYPE_GENERAL_STRING => 'generalString', |
||
176 | self::TYPE_UNIVERSAL_STRING => 'universalString', |
||
177 | //self::TYPE_CHARACTER_STRING => 'characterString', |
||
178 | self::TYPE_BMP_STRING => 'bmpString' |
||
179 | ]; |
||
180 | |||
181 | /** |
||
182 | * String type to character size mapping table. |
||
183 | * |
||
184 | * Non-convertable types are absent from this table. |
||
185 | * size == 0 indicates variable length encoding. |
||
186 | * |
||
187 | * @var array |
||
874 | daniel-mar | 188 | * @access public |
827 | daniel-mar | 189 | */ |
190 | const STRING_TYPE_SIZE = [ |
||
191 | self::TYPE_UTF8_STRING => 0, |
||
192 | self::TYPE_BMP_STRING => 2, |
||
193 | self::TYPE_UNIVERSAL_STRING => 4, |
||
194 | self::TYPE_PRINTABLE_STRING => 1, |
||
195 | self::TYPE_TELETEX_STRING => 1, |
||
196 | self::TYPE_IA5_STRING => 1, |
||
197 | self::TYPE_VISIBLE_STRING => 1, |
||
198 | ]; |
||
199 | |||
200 | /** |
||
201 | * Parse BER-encoding |
||
202 | * |
||
203 | * Serves a similar purpose to openssl's asn1parse |
||
204 | * |
||
205 | * @param Element|string $encoded |
||
206 | * @return array |
||
874 | daniel-mar | 207 | * @access public |
827 | daniel-mar | 208 | */ |
209 | public static function decodeBER($encoded) |
||
210 | { |
||
211 | if ($encoded instanceof Element) { |
||
212 | $encoded = $encoded->element; |
||
213 | } |
||
214 | |||
215 | self::$encoded = $encoded; |
||
216 | |||
217 | $decoded = [self::decode_ber($encoded)]; |
||
218 | |||
219 | // encapsulate in an array for BC with the old decodeBER |
||
220 | return $decoded; |
||
221 | } |
||
222 | |||
223 | /** |
||
224 | * Parse BER-encoding (Helper function) |
||
225 | * |
||
226 | * Sometimes we want to get the BER encoding of a particular tag. $start lets us do that without having to reencode. |
||
227 | * $encoded is passed by reference for the recursive calls done for self::TYPE_BIT_STRING and |
||
228 | * self::TYPE_OCTET_STRING. In those cases, the indefinite length is used. |
||
229 | * |
||
230 | * @param string $encoded |
||
231 | * @param int $start |
||
232 | * @param int $encoded_pos |
||
233 | * @return array|bool |
||
874 | daniel-mar | 234 | * @access private |
827 | daniel-mar | 235 | */ |
236 | private static function decode_ber($encoded, $start = 0, $encoded_pos = 0) |
||
237 | { |
||
238 | $current = ['start' => $start]; |
||
239 | |||
240 | if (!isset($encoded[$encoded_pos])) { |
||
241 | return false; |
||
242 | } |
||
243 | $type = ord($encoded[$encoded_pos++]); |
||
244 | $startOffset = 1; |
||
245 | |||
246 | $constructed = ($type >> 5) & 1; |
||
247 | |||
248 | $tag = $type & 0x1F; |
||
249 | if ($tag == 0x1F) { |
||
250 | $tag = 0; |
||
251 | // process septets (since the eighth bit is ignored, it's not an octet) |
||
252 | do { |
||
253 | if (!isset($encoded[$encoded_pos])) { |
||
254 | return false; |
||
255 | } |
||
256 | $temp = ord($encoded[$encoded_pos++]); |
||
257 | $startOffset++; |
||
258 | $loop = $temp >> 7; |
||
259 | $tag <<= 7; |
||
260 | $temp &= 0x7F; |
||
261 | // "bits 7 to 1 of the first subsequent octet shall not all be zero" |
||
262 | if ($startOffset == 2 && $temp == 0) { |
||
263 | return false; |
||
264 | } |
||
265 | $tag |= $temp; |
||
266 | } while ($loop); |
||
267 | } |
||
268 | |||
269 | $start += $startOffset; |
||
270 | |||
271 | // Length, as discussed in paragraph 8.1.3 of X.690-0207.pdf#page=13 |
||
272 | if (!isset($encoded[$encoded_pos])) { |
||
273 | return false; |
||
274 | } |
||
275 | $length = ord($encoded[$encoded_pos++]); |
||
276 | $start++; |
||
277 | if ($length == 0x80) { // indefinite length |
||
278 | // "[A sender shall] use the indefinite form (see 8.1.3.6) if the encoding is constructed and is not all |
||
279 | // immediately available." -- paragraph 8.1.3.2.c |
||
280 | $length = strlen($encoded) - $encoded_pos; |
||
281 | } elseif ($length & 0x80) { // definite length, long form |
||
282 | // technically, the long form of the length can be represented by up to 126 octets (bytes), but we'll only |
||
283 | // support it up to four. |
||
284 | $length &= 0x7F; |
||
285 | $temp = substr($encoded, $encoded_pos, $length); |
||
286 | $encoded_pos += $length; |
||
287 | // tags of indefinte length don't really have a header length; this length includes the tag |
||
288 | $current += ['headerlength' => $length + 2]; |
||
289 | $start += $length; |
||
290 | extract(unpack('Nlength', substr(str_pad($temp, 4, chr(0), STR_PAD_LEFT), -4))); |
||
291 | /** @var integer $length */ |
||
292 | } else { |
||
293 | $current += ['headerlength' => 2]; |
||
294 | } |
||
295 | |||
296 | if ($length > (strlen($encoded) - $encoded_pos)) { |
||
297 | return false; |
||
298 | } |
||
299 | |||
300 | $content = substr($encoded, $encoded_pos, $length); |
||
301 | $content_pos = 0; |
||
302 | |||
303 | // at this point $length can be overwritten. it's only accurate for definite length things as is |
||
304 | |||
305 | /* Class is UNIVERSAL, APPLICATION, PRIVATE, or CONTEXT-SPECIFIC. The UNIVERSAL class is restricted to the ASN.1 |
||
306 | built-in types. It defines an application-independent data type that must be distinguishable from all other |
||
307 | data types. The other three classes are user defined. The APPLICATION class distinguishes data types that |
||
308 | have a wide, scattered use within a particular presentation context. PRIVATE distinguishes data types within |
||
309 | a particular organization or country. CONTEXT-SPECIFIC distinguishes members of a sequence or set, the |
||
310 | alternatives of a CHOICE, or universally tagged set members. Only the class number appears in braces for this |
||
311 | data type; the term CONTEXT-SPECIFIC does not appear. |
||
312 | |||
313 | -- http://www.obj-sys.com/asn1tutorial/node12.html */ |
||
314 | $class = ($type >> 6) & 3; |
||
315 | switch ($class) { |
||
316 | case self::CLASS_APPLICATION: |
||
317 | case self::CLASS_PRIVATE: |
||
318 | case self::CLASS_CONTEXT_SPECIFIC: |
||
319 | if (!$constructed) { |
||
320 | return [ |
||
321 | 'type' => $class, |
||
322 | 'constant' => $tag, |
||
323 | 'content' => $content, |
||
324 | 'length' => $length + $start - $current['start'] |
||
325 | ] + $current; |
||
326 | } |
||
327 | |||
328 | $newcontent = []; |
||
329 | $remainingLength = $length; |
||
330 | while ($remainingLength > 0) { |
||
331 | $temp = self::decode_ber($content, $start, $content_pos); |
||
332 | if ($temp === false) { |
||
333 | break; |
||
334 | } |
||
335 | $length = $temp['length']; |
||
336 | // end-of-content octets - see paragraph 8.1.5 |
||
337 | if (substr($content, $content_pos + $length, 2) == "\0\0") { |
||
338 | $length += 2; |
||
339 | $start += $length; |
||
340 | $newcontent[] = $temp; |
||
341 | break; |
||
342 | } |
||
343 | $start += $length; |
||
344 | $remainingLength -= $length; |
||
345 | $newcontent[] = $temp; |
||
346 | $content_pos += $length; |
||
347 | } |
||
348 | |||
349 | return [ |
||
350 | 'type' => $class, |
||
351 | 'constant' => $tag, |
||
352 | // the array encapsulation is for BC with the old format |
||
353 | 'content' => $newcontent, |
||
354 | // the only time when $content['headerlength'] isn't defined is when the length is indefinite. |
||
355 | // the absence of $content['headerlength'] is how we know if something is indefinite or not. |
||
356 | // technically, it could be defined to be 2 and then another indicator could be used but whatever. |
||
357 | 'length' => $start - $current['start'] |
||
358 | ] + $current; |
||
359 | } |
||
360 | |||
361 | $current += ['type' => $tag]; |
||
362 | |||
363 | // decode UNIVERSAL tags |
||
364 | switch ($tag) { |
||
365 | case self::TYPE_BOOLEAN: |
||
366 | // "The contents octets shall consist of a single octet." -- paragraph 8.2.1 |
||
367 | if ($constructed || strlen($content) != 1) { |
||
368 | return false; |
||
369 | } |
||
370 | $current['content'] = (bool) ord($content[$content_pos]); |
||
371 | break; |
||
372 | case self::TYPE_INTEGER: |
||
373 | case self::TYPE_ENUMERATED: |
||
374 | if ($constructed) { |
||
375 | return false; |
||
376 | } |
||
377 | $current['content'] = new BigInteger(substr($content, $content_pos), -256); |
||
378 | break; |
||
379 | case self::TYPE_REAL: // not currently supported |
||
380 | return false; |
||
381 | case self::TYPE_BIT_STRING: |
||
382 | // The initial octet shall encode, as an unsigned binary integer with bit 1 as the least significant bit, |
||
383 | // the number of unused bits in the final subsequent octet. The number shall be in the range zero to |
||
384 | // seven. |
||
385 | if (!$constructed) { |
||
386 | $current['content'] = substr($content, $content_pos); |
||
387 | } else { |
||
388 | $temp = self::decode_ber($content, $start, $content_pos); |
||
389 | if ($temp === false) { |
||
390 | return false; |
||
391 | } |
||
392 | $length -= (strlen($content) - $content_pos); |
||
393 | $last = count($temp) - 1; |
||
394 | for ($i = 0; $i < $last; $i++) { |
||
395 | // all subtags should be bit strings |
||
396 | if ($temp[$i]['type'] != self::TYPE_BIT_STRING) { |
||
397 | return false; |
||
398 | } |
||
399 | $current['content'] .= substr($temp[$i]['content'], 1); |
||
400 | } |
||
401 | // all subtags should be bit strings |
||
402 | if ($temp[$last]['type'] != self::TYPE_BIT_STRING) { |
||
403 | return false; |
||
404 | } |
||
405 | $current['content'] = $temp[$last]['content'][0] . $current['content'] . substr($temp[$i]['content'], 1); |
||
406 | } |
||
407 | break; |
||
408 | case self::TYPE_OCTET_STRING: |
||
409 | if (!$constructed) { |
||
410 | $current['content'] = substr($content, $content_pos); |
||
411 | } else { |
||
412 | $current['content'] = ''; |
||
413 | $length = 0; |
||
414 | while (substr($content, $content_pos, 2) != "\0\0") { |
||
415 | $temp = self::decode_ber($content, $length + $start, $content_pos); |
||
416 | if ($temp === false) { |
||
417 | return false; |
||
418 | } |
||
419 | $content_pos += $temp['length']; |
||
420 | // all subtags should be octet strings |
||
421 | if ($temp['type'] != self::TYPE_OCTET_STRING) { |
||
422 | return false; |
||
423 | } |
||
424 | $current['content'] .= $temp['content']; |
||
425 | $length += $temp['length']; |
||
426 | } |
||
427 | if (substr($content, $content_pos, 2) == "\0\0") { |
||
428 | $length += 2; // +2 for the EOC |
||
429 | } |
||
430 | } |
||
431 | break; |
||
432 | case self::TYPE_NULL: |
||
433 | // "The contents octets shall not contain any octets." -- paragraph 8.8.2 |
||
434 | if ($constructed || strlen($content)) { |
||
435 | return false; |
||
436 | } |
||
437 | break; |
||
438 | case self::TYPE_SEQUENCE: |
||
439 | case self::TYPE_SET: |
||
440 | if (!$constructed) { |
||
441 | return false; |
||
442 | } |
||
443 | $offset = 0; |
||
444 | $current['content'] = []; |
||
445 | $content_len = strlen($content); |
||
446 | while ($content_pos < $content_len) { |
||
447 | // if indefinite length construction was used and we have an end-of-content string next |
||
448 | // see paragraphs 8.1.1.3, 8.1.3.2, 8.1.3.6, 8.1.5, and (for an example) 8.6.4.2 |
||
449 | if (!isset($current['headerlength']) && substr($content, $content_pos, 2) == "\0\0") { |
||
450 | $length = $offset + 2; // +2 for the EOC |
||
451 | break 2; |
||
452 | } |
||
453 | $temp = self::decode_ber($content, $start + $offset, $content_pos); |
||
454 | if ($temp === false) { |
||
455 | return false; |
||
456 | } |
||
457 | $content_pos += $temp['length']; |
||
458 | $current['content'][] = $temp; |
||
459 | $offset += $temp['length']; |
||
460 | } |
||
461 | break; |
||
462 | case self::TYPE_OBJECT_IDENTIFIER: |
||
463 | if ($constructed) { |
||
464 | return false; |
||
465 | } |
||
466 | $current['content'] = self::decodeOID(substr($content, $content_pos)); |
||
467 | if ($current['content'] === false) { |
||
468 | return false; |
||
469 | } |
||
470 | break; |
||
471 | /* Each character string type shall be encoded as if it had been declared: |
||
472 | [UNIVERSAL x] IMPLICIT OCTET STRING |
||
473 | |||
474 | -- X.690-0207.pdf#page=23 (paragraph 8.21.3) |
||
475 | |||
476 | Per that, we're not going to do any validation. If there are any illegal characters in the string, |
||
477 | we don't really care */ |
||
478 | case self::TYPE_NUMERIC_STRING: |
||
479 | // 0,1,2,3,4,5,6,7,8,9, and space |
||
480 | case self::TYPE_PRINTABLE_STRING: |
||
481 | // Upper and lower case letters, digits, space, apostrophe, left/right parenthesis, plus sign, comma, |
||
482 | // hyphen, full stop, solidus, colon, equal sign, question mark |
||
483 | case self::TYPE_TELETEX_STRING: |
||
484 | // The Teletex character set in CCITT's T61, space, and delete |
||
485 | // see http://en.wikipedia.org/wiki/Teletex#Character_sets |
||
486 | case self::TYPE_VIDEOTEX_STRING: |
||
487 | // The Videotex character set in CCITT's T.100 and T.101, space, and delete |
||
488 | case self::TYPE_VISIBLE_STRING: |
||
489 | // Printing character sets of international ASCII, and space |
||
490 | case self::TYPE_IA5_STRING: |
||
491 | // International Alphabet 5 (International ASCII) |
||
492 | case self::TYPE_GRAPHIC_STRING: |
||
493 | // All registered G sets, and space |
||
494 | case self::TYPE_GENERAL_STRING: |
||
495 | // All registered C and G sets, space and delete |
||
496 | case self::TYPE_UTF8_STRING: |
||
497 | // ???? |
||
498 | case self::TYPE_BMP_STRING: |
||
499 | if ($constructed) { |
||
500 | return false; |
||
501 | } |
||
502 | $current['content'] = substr($content, $content_pos); |
||
503 | break; |
||
504 | case self::TYPE_UTC_TIME: |
||
505 | case self::TYPE_GENERALIZED_TIME: |
||
506 | if ($constructed) { |
||
507 | return false; |
||
508 | } |
||
509 | $current['content'] = self::decodeTime(substr($content, $content_pos), $tag); |
||
510 | break; |
||
511 | default: |
||
512 | return false; |
||
513 | } |
||
514 | |||
515 | $start += $length; |
||
516 | |||
517 | // ie. length is the length of the full TLV encoding - it's not just the length of the value |
||
518 | return $current + ['length' => $start - $current['start']]; |
||
519 | } |
||
520 | |||
521 | /** |
||
522 | * ASN.1 Map |
||
523 | * |
||
524 | * Provides an ASN.1 semantic mapping ($mapping) from a parsed BER-encoding to a human readable format. |
||
525 | * |
||
526 | * "Special" mappings may be applied on a per tag-name basis via $special. |
||
527 | * |
||
528 | * @param array $decoded |
||
529 | * @param array $mapping |
||
530 | * @param array $special |
||
531 | * @return array|bool|Element|string|null |
||
874 | daniel-mar | 532 | * @access public |
827 | daniel-mar | 533 | */ |
534 | public static function asn1map($decoded, $mapping, $special = []) |
||
535 | { |
||
536 | if (!is_array($decoded)) { |
||
537 | return false; |
||
538 | } |
||
539 | |||
540 | if (isset($mapping['explicit']) && is_array($decoded['content'])) { |
||
541 | $decoded = $decoded['content'][0]; |
||
542 | } |
||
543 | |||
544 | switch (true) { |
||
545 | case $mapping['type'] == self::TYPE_ANY: |
||
546 | $intype = $decoded['type']; |
||
547 | // !isset(self::ANY_MAP[$intype]) produces a fatal error on PHP 5.6 |
||
548 | if (isset($decoded['constant']) || !array_key_exists($intype, self::ANY_MAP) || (ord(self::$encoded[$decoded['start']]) & 0x20)) { |
||
549 | return new Element(substr(self::$encoded, $decoded['start'], $decoded['length'])); |
||
550 | } |
||
551 | $inmap = self::ANY_MAP[$intype]; |
||
552 | if (is_string($inmap)) { |
||
553 | return [$inmap => self::asn1map($decoded, ['type' => $intype] + $mapping, $special)]; |
||
554 | } |
||
555 | break; |
||
556 | case $mapping['type'] == self::TYPE_CHOICE: |
||
557 | foreach ($mapping['children'] as $key => $option) { |
||
558 | switch (true) { |
||
559 | case isset($option['constant']) && $option['constant'] == $decoded['constant']: |
||
560 | case !isset($option['constant']) && $option['type'] == $decoded['type']: |
||
561 | $value = self::asn1map($decoded, $option, $special); |
||
562 | break; |
||
563 | case !isset($option['constant']) && $option['type'] == self::TYPE_CHOICE: |
||
564 | $v = self::asn1map($decoded, $option, $special); |
||
565 | if (isset($v)) { |
||
566 | $value = $v; |
||
567 | } |
||
568 | } |
||
569 | if (isset($value)) { |
||
570 | if (isset($special[$key])) { |
||
571 | $value = $special[$key]($value); |
||
572 | } |
||
573 | return [$key => $value]; |
||
574 | } |
||
575 | } |
||
576 | return null; |
||
577 | case isset($mapping['implicit']): |
||
578 | case isset($mapping['explicit']): |
||
579 | case $decoded['type'] == $mapping['type']: |
||
580 | break; |
||
581 | default: |
||
582 | // if $decoded['type'] and $mapping['type'] are both strings, but different types of strings, |
||
583 | // let it through |
||
584 | switch (true) { |
||
585 | case $decoded['type'] < 18: // self::TYPE_NUMERIC_STRING == 18 |
||
586 | case $decoded['type'] > 30: // self::TYPE_BMP_STRING == 30 |
||
587 | case $mapping['type'] < 18: |
||
588 | case $mapping['type'] > 30: |
||
589 | return null; |
||
590 | } |
||
591 | } |
||
592 | |||
593 | if (isset($mapping['implicit'])) { |
||
594 | $decoded['type'] = $mapping['type']; |
||
595 | } |
||
596 | |||
597 | switch ($decoded['type']) { |
||
598 | case self::TYPE_SEQUENCE: |
||
599 | $map = []; |
||
600 | |||
601 | // ignore the min and max |
||
602 | if (isset($mapping['min']) && isset($mapping['max'])) { |
||
603 | $child = $mapping['children']; |
||
604 | foreach ($decoded['content'] as $content) { |
||
605 | if (($map[] = self::asn1map($content, $child, $special)) === null) { |
||
606 | return null; |
||
607 | } |
||
608 | } |
||
609 | |||
610 | return $map; |
||
611 | } |
||
612 | |||
613 | $n = count($decoded['content']); |
||
614 | $i = 0; |
||
615 | |||
616 | foreach ($mapping['children'] as $key => $child) { |
||
617 | $maymatch = $i < $n; // Match only existing input. |
||
618 | if ($maymatch) { |
||
619 | $temp = $decoded['content'][$i]; |
||
620 | |||
621 | if ($child['type'] != self::TYPE_CHOICE) { |
||
622 | // Get the mapping and input class & constant. |
||
623 | $childClass = $tempClass = self::CLASS_UNIVERSAL; |
||
624 | $constant = null; |
||
625 | if (isset($temp['constant'])) { |
||
626 | $tempClass = $temp['type']; |
||
627 | } |
||
628 | if (isset($child['class'])) { |
||
629 | $childClass = $child['class']; |
||
630 | $constant = $child['cast']; |
||
631 | } elseif (isset($child['constant'])) { |
||
632 | $childClass = self::CLASS_CONTEXT_SPECIFIC; |
||
633 | $constant = $child['constant']; |
||
634 | } |
||
635 | |||
636 | if (isset($constant) && isset($temp['constant'])) { |
||
637 | // Can only match if constants and class match. |
||
638 | $maymatch = $constant == $temp['constant'] && $childClass == $tempClass; |
||
639 | } else { |
||
640 | // Can only match if no constant expected and type matches or is generic. |
||
641 | $maymatch = !isset($child['constant']) && array_search($child['type'], [$temp['type'], self::TYPE_ANY, self::TYPE_CHOICE]) !== false; |
||
642 | } |
||
643 | } |
||
644 | } |
||
645 | |||
646 | if ($maymatch) { |
||
647 | // Attempt submapping. |
||
648 | $candidate = self::asn1map($temp, $child, $special); |
||
649 | $maymatch = $candidate !== null; |
||
650 | } |
||
651 | |||
652 | if ($maymatch) { |
||
653 | // Got the match: use it. |
||
654 | if (isset($special[$key])) { |
||
655 | $candidate = $special[$key]($candidate); |
||
656 | } |
||
657 | $map[$key] = $candidate; |
||
658 | $i++; |
||
659 | } elseif (isset($child['default'])) { |
||
660 | $map[$key] = $child['default']; |
||
661 | } elseif (!isset($child['optional'])) { |
||
662 | return null; // Syntax error. |
||
663 | } |
||
664 | } |
||
665 | |||
666 | // Fail mapping if all input items have not been consumed. |
||
667 | return $i < $n ? null : $map; |
||
668 | |||
669 | // the main diff between sets and sequences is the encapsulation of the foreach in another for loop |
||
670 | case self::TYPE_SET: |
||
671 | $map = []; |
||
672 | |||
673 | // ignore the min and max |
||
674 | if (isset($mapping['min']) && isset($mapping['max'])) { |
||
675 | $child = $mapping['children']; |
||
676 | foreach ($decoded['content'] as $content) { |
||
677 | if (($map[] = self::asn1map($content, $child, $special)) === null) { |
||
678 | return null; |
||
679 | } |
||
680 | } |
||
681 | |||
682 | return $map; |
||
683 | } |
||
684 | |||
685 | for ($i = 0; $i < count($decoded['content']); $i++) { |
||
686 | $temp = $decoded['content'][$i]; |
||
687 | $tempClass = self::CLASS_UNIVERSAL; |
||
688 | if (isset($temp['constant'])) { |
||
689 | $tempClass = $temp['type']; |
||
690 | } |
||
691 | |||
692 | foreach ($mapping['children'] as $key => $child) { |
||
693 | if (isset($map[$key])) { |
||
694 | continue; |
||
695 | } |
||
696 | $maymatch = true; |
||
697 | if ($child['type'] != self::TYPE_CHOICE) { |
||
698 | $childClass = self::CLASS_UNIVERSAL; |
||
699 | $constant = null; |
||
700 | if (isset($child['class'])) { |
||
701 | $childClass = $child['class']; |
||
702 | $constant = $child['cast']; |
||
703 | } elseif (isset($child['constant'])) { |
||
704 | $childClass = self::CLASS_CONTEXT_SPECIFIC; |
||
705 | $constant = $child['constant']; |
||
706 | } |
||
707 | |||
708 | if (isset($constant) && isset($temp['constant'])) { |
||
709 | // Can only match if constants and class match. |
||
710 | $maymatch = $constant == $temp['constant'] && $childClass == $tempClass; |
||
711 | } else { |
||
712 | // Can only match if no constant expected and type matches or is generic. |
||
713 | $maymatch = !isset($child['constant']) && array_search($child['type'], [$temp['type'], self::TYPE_ANY, self::TYPE_CHOICE]) !== false; |
||
714 | } |
||
715 | } |
||
716 | |||
717 | if ($maymatch) { |
||
718 | // Attempt submapping. |
||
719 | $candidate = self::asn1map($temp, $child, $special); |
||
720 | $maymatch = $candidate !== null; |
||
721 | } |
||
722 | |||
723 | if (!$maymatch) { |
||
724 | break; |
||
725 | } |
||
726 | |||
727 | // Got the match: use it. |
||
728 | if (isset($special[$key])) { |
||
729 | $candidate = $special[$key]($candidate); |
||
730 | } |
||
731 | $map[$key] = $candidate; |
||
732 | break; |
||
733 | } |
||
734 | } |
||
735 | |||
736 | foreach ($mapping['children'] as $key => $child) { |
||
737 | if (!isset($map[$key])) { |
||
738 | if (isset($child['default'])) { |
||
739 | $map[$key] = $child['default']; |
||
740 | } elseif (!isset($child['optional'])) { |
||
741 | return null; |
||
742 | } |
||
743 | } |
||
744 | } |
||
745 | return $map; |
||
746 | case self::TYPE_OBJECT_IDENTIFIER: |
||
747 | return isset(self::$oids[$decoded['content']]) ? self::$oids[$decoded['content']] : $decoded['content']; |
||
748 | case self::TYPE_UTC_TIME: |
||
749 | case self::TYPE_GENERALIZED_TIME: |
||
750 | // for explicitly tagged optional stuff |
||
751 | if (is_array($decoded['content'])) { |
||
752 | $decoded['content'] = $decoded['content'][0]['content']; |
||
753 | } |
||
754 | // for implicitly tagged optional stuff |
||
755 | // in theory, doing isset($mapping['implicit']) would work but malformed certs do exist |
||
756 | // in the wild that OpenSSL decodes without issue so we'll support them as well |
||
757 | if (!is_object($decoded['content'])) { |
||
758 | $decoded['content'] = self::decodeTime($decoded['content'], $decoded['type']); |
||
759 | } |
||
760 | return $decoded['content'] ? $decoded['content']->format(self::$format) : false; |
||
761 | case self::TYPE_BIT_STRING: |
||
762 | if (isset($mapping['mapping'])) { |
||
763 | $offset = ord($decoded['content'][0]); |
||
764 | $size = (strlen($decoded['content']) - 1) * 8 - $offset; |
||
765 | /* |
||
766 | From X.680-0207.pdf#page=46 (21.7): |
||
767 | |||
768 | "When a "NamedBitList" is used in defining a bitstring type ASN.1 encoding rules are free to add (or remove) |
||
769 | arbitrarily any trailing 0 bits to (or from) values that are being encoded or decoded. Application designers should |
||
770 | therefore ensure that different semantics are not associated with such values which differ only in the number of trailing |
||
771 | |||
772 | */ |
||
773 | $bits = count($mapping['mapping']) == $size ? [] : array_fill(0, count($mapping['mapping']) - $size, false); |
||
774 | for ($i = strlen($decoded['content']) - 1; $i > 0; $i--) { |
||
775 | $current = ord($decoded['content'][$i]); |
||
776 | for ($j = $offset; $j < 8; $j++) { |
||
777 | $bits[] = (bool) ($current & (1 << $j)); |
||
778 | } |
||
779 | $offset = 0; |
||
780 | } |
||
781 | $values = []; |
||
782 | $map = array_reverse($mapping['mapping']); |
||
783 | foreach ($map as $i => $value) { |
||
784 | if ($bits[$i]) { |
||
785 | $values[] = $value; |
||
786 | } |
||
787 | } |
||
788 | return $values; |
||
789 | } |
||
790 | // fall-through |
||
791 | case self::TYPE_OCTET_STRING: |
||
792 | return $decoded['content']; |
||
793 | case self::TYPE_NULL: |
||
794 | return ''; |
||
795 | case self::TYPE_BOOLEAN: |
||
796 | case self::TYPE_NUMERIC_STRING: |
||
797 | case self::TYPE_PRINTABLE_STRING: |
||
798 | case self::TYPE_TELETEX_STRING: |
||
799 | case self::TYPE_VIDEOTEX_STRING: |
||
800 | case self::TYPE_IA5_STRING: |
||
801 | case self::TYPE_GRAPHIC_STRING: |
||
802 | case self::TYPE_VISIBLE_STRING: |
||
803 | case self::TYPE_GENERAL_STRING: |
||
804 | case self::TYPE_UNIVERSAL_STRING: |
||
805 | case self::TYPE_UTF8_STRING: |
||
806 | case self::TYPE_BMP_STRING: |
||
807 | return $decoded['content']; |
||
808 | case self::TYPE_INTEGER: |
||
809 | case self::TYPE_ENUMERATED: |
||
810 | $temp = $decoded['content']; |
||
811 | if (isset($mapping['implicit'])) { |
||
812 | $temp = new BigInteger($decoded['content'], -256); |
||
813 | } |
||
814 | if (isset($mapping['mapping'])) { |
||
815 | $temp = (int) $temp->toString(); |
||
816 | return isset($mapping['mapping'][$temp]) ? |
||
817 | $mapping['mapping'][$temp] : |
||
818 | false; |
||
819 | } |
||
820 | return $temp; |
||
821 | } |
||
822 | } |
||
823 | |||
824 | /** |
||
825 | * DER-decode the length |
||
826 | * |
||
827 | * DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4. See |
||
828 | * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information. |
||
829 | * |
||
874 | daniel-mar | 830 | * @access public |
827 | daniel-mar | 831 | * @param string $string |
832 | * @return int |
||
833 | */ |
||
834 | public static function decodeLength(&$string) |
||
835 | { |
||
836 | $length = ord(Strings::shift($string)); |
||
837 | if ($length & 0x80) { // definite length, long form |
||
838 | $length &= 0x7F; |
||
839 | $temp = Strings::shift($string, $length); |
||
840 | list(, $length) = unpack('N', substr(str_pad($temp, 4, chr(0), STR_PAD_LEFT), -4)); |
||
841 | } |
||
842 | return $length; |
||
843 | } |
||
844 | |||
845 | /** |
||
846 | * ASN.1 Encode |
||
847 | * |
||
848 | * DER-encodes an ASN.1 semantic mapping ($mapping). Some libraries would probably call this function |
||
849 | * an ASN.1 compiler. |
||
850 | * |
||
851 | * "Special" mappings can be applied via $special. |
||
852 | * |
||
853 | * @param Element|string|array $source |
||
854 | * @param array $mapping |
||
855 | * @param array $special |
||
856 | * @return string |
||
874 | daniel-mar | 857 | * @access public |
827 | daniel-mar | 858 | */ |
859 | public static function encodeDER($source, $mapping, $special = []) |
||
860 | { |
||
861 | self::$location = []; |
||
862 | return self::encode_der($source, $mapping, null, $special); |
||
863 | } |
||
864 | |||
865 | /** |
||
866 | * ASN.1 Encode (Helper function) |
||
867 | * |
||
868 | * @param Element|string|array|null $source |
||
869 | * @param array $mapping |
||
870 | * @param int $idx |
||
871 | * @param array $special |
||
872 | * @return string |
||
874 | daniel-mar | 873 | * @access private |
827 | daniel-mar | 874 | */ |
875 | private static function encode_der($source, $mapping, $idx = null, $special = []) |
||
876 | { |
||
877 | if ($source instanceof Element) { |
||
878 | return $source->element; |
||
879 | } |
||
880 | |||
881 | // do not encode (implicitly optional) fields with value set to default |
||
882 | if (isset($mapping['default']) && $source === $mapping['default']) { |
||
883 | return ''; |
||
884 | } |
||
885 | |||
886 | if (isset($idx)) { |
||
887 | if (isset($special[$idx])) { |
||
888 | $source = $special[$idx]($source); |
||
889 | } |
||
890 | self::$location[] = $idx; |
||
891 | } |
||
892 | |||
893 | $tag = $mapping['type']; |
||
894 | |||
895 | switch ($tag) { |
||
896 | case self::TYPE_SET: // Children order is not important, thus process in sequence. |
||
897 | case self::TYPE_SEQUENCE: |
||
898 | $tag |= 0x20; // set the constructed bit |
||
899 | |||
900 | // ignore the min and max |
||
901 | if (isset($mapping['min']) && isset($mapping['max'])) { |
||
902 | $value = []; |
||
903 | $child = $mapping['children']; |
||
904 | |||
905 | foreach ($source as $content) { |
||
906 | $temp = self::encode_der($content, $child, null, $special); |
||
907 | if ($temp === false) { |
||
908 | return false; |
||
909 | } |
||
910 | $value[] = $temp; |
||
911 | } |
||
912 | /* "The encodings of the component values of a set-of value shall appear in ascending order, the encodings being compared |
||
913 | as octet strings with the shorter components being padded at their trailing end with 0-octets. |
||
914 | NOTE - The padding octets are for comparison purposes only and do not appear in the encodings." |
||
915 | |||
916 | -- sec 11.6 of http://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf */ |
||
917 | if ($mapping['type'] == self::TYPE_SET) { |
||
918 | sort($value); |
||
919 | } |
||
920 | $value = implode('', $value); |
||
921 | break; |
||
922 | } |
||
923 | |||
924 | $value = ''; |
||
925 | foreach ($mapping['children'] as $key => $child) { |
||
926 | if (!array_key_exists($key, $source)) { |
||
927 | if (!isset($child['optional'])) { |
||
928 | return false; |
||
929 | } |
||
930 | continue; |
||
931 | } |
||
932 | |||
933 | $temp = self::encode_der($source[$key], $child, $key, $special); |
||
934 | if ($temp === false) { |
||
935 | return false; |
||
936 | } |
||
937 | |||
938 | // An empty child encoding means it has been optimized out. |
||
939 | // Else we should have at least one tag byte. |
||
940 | if ($temp === '') { |
||
941 | continue; |
||
942 | } |
||
943 | |||
944 | // if isset($child['constant']) is true then isset($child['optional']) should be true as well |
||
945 | if (isset($child['constant'])) { |
||
946 | /* |
||
947 | From X.680-0207.pdf#page=58 (30.6): |
||
948 | |||
949 | "The tagging construction specifies explicit tagging if any of the following holds: |
||
950 | ... |
||
951 | c) the "Tag Type" alternative is used and the value of "TagDefault" for the module is IMPLICIT TAGS or |
||
952 | AUTOMATIC TAGS, but the type defined by "Type" is an untagged choice type, an untagged open type, or |
||
953 | an untagged "DummyReference" (see ITU-T Rec. X.683 | ISO/IEC 8824-4, 8.3)." |
||
954 | */ |
||
955 | if (isset($child['explicit']) || $child['type'] == self::TYPE_CHOICE) { |
||
956 | $subtag = chr((self::CLASS_CONTEXT_SPECIFIC << 6) | 0x20 | $child['constant']); |
||
957 | $temp = $subtag . self::encodeLength(strlen($temp)) . $temp; |
||
958 | } else { |
||
959 | $subtag = chr((self::CLASS_CONTEXT_SPECIFIC << 6) | (ord($temp[0]) & 0x20) | $child['constant']); |
||
960 | $temp = $subtag . substr($temp, 1); |
||
961 | } |
||
962 | } |
||
963 | $value .= $temp; |
||
964 | } |
||
965 | break; |
||
966 | case self::TYPE_CHOICE: |
||
967 | $temp = false; |
||
968 | |||
969 | foreach ($mapping['children'] as $key => $child) { |
||
970 | if (!isset($source[$key])) { |
||
971 | continue; |
||
972 | } |
||
973 | |||
974 | $temp = self::encode_der($source[$key], $child, $key, $special); |
||
975 | if ($temp === false) { |
||
976 | return false; |
||
977 | } |
||
978 | |||
979 | // An empty child encoding means it has been optimized out. |
||
980 | // Else we should have at least one tag byte. |
||
981 | if ($temp === '') { |
||
982 | continue; |
||
983 | } |
||
984 | |||
985 | $tag = ord($temp[0]); |
||
986 | |||
987 | // if isset($child['constant']) is true then isset($child['optional']) should be true as well |
||
988 | if (isset($child['constant'])) { |
||
989 | if (isset($child['explicit']) || $child['type'] == self::TYPE_CHOICE) { |
||
990 | $subtag = chr((self::CLASS_CONTEXT_SPECIFIC << 6) | 0x20 | $child['constant']); |
||
991 | $temp = $subtag . self::encodeLength(strlen($temp)) . $temp; |
||
992 | } else { |
||
993 | $subtag = chr((self::CLASS_CONTEXT_SPECIFIC << 6) | (ord($temp[0]) & 0x20) | $child['constant']); |
||
994 | $temp = $subtag . substr($temp, 1); |
||
995 | } |
||
996 | } |
||
997 | } |
||
998 | |||
999 | if (isset($idx)) { |
||
1000 | array_pop(self::$location); |
||
1001 | } |
||
1002 | |||
1003 | if ($temp && isset($mapping['cast'])) { |
||
1004 | $temp[0] = chr(($mapping['class'] << 6) | ($tag & 0x20) | $mapping['cast']); |
||
1005 | } |
||
1006 | |||
1007 | return $temp; |
||
1008 | case self::TYPE_INTEGER: |
||
1009 | case self::TYPE_ENUMERATED: |
||
1010 | if (!isset($mapping['mapping'])) { |
||
1011 | if (is_numeric($source)) { |
||
1012 | $source = new BigInteger($source); |
||
1013 | } |
||
1014 | $value = $source->toBytes(true); |
||
1015 | } else { |
||
1016 | $value = array_search($source, $mapping['mapping']); |
||
1017 | if ($value === false) { |
||
1018 | return false; |
||
1019 | } |
||
1020 | $value = new BigInteger($value); |
||
1021 | $value = $value->toBytes(true); |
||
1022 | } |
||
1023 | if (!strlen($value)) { |
||
1024 | $value = chr(0); |
||
1025 | } |
||
1026 | break; |
||
1027 | case self::TYPE_UTC_TIME: |
||
1028 | case self::TYPE_GENERALIZED_TIME: |
||
1029 | $format = $mapping['type'] == self::TYPE_UTC_TIME ? 'y' : 'Y'; |
||
1030 | $format .= 'mdHis'; |
||
1031 | // if $source does _not_ include timezone information within it then assume that the timezone is GMT |
||
1032 | $date = new \DateTime($source, new \DateTimeZone('GMT')); |
||
1033 | // if $source _does_ include timezone information within it then convert the time to GMT |
||
1034 | $date->setTimezone(new \DateTimeZone('GMT')); |
||
1035 | $value = $date->format($format) . 'Z'; |
||
1036 | break; |
||
1037 | case self::TYPE_BIT_STRING: |
||
1038 | if (isset($mapping['mapping'])) { |
||
1039 | $bits = array_fill(0, count($mapping['mapping']), 0); |
||
1040 | $size = 0; |
||
1041 | for ($i = 0; $i < count($mapping['mapping']); $i++) { |
||
1042 | if (in_array($mapping['mapping'][$i], $source)) { |
||
1043 | $bits[$i] = 1; |
||
1044 | $size = $i; |
||
1045 | } |
||
1046 | } |
||
1047 | |||
1048 | if (isset($mapping['min']) && $mapping['min'] >= 1 && $size < $mapping['min']) { |
||
1049 | $size = $mapping['min'] - 1; |
||
1050 | } |
||
1051 | |||
1052 | $offset = 8 - (($size + 1) & 7); |
||
1053 | $offset = $offset !== 8 ? $offset : 0; |
||
1054 | |||
1055 | $value = chr($offset); |
||
1056 | |||
1057 | for ($i = $size + 1; $i < count($mapping['mapping']); $i++) { |
||
1058 | unset($bits[$i]); |
||
1059 | } |
||
1060 | |||
1061 | $bits = implode('', array_pad($bits, $size + $offset + 1, 0)); |
||
1062 | $bytes = explode(' ', rtrim(chunk_split($bits, 8, ' '))); |
||
1063 | foreach ($bytes as $byte) { |
||
1064 | $value .= chr(bindec($byte)); |
||
1065 | } |
||
1066 | |||
1067 | break; |
||
1068 | } |
||
1069 | // fall-through |
||
1070 | case self::TYPE_OCTET_STRING: |
||
1071 | /* The initial octet shall encode, as an unsigned binary integer with bit 1 as the least significant bit, |
||
1072 | the number of unused bits in the final subsequent octet. The number shall be in the range zero to seven. |
||
1073 | |||
1074 | -- http://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#page=16 */ |
||
1075 | $value = $source; |
||
1076 | break; |
||
1077 | case self::TYPE_OBJECT_IDENTIFIER: |
||
1078 | $value = self::encodeOID($source); |
||
1079 | break; |
||
1080 | case self::TYPE_ANY: |
||
1081 | $loc = self::$location; |
||
1082 | if (isset($idx)) { |
||
1083 | array_pop(self::$location); |
||
1084 | } |
||
1085 | |||
1086 | switch (true) { |
||
1087 | case !isset($source): |
||
1088 | return self::encode_der(null, ['type' => self::TYPE_NULL] + $mapping, null, $special); |
||
1089 | case is_int($source): |
||
1090 | case $source instanceof BigInteger: |
||
1091 | return self::encode_der($source, ['type' => self::TYPE_INTEGER] + $mapping, null, $special); |
||
1092 | case is_float($source): |
||
1093 | return self::encode_der($source, ['type' => self::TYPE_REAL] + $mapping, null, $special); |
||
1094 | case is_bool($source): |
||
1095 | return self::encode_der($source, ['type' => self::TYPE_BOOLEAN] + $mapping, null, $special); |
||
1096 | case is_array($source) && count($source) == 1: |
||
1097 | $typename = implode('', array_keys($source)); |
||
1098 | $outtype = array_search($typename, self::ANY_MAP, true); |
||
1099 | if ($outtype !== false) { |
||
1100 | return self::encode_der($source[$typename], ['type' => $outtype] + $mapping, null, $special); |
||
1101 | } |
||
1102 | } |
||
1103 | |||
1104 | $filters = self::$filters; |
||
1105 | foreach ($loc as $part) { |
||
1106 | if (!isset($filters[$part])) { |
||
1107 | $filters = false; |
||
1108 | break; |
||
1109 | } |
||
1110 | $filters = $filters[$part]; |
||
1111 | } |
||
1112 | if ($filters === false) { |
||
1113 | throw new \RuntimeException('No filters defined for ' . implode('/', $loc)); |
||
1114 | } |
||
1115 | return self::encode_der($source, $filters + $mapping, null, $special); |
||
1116 | case self::TYPE_NULL: |
||
1117 | $value = ''; |
||
1118 | break; |
||
1119 | case self::TYPE_NUMERIC_STRING: |
||
1120 | case self::TYPE_TELETEX_STRING: |
||
1121 | case self::TYPE_PRINTABLE_STRING: |
||
1122 | case self::TYPE_UNIVERSAL_STRING: |
||
1123 | case self::TYPE_UTF8_STRING: |
||
1124 | case self::TYPE_BMP_STRING: |
||
1125 | case self::TYPE_IA5_STRING: |
||
1126 | case self::TYPE_VISIBLE_STRING: |
||
1127 | case self::TYPE_VIDEOTEX_STRING: |
||
1128 | case self::TYPE_GRAPHIC_STRING: |
||
1129 | case self::TYPE_GENERAL_STRING: |
||
1130 | $value = $source; |
||
1131 | break; |
||
1132 | case self::TYPE_BOOLEAN: |
||
1133 | $value = $source ? "\xFF" : "\x00"; |
||
1134 | break; |
||
1135 | default: |
||
1136 | throw new \RuntimeException('Mapping provides no type definition for ' . implode('/', self::$location)); |
||
1137 | } |
||
1138 | |||
1139 | if (isset($idx)) { |
||
1140 | array_pop(self::$location); |
||
1141 | } |
||
1142 | |||
1143 | if (isset($mapping['cast'])) { |
||
1144 | if (isset($mapping['explicit']) || $mapping['type'] == self::TYPE_CHOICE) { |
||
1145 | $value = chr($tag) . self::encodeLength(strlen($value)) . $value; |
||
1146 | $tag = ($mapping['class'] << 6) | 0x20 | $mapping['cast']; |
||
1147 | } else { |
||
1148 | $tag = ($mapping['class'] << 6) | (ord($temp[0]) & 0x20) | $mapping['cast']; |
||
1149 | } |
||
1150 | } |
||
1151 | |||
1152 | return chr($tag) . self::encodeLength(strlen($value)) . $value; |
||
1153 | } |
||
1154 | |||
1155 | /** |
||
1156 | * BER-decode the OID |
||
1157 | * |
||
1158 | * Called by _decode_ber() |
||
1159 | * |
||
874 | daniel-mar | 1160 | * @access public |
827 | daniel-mar | 1161 | * @param string $content |
1162 | * @return string |
||
1163 | */ |
||
1164 | public static function decodeOID($content) |
||
1165 | { |
||
1166 | static $eighty; |
||
1167 | if (!$eighty) { |
||
1168 | $eighty = new BigInteger(80); |
||
1169 | } |
||
1170 | |||
1171 | $oid = []; |
||
1172 | $pos = 0; |
||
1173 | $len = strlen($content); |
||
1174 | |||
1175 | if (ord($content[$len - 1]) & 0x80) { |
||
1176 | return false; |
||
1177 | } |
||
1178 | |||
1179 | $n = new BigInteger(); |
||
1180 | while ($pos < $len) { |
||
1181 | $temp = ord($content[$pos++]); |
||
1182 | $n = $n->bitwise_leftShift(7); |
||
1183 | $n = $n->bitwise_or(new BigInteger($temp & 0x7F)); |
||
1184 | if (~$temp & 0x80) { |
||
1185 | $oid[] = $n; |
||
1186 | $n = new BigInteger(); |
||
1187 | } |
||
1188 | } |
||
1189 | $part1 = array_shift($oid); |
||
1190 | $first = floor(ord($content[0]) / 40); |
||
1191 | /* |
||
1192 | "This packing of the first two object identifier components recognizes that only three values are allocated from the root |
||
1193 | node, and at most 39 subsequent values from nodes reached by X = 0 and X = 1." |
||
1194 | |||
1195 | -- https://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#page=22 |
||
1196 | */ |
||
1197 | if ($first <= 2) { // ie. 0 <= ord($content[0]) < 120 (0x78) |
||
1198 | array_unshift($oid, ord($content[0]) % 40); |
||
1199 | array_unshift($oid, $first); |
||
1200 | } else { |
||
1201 | array_unshift($oid, $part1->subtract($eighty)); |
||
1202 | array_unshift($oid, 2); |
||
1203 | } |
||
1204 | |||
1205 | return implode('.', $oid); |
||
1206 | } |
||
1207 | |||
1208 | /** |
||
1209 | * DER-encode the OID |
||
1210 | * |
||
1211 | * Called by _encode_der() |
||
1212 | * |
||
874 | daniel-mar | 1213 | * @access public |
827 | daniel-mar | 1214 | * @param string $source |
1215 | * @return string |
||
1216 | */ |
||
1217 | public static function encodeOID($source) |
||
1218 | { |
||
1219 | static $mask, $zero, $forty; |
||
1220 | if (!$mask) { |
||
1221 | $mask = new BigInteger(0x7F); |
||
1222 | $zero = new BigInteger(); |
||
1223 | $forty = new BigInteger(40); |
||
1224 | } |
||
1225 | |||
1226 | if (!preg_match('#(?:\d+\.)+#', $source)) { |
||
1227 | $oid = isset(self::$reverseOIDs[$source]) ? self::$reverseOIDs[$source] : false; |
||
1228 | } else { |
||
1229 | $oid = $source; |
||
1230 | } |
||
1231 | if ($oid === false) { |
||
1232 | throw new \RuntimeException('Invalid OID'); |
||
1233 | } |
||
1234 | |||
1235 | $parts = explode('.', $oid); |
||
1236 | $part1 = array_shift($parts); |
||
1237 | $part2 = array_shift($parts); |
||
1238 | |||
1239 | $first = new BigInteger($part1); |
||
1240 | $first = $first->multiply($forty); |
||
1241 | $first = $first->add(new BigInteger($part2)); |
||
1242 | |||
1243 | array_unshift($parts, $first->toString()); |
||
1244 | |||
1245 | $value = ''; |
||
1246 | foreach ($parts as $part) { |
||
1247 | if (!$part) { |
||
1248 | $temp = "\0"; |
||
1249 | } else { |
||
1250 | $temp = ''; |
||
1251 | $part = new BigInteger($part); |
||
1252 | while (!$part->equals($zero)) { |
||
1253 | $submask = $part->bitwise_and($mask); |
||
1254 | $submask->setPrecision(8); |
||
1255 | $temp = (chr(0x80) | $submask->toBytes()) . $temp; |
||
1256 | $part = $part->bitwise_rightShift(7); |
||
1257 | } |
||
1258 | $temp[strlen($temp) - 1] = $temp[strlen($temp) - 1] & chr(0x7F); |
||
1259 | } |
||
1260 | $value .= $temp; |
||
1261 | } |
||
1262 | |||
1263 | return $value; |
||
1264 | } |
||
1265 | |||
1266 | /** |
||
1267 | * BER-decode the time |
||
1268 | * |
||
1269 | * Called by _decode_ber() and in the case of implicit tags asn1map(). |
||
1270 | * |
||
874 | daniel-mar | 1271 | * @access private |
827 | daniel-mar | 1272 | * @param string $content |
1273 | * @param int $tag |
||
1274 | * @return \DateTime|false |
||
1275 | */ |
||
1276 | private static function decodeTime($content, $tag) |
||
1277 | { |
||
1278 | /* UTCTime: |
||
1279 | http://tools.ietf.org/html/rfc5280#section-4.1.2.5.1 |
||
1280 | http://www.obj-sys.com/asn1tutorial/node15.html |
||
1281 | |||
1282 | GeneralizedTime: |
||
1283 | http://tools.ietf.org/html/rfc5280#section-4.1.2.5.2 |
||
1284 | http://www.obj-sys.com/asn1tutorial/node14.html */ |
||
1285 | |||
1286 | $format = 'YmdHis'; |
||
1287 | |||
1288 | if ($tag == self::TYPE_UTC_TIME) { |
||
1289 | // https://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#page=28 says "the seconds |
||
1290 | // element shall always be present" but none-the-less I've seen X509 certs where it isn't and if the |
||
1291 | // browsers parse it phpseclib ought to too |
||
1292 | if (preg_match('#^(\d{10})(Z|[+-]\d{4})$#', $content, $matches)) { |
||
1293 | $content = $matches[1] . '00' . $matches[2]; |
||
1294 | } |
||
1295 | $prefix = substr($content, 0, 2) >= 50 ? '19' : '20'; |
||
1296 | $content = $prefix . $content; |
||
1297 | } elseif (strpos($content, '.') !== false) { |
||
1298 | $format .= '.u'; |
||
1299 | } |
||
1300 | |||
1301 | if ($content[strlen($content) - 1] == 'Z') { |
||
1302 | $content = substr($content, 0, -1) . '+0000'; |
||
1303 | } |
||
1304 | |||
1305 | if (strpos($content, '-') !== false || strpos($content, '+') !== false) { |
||
1306 | $format .= 'O'; |
||
1307 | } |
||
1308 | |||
1309 | // error supression isn't necessary as of PHP 7.0: |
||
1310 | // http://php.net/manual/en/migration70.other-changes.php |
||
1311 | return @\DateTime::createFromFormat($format, $content); |
||
1312 | } |
||
1313 | |||
1314 | /** |
||
1315 | * Set the time format |
||
1316 | * |
||
1317 | * Sets the time / date format for asn1map(). |
||
1318 | * |
||
874 | daniel-mar | 1319 | * @access public |
827 | daniel-mar | 1320 | * @param string $format |
1321 | */ |
||
1322 | public static function setTimeFormat($format) |
||
1323 | { |
||
1324 | self::$format = $format; |
||
1325 | } |
||
1326 | |||
1327 | /** |
||
1328 | * Load OIDs |
||
1329 | * |
||
1330 | * Load the relevant OIDs for a particular ASN.1 semantic mapping. |
||
1331 | * Previously loaded OIDs are retained. |
||
1332 | * |
||
874 | daniel-mar | 1333 | * @access public |
827 | daniel-mar | 1334 | * @param array $oids |
1335 | */ |
||
1336 | public static function loadOIDs($oids) |
||
1337 | { |
||
1338 | self::$reverseOIDs += $oids; |
||
1339 | self::$oids = array_flip(self::$reverseOIDs); |
||
1340 | } |
||
1341 | |||
1342 | /** |
||
1343 | * Set filters |
||
1344 | * |
||
1345 | * See \phpseclib3\File\X509, etc, for an example. |
||
1346 | * Previously loaded filters are not retained. |
||
1347 | * |
||
874 | daniel-mar | 1348 | * @access public |
827 | daniel-mar | 1349 | * @param array $filters |
1350 | */ |
||
1351 | public static function setFilters($filters) |
||
1352 | { |
||
1353 | self::$filters = $filters; |
||
1354 | } |
||
1355 | |||
1356 | /** |
||
1357 | * String type conversion |
||
1358 | * |
||
1359 | * This is a lazy conversion, dealing only with character size. |
||
1360 | * No real conversion table is used. |
||
1361 | * |
||
1362 | * @param string $in |
||
1363 | * @param int $from |
||
1364 | * @param int $to |
||
1365 | * @return string |
||
874 | daniel-mar | 1366 | * @access public |
827 | daniel-mar | 1367 | */ |
1368 | public static function convert($in, $from = self::TYPE_UTF8_STRING, $to = self::TYPE_UTF8_STRING) |
||
1369 | { |
||
1370 | // isset(self::STRING_TYPE_SIZE[$from] returns a fatal error on PHP 5.6 |
||
1371 | if (!array_key_exists($from, self::STRING_TYPE_SIZE) || !array_key_exists($to, self::STRING_TYPE_SIZE)) { |
||
1372 | return false; |
||
1373 | } |
||
1374 | $insize = self::STRING_TYPE_SIZE[$from]; |
||
1375 | $outsize = self::STRING_TYPE_SIZE[$to]; |
||
1376 | $inlength = strlen($in); |
||
1377 | $out = ''; |
||
1378 | |||
1379 | for ($i = 0; $i < $inlength;) { |
||
1380 | if ($inlength - $i < $insize) { |
||
1381 | return false; |
||
1382 | } |
||
1383 | |||
1384 | // Get an input character as a 32-bit value. |
||
1385 | $c = ord($in[$i++]); |
||
1386 | switch (true) { |
||
1387 | case $insize == 4: |
||
1388 | $c = ($c << 8) | ord($in[$i++]); |
||
1389 | $c = ($c << 8) | ord($in[$i++]); |
||
1390 | // fall-through |
||
1391 | case $insize == 2: |
||
1392 | $c = ($c << 8) | ord($in[$i++]); |
||
1393 | // fall-through |
||
1394 | case $insize == 1: |
||
1395 | break; |
||
1396 | case ($c & 0x80) == 0x00: |
||
1397 | break; |
||
1398 | case ($c & 0x40) == 0x00: |
||
1399 | return false; |
||
1400 | default: |
||
1401 | $bit = 6; |
||
1402 | do { |
||
1403 | if ($bit > 25 || $i >= $inlength || (ord($in[$i]) & 0xC0) != 0x80) { |
||
1404 | return false; |
||
1405 | } |
||
1406 | $c = ($c << 6) | (ord($in[$i++]) & 0x3F); |
||
1407 | $bit += 5; |
||
1408 | $mask = 1 << $bit; |
||
1409 | } while ($c & $bit); |
||
1410 | $c &= $mask - 1; |
||
1411 | break; |
||
1412 | } |
||
1413 | |||
1414 | // Convert and append the character to output string. |
||
1415 | $v = ''; |
||
1416 | switch (true) { |
||
1417 | case $outsize == 4: |
||
1418 | $v .= chr($c & 0xFF); |
||
1419 | $c >>= 8; |
||
1420 | $v .= chr($c & 0xFF); |
||
1421 | $c >>= 8; |
||
1422 | // fall-through |
||
1423 | case $outsize == 2: |
||
1424 | $v .= chr($c & 0xFF); |
||
1425 | $c >>= 8; |
||
1426 | // fall-through |
||
1427 | case $outsize == 1: |
||
1428 | $v .= chr($c & 0xFF); |
||
1429 | $c >>= 8; |
||
1430 | if ($c) { |
||
1431 | return false; |
||
1432 | } |
||
1433 | break; |
||
1434 | case ($c & 0x80000000) != 0: |
||
1435 | return false; |
||
1436 | case $c >= 0x04000000: |
||
1437 | $v .= chr(0x80 | ($c & 0x3F)); |
||
1438 | $c = ($c >> 6) | 0x04000000; |
||
1439 | // fall-through |
||
1440 | case $c >= 0x00200000: |
||
1441 | $v .= chr(0x80 | ($c & 0x3F)); |
||
1442 | $c = ($c >> 6) | 0x00200000; |
||
1443 | // fall-through |
||
1444 | case $c >= 0x00010000: |
||
1445 | $v .= chr(0x80 | ($c & 0x3F)); |
||
1446 | $c = ($c >> 6) | 0x00010000; |
||
1447 | // fall-through |
||
1448 | case $c >= 0x00000800: |
||
1449 | $v .= chr(0x80 | ($c & 0x3F)); |
||
1450 | $c = ($c >> 6) | 0x00000800; |
||
1451 | // fall-through |
||
1452 | case $c >= 0x00000080: |
||
1453 | $v .= chr(0x80 | ($c & 0x3F)); |
||
1454 | $c = ($c >> 6) | 0x000000C0; |
||
1455 | // fall-through |
||
1456 | default: |
||
1457 | $v .= chr($c); |
||
1458 | break; |
||
1459 | } |
||
1460 | $out .= strrev($v); |
||
1461 | } |
||
1462 | return $out; |
||
1463 | } |
||
1464 | |||
1465 | /** |
||
1466 | * Extract raw BER from Base64 encoding |
||
1467 | * |
||
874 | daniel-mar | 1468 | * @access private |
827 | daniel-mar | 1469 | * @param string $str |
1470 | * @return string |
||
1471 | */ |
||
1472 | public static function extractBER($str) |
||
1473 | { |
||
1474 | /* X.509 certs are assumed to be base64 encoded but sometimes they'll have additional things in them |
||
1475 | * above and beyond the ceritificate. |
||
1476 | * ie. some may have the following preceding the -----BEGIN CERTIFICATE----- line: |
||
1477 | * |
||
1478 | * Bag Attributes |
||
1479 | * localKeyID: 01 00 00 00 |
||
1480 | * subject=/O=organization/OU=org unit/CN=common name |
||
1481 | * issuer=/O=organization/CN=common name |
||
1482 | */ |
||
1483 | if (strlen($str) > ini_get('pcre.backtrack_limit')) { |
||
1484 | $temp = $str; |
||
1485 | } else { |
||
1486 | $temp = preg_replace('#.*?^-+[^-]+-+[\r\n ]*$#ms', '', $str, 1); |
||
1487 | $temp = preg_replace('#-+END.*[\r\n ]*.*#ms', '', $temp, 1); |
||
1488 | } |
||
1489 | // remove new lines |
||
1490 | $temp = str_replace(["\r", "\n", ' '], '', $temp); |
||
1491 | // remove the -----BEGIN CERTIFICATE----- and -----END CERTIFICATE----- stuff |
||
1492 | $temp = preg_replace('#^-+[^-]+-+|-+[^-]+-+$#', '', $temp); |
||
1493 | $temp = preg_match('#^[a-zA-Z\d/+]*={0,2}$#', $temp) ? Base64::decode($temp) : false; |
||
1494 | return $temp != false ? $temp : $str; |
||
1495 | } |
||
1496 | |||
1497 | /** |
||
1498 | * DER-encode the length |
||
1499 | * |
||
1500 | * DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4. See |
||
1501 | * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information. |
||
1502 | * |
||
874 | daniel-mar | 1503 | * @access public |
827 | daniel-mar | 1504 | * @param int $length |
1505 | * @return string |
||
1506 | */ |
||
1507 | public static function encodeLength($length) |
||
1508 | { |
||
1509 | if ($length <= 0x7F) { |
||
1510 | return chr($length); |
||
1511 | } |
||
1512 | |||
1513 | $temp = ltrim(pack('N', $length), chr(0)); |
||
1514 | return pack('Ca*', 0x80 | strlen($temp), $temp); |
||
1515 | } |
||
1516 | |||
1517 | /** |
||
1518 | * Returns the OID corresponding to a name |
||
1519 | * |
||
1520 | * What's returned in the associative array returned by loadX509() (or load*()) is either a name or an OID if |
||
1521 | * no OID to name mapping is available. The problem with this is that what may be an unmapped OID in one version |
||
1522 | * of phpseclib may not be unmapped in the next version, so apps that are looking at this OID may not be able |
||
1523 | * to work from version to version. |
||
1524 | * |
||
1525 | * This method will return the OID if a name is passed to it and if no mapping is avialable it'll assume that |
||
1526 | * what's being passed to it already is an OID and return that instead. A few examples. |
||
1527 | * |
||
1528 | * getOID('2.16.840.1.101.3.4.2.1') == '2.16.840.1.101.3.4.2.1' |
||
1529 | * getOID('id-sha256') == '2.16.840.1.101.3.4.2.1' |
||
1530 | * getOID('zzz') == 'zzz' |
||
1531 | * |
||
874 | daniel-mar | 1532 | * @access public |
827 | daniel-mar | 1533 | * @param string $name |
1534 | * @return string |
||
1535 | */ |
||
1536 | public static function getOID($name) |
||
1537 | { |
||
1538 | return isset(self::$reverseOIDs[$name]) ? self::$reverseOIDs[$name] : $name; |
||
1539 | } |
||
1540 | } |