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