4 * Pure-PHP ASN.1 Parser
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
12 * File_ASN1 decodes and encodes DER formatted messages and places them in a semantic context.
14 * Uses the 1988 ASN.1 syntax.
16 * LICENSE: Permission is hereby granted, free of charge, to any person obtaining a copy
17 * of this software and associated documentation files (the "Software"), to deal
18 * in the Software without restriction, including without limitation the rights
19 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
20 * copies of the Software, and to permit persons to whom the Software is
21 * furnished to do so, subject to the following conditions:
23 * The above copyright notice and this permission notice shall be included in
24 * all copies or substantial portions of the Software.
26 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
27 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
28 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
29 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
30 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
31 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
36 * @author Jim Wigginton <terrafrost@php.net>
37 * @copyright 2012 Jim Wigginton
38 * @license http://www.opensource.org/licenses/mit-license.html MIT License
39 * @link http://phpseclib.sourceforge.net
46 * @link http://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#page=12
48 define('FILE_ASN1_CLASS_UNIVERSAL', 0);
49 define('FILE_ASN1_CLASS_APPLICATION', 1);
50 define('FILE_ASN1_CLASS_CONTEXT_SPECIFIC', 2);
51 define('FILE_ASN1_CLASS_PRIVATE', 3);
58 * @link http://www.obj-sys.com/asn1tutorial/node124.html
60 define('FILE_ASN1_TYPE_BOOLEAN', 1);
61 define('FILE_ASN1_TYPE_INTEGER', 2);
62 define('FILE_ASN1_TYPE_BIT_STRING', 3);
63 define('FILE_ASN1_TYPE_OCTET_STRING', 4);
64 define('FILE_ASN1_TYPE_NULL', 5);
65 define('FILE_ASN1_TYPE_OBJECT_IDENTIFIER', 6);
66 //define('FILE_ASN1_TYPE_OBJECT_DESCRIPTOR', 7);
67 //define('FILE_ASN1_TYPE_INSTANCE_OF', 8); // EXTERNAL
68 define('FILE_ASN1_TYPE_REAL', 9);
69 define('FILE_ASN1_TYPE_ENUMERATED', 10);
70 //define('FILE_ASN1_TYPE_EMBEDDED', 11);
71 define('FILE_ASN1_TYPE_UTF8_STRING', 12);
72 //define('FILE_ASN1_TYPE_RELATIVE_OID', 13);
73 define('FILE_ASN1_TYPE_SEQUENCE', 16); // SEQUENCE OF
74 define('FILE_ASN1_TYPE_SET', 17); // SET OF
80 * @link http://www.obj-sys.com/asn1tutorial/node10.html
82 define('FILE_ASN1_TYPE_NUMERIC_STRING', 18);
83 define('FILE_ASN1_TYPE_PRINTABLE_STRING', 19);
84 define('FILE_ASN1_TYPE_TELETEX_STRING', 20); // T61String
85 define('FILE_ASN1_TYPE_VIDEOTEX_STRING', 21);
86 define('FILE_ASN1_TYPE_IA5_STRING', 22);
87 define('FILE_ASN1_TYPE_UTC_TIME', 23);
88 define('FILE_ASN1_TYPE_GENERALIZED_TIME', 24);
89 define('FILE_ASN1_TYPE_GRAPHIC_STRING', 25);
90 define('FILE_ASN1_TYPE_VISIBLE_STRING', 26); // ISO646String
91 define('FILE_ASN1_TYPE_GENERAL_STRING', 27);
92 define('FILE_ASN1_TYPE_UNIVERSAL_STRING', 28);
93 //define('FILE_ASN1_TYPE_CHARACTER_STRING', 29);
94 define('FILE_ASN1_TYPE_BMP_STRING', 30);
100 * These tags are kinda place holders for other tags.
104 define('FILE_ASN1_TYPE_CHOICE', -1);
105 define('FILE_ASN1_TYPE_ANY', -2);
111 * Bypass normal encoding rules in File_ASN1::encodeDER()
114 * @author Jim Wigginton <terrafrost@php.net>
117 class File_ASN1_Element
130 * @param String $encoded
131 * @return File_ASN1_Element
134 function File_ASN1_Element($encoded)
136 $this->element = $encoded;
141 * Pure-PHP ASN.1 Parser
144 * @author Jim Wigginton <terrafrost@php.net>
150 * ASN.1 object identifier
154 * @link http://en.wikipedia.org/wiki/Object_identifier
159 * Default date format
163 * @link http://php.net/class.datetime
165 var $format = 'D, d M Y H:i:s O';
168 * Default date format
172 * @see File_ASN1::setTimeFormat()
173 * @see File_ASN1::asn1map()
174 * @link http://php.net/class.datetime
181 * If the mapping type is FILE_ASN1_TYPE_ANY what do we actually encode it as?
185 * @see File_ASN1::_encode_der()
190 * Type mapping table for the ANY type.
192 * Structured or unknown types are mapped to a FILE_ASN1_Element.
193 * Unambiguous types get the direct mapping (int/real/bool).
194 * Others are mapped as a choice, with an extra indexing level.
200 FILE_ASN1_TYPE_BOOLEAN => true,
201 FILE_ASN1_TYPE_INTEGER => true,
202 FILE_ASN1_TYPE_BIT_STRING => 'bitString',
203 FILE_ASN1_TYPE_OCTET_STRING => 'octetString',
204 FILE_ASN1_TYPE_NULL => 'null',
205 FILE_ASN1_TYPE_OBJECT_IDENTIFIER => 'objectIdentifier',
206 FILE_ASN1_TYPE_REAL => true,
207 FILE_ASN1_TYPE_ENUMERATED => 'enumerated',
208 FILE_ASN1_TYPE_UTF8_STRING => 'utf8String',
209 FILE_ASN1_TYPE_NUMERIC_STRING => 'numericString',
210 FILE_ASN1_TYPE_PRINTABLE_STRING => 'printableString',
211 FILE_ASN1_TYPE_TELETEX_STRING => 'teletexString',
212 FILE_ASN1_TYPE_VIDEOTEX_STRING => 'videotexString',
213 FILE_ASN1_TYPE_IA5_STRING => 'ia5String',
214 FILE_ASN1_TYPE_UTC_TIME => 'utcTime',
215 FILE_ASN1_TYPE_GENERALIZED_TIME => 'generalTime',
216 FILE_ASN1_TYPE_GRAPHIC_STRING => 'graphicString',
217 FILE_ASN1_TYPE_VISIBLE_STRING => 'visibleString',
218 FILE_ASN1_TYPE_GENERAL_STRING => 'generalString',
219 FILE_ASN1_TYPE_UNIVERSAL_STRING => 'universalString',
220 //FILE_ASN1_TYPE_CHARACTER_STRING => 'characterString',
221 FILE_ASN1_TYPE_BMP_STRING => 'bmpString'
225 * String type to character size mapping table.
227 * Non-convertable types are absent from this table.
228 * size == 0 indicates variable length encoding.
233 var $stringTypeSize = array(
234 FILE_ASN1_TYPE_UTF8_STRING => 0,
235 FILE_ASN1_TYPE_BMP_STRING => 2,
236 FILE_ASN1_TYPE_UNIVERSAL_STRING => 4,
237 FILE_ASN1_TYPE_PRINTABLE_STRING => 1,
238 FILE_ASN1_TYPE_TELETEX_STRING => 1,
239 FILE_ASN1_TYPE_IA5_STRING => 1,
240 FILE_ASN1_TYPE_VISIBLE_STRING => 1,
244 * Default Constructor.
250 static $static_init = null;
253 if (!class_exists('Math_BigInteger')) {
254 include_once 'Math/BigInteger.php';
262 * Serves a similar purpose to openssl's asn1parse
264 * @param String $encoded
268 function decodeBER($encoded)
270 if (is_object($encoded) && strtolower(get_class($encoded)) == 'file_asn1_element') {
271 $encoded = $encoded->element;
274 $this->encoded = $encoded;
275 // encapsulate in an array for BC with the old decodeBER
276 return array($this->_decode_ber($encoded));
280 * Parse BER-encoding (Helper function)
282 * Sometimes we want to get the BER encoding of a particular tag. $start lets us do that without having to reencode.
283 * $encoded is passed by reference for the recursive calls done for FILE_ASN1_TYPE_BIT_STRING and
284 * FILE_ASN1_TYPE_OCTET_STRING. In those cases, the indefinite length is used.
286 * @param String $encoded
287 * @param Integer $start
291 function _decode_ber($encoded, $start = 0)
293 $current = array('start' => $start);
295 $type = ord($this->_string_shift($encoded));
298 $constructed = ($type >> 5) & 1;
303 // process septets (since the eighth bit is ignored, it's not an octet)
305 $loop = ord($encoded[0]) >> 7;
307 $tag |= ord($this->_string_shift($encoded)) & 0x7F;
312 // Length, as discussed in paragraph 8.1.3 of X.690-0207.pdf#page=13
313 $length = ord($this->_string_shift($encoded));
315 if ( $length == 0x80 ) { // indefinite length
316 // "[A sender shall] use the indefinite form (see 8.1.3.6) if the encoding is constructed and is not all
317 // immediately available." -- paragraph 8.1.3.2.c
318 $length = strlen($encoded);
319 } elseif ( $length & 0x80 ) { // definite length, long form
320 // technically, the long form of the length can be represented by up to 126 octets (bytes), but we'll only
321 // support it up to four.
323 $temp = $this->_string_shift($encoded, $length);
324 // tags of indefinte length don't really have a header length; this length includes the tag
325 $current+= array('headerlength' => $length + 2);
327 extract(unpack('Nlength', substr(str_pad($temp, 4, chr(0), STR_PAD_LEFT), -4)));
329 $current+= array('headerlength' => 2);
332 $content = $this->_string_shift($encoded, $length);
334 // at this point $length can be overwritten. it's only accurate for definite length things as is
336 /* Class is UNIVERSAL, APPLICATION, PRIVATE, or CONTEXT-SPECIFIC. The UNIVERSAL class is restricted to the ASN.1
337 built-in types. It defines an application-independent data type that must be distinguishable from all other
338 data types. The other three classes are user defined. The APPLICATION class distinguishes data types that
339 have a wide, scattered use within a particular presentation context. PRIVATE distinguishes data types within
340 a particular organization or country. CONTEXT-SPECIFIC distinguishes members of a sequence or set, the
341 alternatives of a CHOICE, or universally tagged set members. Only the class number appears in braces for this
342 data type; the term CONTEXT-SPECIFIC does not appear.
344 -- http://www.obj-sys.com/asn1tutorial/node12.html */
345 $class = ($type >> 6) & 3;
347 case FILE_ASN1_CLASS_APPLICATION:
348 case FILE_ASN1_CLASS_PRIVATE:
349 case FILE_ASN1_CLASS_CONTEXT_SPECIFIC:
354 'content' => $content,
355 'length' => $length + $start - $current['start']
359 $newcontent = array();
360 if (strlen($content)) {
361 $newcontent = $this->_decode_ber($content, $start);
362 $length = $newcontent['length'];
363 if (substr($content, $length, 2) == "\0\0") {
367 $newcontent = array($newcontent);
373 // the array encapsulation is for BC with the old format
374 'content' => $newcontent,
375 // the only time when $content['headerlength'] isn't defined is when the length is indefinite.
376 // the absence of $content['headerlength'] is how we know if something is indefinite or not.
377 // technically, it could be defined to be 2 and then another indicator could be used but whatever.
378 'length' => $start - $current['start']
382 $current+= array('type' => $tag);
384 // decode UNIVERSAL tags
386 case FILE_ASN1_TYPE_BOOLEAN:
387 // "The contents octets shall consist of a single octet." -- paragraph 8.2.1
388 //if (strlen($content) != 1) {
391 $current['content'] = (bool) ord($content[0]);
393 case FILE_ASN1_TYPE_INTEGER:
394 case FILE_ASN1_TYPE_ENUMERATED:
395 $current['content'] = new Math_BigInteger($content, -256);
397 case FILE_ASN1_TYPE_REAL: // not currently supported
399 case FILE_ASN1_TYPE_BIT_STRING:
400 // The initial octet shall encode, as an unsigned binary integer with bit 1 as the least significant bit,
401 // the number of unused bits in the final subsequent octet. The number shall be in the range zero to
404 $current['content'] = $content;
406 $temp = $this->_decode_ber($content, $start);
407 $length-= strlen($content);
408 $last = count($temp) - 1;
409 for ($i = 0; $i < $last; $i++) {
410 // all subtags should be bit strings
411 //if ($temp[$i]['type'] != FILE_ASN1_TYPE_BIT_STRING) {
414 $current['content'].= substr($temp[$i]['content'], 1);
416 // all subtags should be bit strings
417 //if ($temp[$last]['type'] != FILE_ASN1_TYPE_BIT_STRING) {
420 $current['content'] = $temp[$last]['content'][0] . $current['content'] . substr($temp[$i]['content'], 1);
423 case FILE_ASN1_TYPE_OCTET_STRING:
425 $current['content'] = $content;
427 $current['content'] = '';
429 while (substr($content, 0, 2) != "\0\0") {
430 $temp = $this->_decode_ber($content, $length + $start);
431 $this->_string_shift($content, $temp['length']);
432 // all subtags should be octet strings
433 //if ($temp['type'] != FILE_ASN1_TYPE_OCTET_STRING) {
436 $current['content'].= $temp['content'];
437 $length+= $temp['length'];
439 if (substr($content, 0, 2) == "\0\0") {
440 $length+= 2; // +2 for the EOC
444 case FILE_ASN1_TYPE_NULL:
445 // "The contents octets shall not contain any octets." -- paragraph 8.8.2
446 //if (strlen($content)) {
450 case FILE_ASN1_TYPE_SEQUENCE:
451 case FILE_ASN1_TYPE_SET:
453 $current['content'] = array();
454 while (strlen($content)) {
455 // if indefinite length construction was used and we have an end-of-content string next
456 // 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
457 if (!isset($current['headerlength']) && substr($content, 0, 2) == "\0\0") {
458 $length = $offset + 2; // +2 for the EOC
461 $temp = $this->_decode_ber($content, $start + $offset);
462 $this->_string_shift($content, $temp['length']);
463 $current['content'][] = $temp;
464 $offset+= $temp['length'];
467 case FILE_ASN1_TYPE_OBJECT_IDENTIFIER:
468 $temp = ord($this->_string_shift($content));
469 $current['content'] = sprintf('%d.%d', floor($temp / 40), $temp % 40);
472 while (strlen($content)) {
473 $temp = ord($this->_string_shift($content));
475 $valuen |= $temp & 0x7F;
477 $current['content'].= ".$valuen";
481 // the eighth bit of the last byte should not be 1
486 /* Each character string type shall be encoded as if it had been declared:
487 [UNIVERSAL x] IMPLICIT OCTET STRING
489 -- X.690-0207.pdf#page=23 (paragraph 8.21.3)
491 Per that, we're not going to do any validation. If there are any illegal characters in the string,
492 we don't really care */
493 case FILE_ASN1_TYPE_NUMERIC_STRING:
494 // 0,1,2,3,4,5,6,7,8,9, and space
495 case FILE_ASN1_TYPE_PRINTABLE_STRING:
496 // Upper and lower case letters, digits, space, apostrophe, left/right parenthesis, plus sign, comma,
497 // hyphen, full stop, solidus, colon, equal sign, question mark
498 case FILE_ASN1_TYPE_TELETEX_STRING:
499 // The Teletex character set in CCITT's T61, space, and delete
500 // see http://en.wikipedia.org/wiki/Teletex#Character_sets
501 case FILE_ASN1_TYPE_VIDEOTEX_STRING:
502 // The Videotex character set in CCITT's T.100 and T.101, space, and delete
503 case FILE_ASN1_TYPE_VISIBLE_STRING:
504 // Printing character sets of international ASCII, and space
505 case FILE_ASN1_TYPE_IA5_STRING:
506 // International Alphabet 5 (International ASCII)
507 case FILE_ASN1_TYPE_GRAPHIC_STRING:
508 // All registered G sets, and space
509 case FILE_ASN1_TYPE_GENERAL_STRING:
510 // All registered C and G sets, space and delete
511 case FILE_ASN1_TYPE_UTF8_STRING:
513 case FILE_ASN1_TYPE_BMP_STRING:
514 $current['content'] = $content;
516 case FILE_ASN1_TYPE_UTC_TIME:
517 case FILE_ASN1_TYPE_GENERALIZED_TIME:
518 $current['content'] = $this->_decodeTime($content, $tag);
524 // ie. length is the length of the full TLV encoding - it's not just the length of the value
525 return $current + array('length' => $start - $current['start']);
531 * Provides an ASN.1 semantic mapping ($mapping) from a parsed BER-encoding to a human readable format.
533 * "Special" mappings may be applied on a per tag-name basis via $special.
535 * @param Array $decoded
536 * @param Array $mapping
537 * @param Array $special
541 function asn1map($decoded, $mapping, $special = array())
543 if (isset($mapping['explicit']) && is_array($decoded['content'])) {
544 $decoded = $decoded['content'][0];
548 case $mapping['type'] == FILE_ASN1_TYPE_ANY:
549 $intype = $decoded['type'];
550 if (isset($decoded['constant']) || !isset($this->ANYmap[$intype]) || ($this->encoded[$decoded['start']] & 0x20)) {
551 return new File_ASN1_Element(substr($this->encoded, $decoded['start'], $decoded['length']));
553 $inmap = $this->ANYmap[$intype];
554 if (is_string($inmap)) {
555 return array($inmap => $this->asn1map($decoded, array('type' => $intype) + $mapping, $special));
558 case $mapping['type'] == FILE_ASN1_TYPE_CHOICE:
559 foreach ($mapping['children'] as $key => $option) {
561 case isset($option['constant']) && $option['constant'] == $decoded['constant']:
562 case !isset($option['constant']) && $option['type'] == $decoded['type']:
563 $value = $this->asn1map($decoded, $option, $special);
565 case !isset($option['constant']) && $option['type'] == FILE_ASN1_TYPE_CHOICE:
566 $v = $this->asn1map($decoded, $option, $special);
572 if (isset($special[$key])) {
573 $value = call_user_func($special[$key], $value);
575 return array($key => $value);
579 case isset($mapping['implicit']):
580 case isset($mapping['explicit']):
581 case $decoded['type'] == $mapping['type']:
584 // if $decoded['type'] and $mapping['type'] are both strings, but different types of strings,
587 case $decoded['type'] < 18: // FILE_ASN1_TYPE_NUMERIC_STRING == 18
588 case $decoded['type'] > 30: // FILE_ASN1_TYPE_BMP_STRING == 30
589 case $mapping['type'] < 18:
590 case $mapping['type'] > 30:
595 if (isset($mapping['implicit'])) {
596 $decoded['type'] = $mapping['type'];
599 switch ($decoded['type']) {
600 case FILE_ASN1_TYPE_SEQUENCE:
603 // ignore the min and max
604 if (isset($mapping['min']) && isset($mapping['max'])) {
605 $child = $mapping['children'];
606 foreach ($decoded['content'] as $content) {
607 if (($map[] = $this->asn1map($content, $child, $special)) === null) {
615 $n = count($decoded['content']);
618 foreach ($mapping['children'] as $key => $child) {
619 $maymatch = $i < $n; // Match only existing input.
621 $temp = $decoded['content'][$i];
623 if ($child['type'] != FILE_ASN1_TYPE_CHOICE) {
624 // Get the mapping and input class & constant.
625 $childClass = $tempClass = FILE_ASN1_CLASS_UNIVERSAL;
627 if (isset($temp['constant'])) {
628 $tempClass = isset($temp['class']) ? $temp['class'] : FILE_ASN1_CLASS_CONTEXT_SPECIFIC;
630 if (isset($child['class'])) {
631 $childClass = $child['class'];
632 $constant = $child['cast'];
633 } elseif (isset($child['constant'])) {
634 $childClass = FILE_ASN1_CLASS_CONTEXT_SPECIFIC;
635 $constant = $child['constant'];
638 if (isset($constant) && isset($temp['constant'])) {
639 // Can only match if constants and class match.
640 $maymatch = $constant == $temp['constant'] && $childClass == $tempClass;
642 // Can only match if no constant expected and type matches or is generic.
643 $maymatch = !isset($child['constant']) && array_search($child['type'], array($temp['type'], FILE_ASN1_TYPE_ANY, FILE_ASN1_TYPE_CHOICE)) !== false;
649 // Attempt submapping.
650 $candidate = $this->asn1map($temp, $child, $special);
651 $maymatch = $candidate !== null;
655 // Got the match: use it.
656 if (isset($special[$key])) {
657 $candidate = call_user_func($special[$key], $candidate);
659 $map[$key] = $candidate;
661 } elseif (isset($child['default'])) {
662 $map[$key] = $child['default']; // Use default.
663 } elseif (!isset($child['optional'])) {
664 return null; // Syntax error.
668 // Fail mapping if all input items have not been consumed.
669 return $i < $n? null: $map;
671 // the main diff between sets and sequences is the encapsulation of the foreach in another for loop
672 case FILE_ASN1_TYPE_SET:
675 // ignore the min and max
676 if (isset($mapping['min']) && isset($mapping['max'])) {
677 $child = $mapping['children'];
678 foreach ($decoded['content'] as $content) {
679 if (($map[] = $this->asn1map($content, $child, $special)) === null) {
687 for ($i = 0; $i < count($decoded['content']); $i++) {
688 $temp = $decoded['content'][$i];
689 $tempClass = FILE_ASN1_CLASS_UNIVERSAL;
690 if (isset($temp['constant'])) {
691 $tempClass = isset($temp['class']) ? $temp['class'] : FILE_ASN1_CLASS_CONTEXT_SPECIFIC;
694 foreach ($mapping['children'] as $key => $child) {
695 if (isset($map[$key])) {
699 if ($child['type'] != FILE_ASN1_TYPE_CHOICE) {
700 $childClass = FILE_ASN1_CLASS_UNIVERSAL;
702 if (isset($child['class'])) {
703 $childClass = $child['class'];
704 $constant = $child['cast'];
705 } elseif (isset($child['constant'])) {
706 $childClass = FILE_ASN1_CLASS_CONTEXT_SPECIFIC;
707 $constant = $child['constant'];
710 if (isset($constant) && isset($temp['constant'])) {
711 // Can only match if constants and class match.
712 $maymatch = $constant == $temp['constant'] && $childClass == $tempClass;
714 // Can only match if no constant expected and type matches or is generic.
715 $maymatch = !isset($child['constant']) && array_search($child['type'], array($temp['type'], FILE_ASN1_TYPE_ANY, FILE_ASN1_TYPE_CHOICE)) !== false;
720 // Attempt submapping.
721 $candidate = $this->asn1map($temp, $child, $special);
722 $maymatch = $candidate !== null;
729 // Got the match: use it.
730 if (isset($special[$key])) {
731 $candidate = call_user_func($special[$key], $candidate);
733 $map[$key] = $candidate;
738 foreach ($mapping['children'] as $key => $child) {
739 if (!isset($map[$key])) {
740 if (isset($child['default'])) {
741 $map[$key] = $child['default'];
742 } elseif (!isset($child['optional'])) {
748 case FILE_ASN1_TYPE_OBJECT_IDENTIFIER:
749 return isset($this->oids[$decoded['content']]) ? $this->oids[$decoded['content']] : $decoded['content'];
750 case FILE_ASN1_TYPE_UTC_TIME:
751 case FILE_ASN1_TYPE_GENERALIZED_TIME:
752 if (isset($mapping['implicit'])) {
753 $decoded['content'] = $this->_decodeTime($decoded['content'], $decoded['type']);
755 return @date($this->format, $decoded['content']);
756 case FILE_ASN1_TYPE_BIT_STRING:
757 if (isset($mapping['mapping'])) {
758 $offset = ord($decoded['content'][0]);
759 $size = (strlen($decoded['content']) - 1) * 8 - $offset;
761 From X.680-0207.pdf#page=46 (21.7):
763 "When a "NamedBitList" is used in defining a bitstring type ASN.1 encoding rules are free to add (or remove)
764 arbitrarily any trailing 0 bits to (or from) values that are being encoded or decoded. Application designers should
765 therefore ensure that different semantics are not associated with such values which differ only in the number of trailing
768 $bits = count($mapping['mapping']) == $size ? array() : array_fill(0, count($mapping['mapping']) - $size, false);
769 for ($i = strlen($decoded['content']) - 1; $i > 0; $i--) {
770 $current = ord($decoded['content'][$i]);
771 for ($j = $offset; $j < 8; $j++) {
772 $bits[] = (bool) ($current & (1 << $j));
777 $map = array_reverse($mapping['mapping']);
778 foreach ($map as $i => $value) {
785 case FILE_ASN1_TYPE_OCTET_STRING:
786 return base64_encode($decoded['content']);
787 case FILE_ASN1_TYPE_NULL:
789 case FILE_ASN1_TYPE_BOOLEAN:
790 return $decoded['content'];
791 case FILE_ASN1_TYPE_NUMERIC_STRING:
792 case FILE_ASN1_TYPE_PRINTABLE_STRING:
793 case FILE_ASN1_TYPE_TELETEX_STRING:
794 case FILE_ASN1_TYPE_VIDEOTEX_STRING:
795 case FILE_ASN1_TYPE_IA5_STRING:
796 case FILE_ASN1_TYPE_GRAPHIC_STRING:
797 case FILE_ASN1_TYPE_VISIBLE_STRING:
798 case FILE_ASN1_TYPE_GENERAL_STRING:
799 case FILE_ASN1_TYPE_UNIVERSAL_STRING:
800 case FILE_ASN1_TYPE_UTF8_STRING:
801 case FILE_ASN1_TYPE_BMP_STRING:
802 return $decoded['content'];
803 case FILE_ASN1_TYPE_INTEGER:
804 case FILE_ASN1_TYPE_ENUMERATED:
805 $temp = $decoded['content'];
806 if (isset($mapping['implicit'])) {
807 $temp = new Math_BigInteger($decoded['content'], -256);
809 if (isset($mapping['mapping'])) {
810 $temp = (int) $temp->toString();
811 return isset($mapping['mapping'][$temp]) ?
812 $mapping['mapping'][$temp] :
822 * DER-encodes an ASN.1 semantic mapping ($mapping). Some libraries would probably call this function
825 * "Special" mappings can be applied via $special.
827 * @param String $source
828 * @param String $mapping
829 * @param Integer $idx
833 function encodeDER($source, $mapping, $special = array())
835 $this->location = array();
836 return $this->_encode_der($source, $mapping, null, $special);
840 * ASN.1 Encode (Helper function)
842 * @param String $source
843 * @param String $mapping
844 * @param Integer $idx
848 function _encode_der($source, $mapping, $idx = null, $special = array())
850 if (is_object($source) && strtolower(get_class($source)) == 'file_asn1_element') {
851 return $source->element;
854 // do not encode (implicitly optional) fields with value set to default
855 if (isset($mapping['default']) && $source === $mapping['default']) {
860 if (isset($special[$idx])) {
861 $source = call_user_func($special[$idx], $source);
863 $this->location[] = $idx;
866 $tag = $mapping['type'];
869 case FILE_ASN1_TYPE_SET: // Children order is not important, thus process in sequence.
870 case FILE_ASN1_TYPE_SEQUENCE:
871 $tag|= 0x20; // set the constructed bit
874 // ignore the min and max
875 if (isset($mapping['min']) && isset($mapping['max'])) {
876 $child = $mapping['children'];
878 foreach ($source as $content) {
879 $temp = $this->_encode_der($content, $child, null, $special);
880 if ($temp === false) {
888 foreach ($mapping['children'] as $key => $child) {
889 if (!isset($source[$key])) {
890 if (!isset($child['optional'])) {
896 $temp = $this->_encode_der($source[$key], $child, $key, $special);
897 if ($temp === false) {
901 // An empty child encoding means it has been optimized out.
902 // Else we should have at least one tag byte.
907 // if isset($child['constant']) is true then isset($child['optional']) should be true as well
908 if (isset($child['constant'])) {
910 From X.680-0207.pdf#page=58 (30.6):
912 "The tagging construction specifies explicit tagging if any of the following holds:
914 c) the "Tag Type" alternative is used and the value of "TagDefault" for the module is IMPLICIT TAGS or
915 AUTOMATIC TAGS, but the type defined by "Type" is an untagged choice type, an untagged open type, or
916 an untagged "DummyReference" (see ITU-T Rec. X.683 | ISO/IEC 8824-4, 8.3)."
918 if (isset($child['explicit']) || $child['type'] == FILE_ASN1_TYPE_CHOICE) {
919 $subtag = chr((FILE_ASN1_CLASS_CONTEXT_SPECIFIC << 6) | 0x20 | $child['constant']);
920 $temp = $subtag . $this->_encodeLength(strlen($temp)) . $temp;
922 $subtag = chr((FILE_ASN1_CLASS_CONTEXT_SPECIFIC << 6) | (ord($temp[0]) & 0x20) | $child['constant']);
923 $temp = $subtag . substr($temp, 1);
929 case FILE_ASN1_TYPE_CHOICE:
932 foreach ($mapping['children'] as $key => $child) {
933 if (!isset($source[$key])) {
937 $temp = $this->_encode_der($source[$key], $child, $key, $special);
938 if ($temp === false) {
942 // An empty child encoding means it has been optimized out.
943 // Else we should have at least one tag byte.
948 $tag = ord($temp[0]);
950 // if isset($child['constant']) is true then isset($child['optional']) should be true as well
951 if (isset($child['constant'])) {
952 if (isset($child['explicit']) || $child['type'] == FILE_ASN1_TYPE_CHOICE) {
953 $subtag = chr((FILE_ASN1_CLASS_CONTEXT_SPECIFIC << 6) | 0x20 | $child['constant']);
954 $temp = $subtag . $this->_encodeLength(strlen($temp)) . $temp;
956 $subtag = chr((FILE_ASN1_CLASS_CONTEXT_SPECIFIC << 6) | (ord($temp[0]) & 0x20) | $child['constant']);
957 $temp = $subtag . substr($temp, 1);
963 array_pop($this->location);
966 if ($temp && isset($mapping['cast'])) {
967 $temp[0] = chr(($mapping['class'] << 6) | ($tag & 0x20) | $mapping['cast']);
971 case FILE_ASN1_TYPE_INTEGER:
972 case FILE_ASN1_TYPE_ENUMERATED:
973 if (!isset($mapping['mapping'])) {
974 if (is_numeric($source)) {
975 $source = new Math_BigInteger($source);
977 $value = $source->toBytes(true);
979 $value = array_search($source, $mapping['mapping']);
980 if ($value === false) {
983 $value = new Math_BigInteger($value);
984 $value = $value->toBytes(true);
986 if (!strlen($value)) {
990 case FILE_ASN1_TYPE_UTC_TIME:
991 case FILE_ASN1_TYPE_GENERALIZED_TIME:
992 $format = $mapping['type'] == FILE_ASN1_TYPE_UTC_TIME ? 'y' : 'Y';
994 $value = @gmdate($format, strtotime($source)) . 'Z';
996 case FILE_ASN1_TYPE_BIT_STRING:
997 if (isset($mapping['mapping'])) {
998 $bits = array_fill(0, count($mapping['mapping']), 0);
1000 for ($i = 0; $i < count($mapping['mapping']); $i++) {
1001 if (in_array($mapping['mapping'][$i], $source)) {
1007 if (isset($mapping['min']) && $mapping['min'] >= 1 && $size < $mapping['min']) {
1008 $size = $mapping['min'] - 1;
1011 $offset = 8 - (($size + 1) & 7);
1012 $offset = $offset !== 8 ? $offset : 0;
1014 $value = chr($offset);
1016 for ($i = $size + 1; $i < count($mapping['mapping']); $i++) {
1020 $bits = implode('', array_pad($bits, $size + $offset + 1, 0));
1021 $bytes = explode(' ', rtrim(chunk_split($bits, 8, ' ')));
1022 foreach ($bytes as $byte) {
1023 $value.= chr(bindec($byte));
1028 case FILE_ASN1_TYPE_OCTET_STRING:
1029 /* The initial octet shall encode, as an unsigned binary integer with bit 1 as the least significant bit,
1030 the number of unused bits in the final subsequent octet. The number shall be in the range zero to seven.
1032 -- http://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#page=16 */
1033 $value = base64_decode($source);
1035 case FILE_ASN1_TYPE_OBJECT_IDENTIFIER:
1036 $oid = preg_match('#(?:\d+\.)+#', $source) ? $source : array_search($source, $this->oids);
1037 if ($oid === false) {
1038 user_error('Invalid OID');
1042 $parts = explode('.', $oid);
1043 $value = chr(40 * $parts[0] + $parts[1]);
1044 for ($i = 2; $i < count($parts); $i++) {
1049 while ($parts[$i]) {
1050 $temp = chr(0x80 | ($parts[$i] & 0x7F)) . $temp;
1053 $temp[strlen($temp) - 1] = $temp[strlen($temp) - 1] & chr(0x7F);
1058 case FILE_ASN1_TYPE_ANY:
1059 $loc = $this->location;
1061 array_pop($this->location);
1065 case !isset($source):
1066 return $this->_encode_der(null, array('type' => FILE_ASN1_TYPE_NULL) + $mapping, null, $special);
1067 case is_int($source):
1068 case is_object($source) && strtolower(get_class($source)) == 'math_biginteger':
1069 return $this->_encode_der($source, array('type' => FILE_ASN1_TYPE_INTEGER) + $mapping, null, $special);
1070 case is_float($source):
1071 return $this->_encode_der($source, array('type' => FILE_ASN1_TYPE_REAL) + $mapping, null, $special);
1072 case is_bool($source):
1073 return $this->_encode_der($source, array('type' => FILE_ASN1_TYPE_BOOLEAN) + $mapping, null, $special);
1074 case is_array($source) && count($source) == 1:
1075 $typename = implode('', array_keys($source));
1076 $outtype = array_search($typename, $this->ANYmap, true);
1077 if ($outtype !== false) {
1078 return $this->_encode_der($source[$typename], array('type' => $outtype) + $mapping, null, $special);
1082 $filters = $this->filters;
1083 foreach ($loc as $part) {
1084 if (!isset($filters[$part])) {
1088 $filters = $filters[$part];
1090 if ($filters === false) {
1091 user_error('No filters defined for ' . implode('/', $loc));
1094 return $this->_encode_der($source, $filters + $mapping, null, $special);
1095 case FILE_ASN1_TYPE_NULL:
1098 case FILE_ASN1_TYPE_NUMERIC_STRING:
1099 case FILE_ASN1_TYPE_TELETEX_STRING:
1100 case FILE_ASN1_TYPE_PRINTABLE_STRING:
1101 case FILE_ASN1_TYPE_UNIVERSAL_STRING:
1102 case FILE_ASN1_TYPE_UTF8_STRING:
1103 case FILE_ASN1_TYPE_BMP_STRING:
1104 case FILE_ASN1_TYPE_IA5_STRING:
1105 case FILE_ASN1_TYPE_VISIBLE_STRING:
1106 case FILE_ASN1_TYPE_VIDEOTEX_STRING:
1107 case FILE_ASN1_TYPE_GRAPHIC_STRING:
1108 case FILE_ASN1_TYPE_GENERAL_STRING:
1111 case FILE_ASN1_TYPE_BOOLEAN:
1112 $value = $source ? "\xFF" : "\x00";
1115 user_error('Mapping provides no type definition for ' . implode('/', $this->location));
1120 array_pop($this->location);
1123 if (isset($mapping['cast'])) {
1124 if (isset($mapping['explicit']) || $mapping['type'] == FILE_ASN1_TYPE_CHOICE) {
1125 $value = chr($tag) . $this->_encodeLength(strlen($value)) . $value;
1126 $tag = ($mapping['class'] << 6) | 0x20 | $mapping['cast'];
1128 $tag = ($mapping['class'] << 6) | (ord($temp[0]) & 0x20) | $mapping['cast'];
1132 return chr($tag) . $this->_encodeLength(strlen($value)) . $value;
1136 * DER-encode the length
1138 * DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4. See
1139 * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information.
1142 * @param Integer $length
1145 function _encodeLength($length)
1147 if ($length <= 0x7F) {
1148 return chr($length);
1151 $temp = ltrim(pack('N', $length), chr(0));
1152 return pack('Ca*', 0x80 | strlen($temp), $temp);
1156 * BER-decode the time
1158 * Called by _decode_ber() and in the case of implicit tags asn1map().
1161 * @param String $content
1162 * @param Integer $tag
1165 function _decodeTime($content, $tag)
1168 http://tools.ietf.org/html/rfc5280#section-4.1.2.5.1
1169 http://www.obj-sys.com/asn1tutorial/node15.html
1172 http://tools.ietf.org/html/rfc5280#section-4.1.2.5.2
1173 http://www.obj-sys.com/asn1tutorial/node14.html */
1175 $pattern = $tag == FILE_ASN1_TYPE_UTC_TIME ?
1176 '#(..)(..)(..)(..)(..)(..)(.*)#' :
1177 '#(....)(..)(..)(..)(..)(..).*([Z+-].*)$#';
1179 preg_match($pattern, $content, $matches);
1181 list(, $year, $month, $day, $hour, $minute, $second, $timezone) = $matches;
1183 if ($tag == FILE_ASN1_TYPE_UTC_TIME) {
1184 $year = $year >= 50 ? "19$year" : "20$year";
1187 if ($timezone == 'Z') {
1188 $mktime = 'gmmktime';
1190 } elseif (preg_match('#([+-])(\d\d)(\d\d)#', $timezone, $matches)) {
1191 $mktime = 'gmmktime';
1192 $timezone = 60 * $matches[3] + 3600 * $matches[2];
1193 if ($matches[1] == '-') {
1194 $timezone = -$timezone;
1201 return @$mktime($hour, $minute, $second, $month, $day, $year) + $timezone;
1205 * Set the time format
1207 * Sets the time / date format for asn1map().
1210 * @param String $format
1212 function setTimeFormat($format)
1214 $this->format = $format;
1220 * Load the relevant OIDs for a particular ASN.1 semantic mapping.
1223 * @param Array $oids
1225 function loadOIDs($oids)
1227 $this->oids = $oids;
1233 * See File_X509, etc, for an example.
1236 * @param Array $filters
1238 function loadFilters($filters)
1240 $this->filters = $filters;
1246 * Inspired by array_shift
1248 * @param String $string
1249 * @param optional Integer $index
1253 function _string_shift(&$string, $index = 1)
1255 $substr = substr($string, 0, $index);
1256 $string = substr($string, $index);
1261 * String type conversion
1263 * This is a lazy conversion, dealing only with character size.
1264 * No real conversion table is used.
1267 * @param optional Integer $from
1268 * @param optional Integer $to
1272 function convert($in, $from = FILE_ASN1_TYPE_UTF8_STRING, $to = FILE_ASN1_TYPE_UTF8_STRING)
1274 if (!isset($this->stringTypeSize[$from]) || !isset($this->stringTypeSize[$to])) {
1277 $insize = $this->stringTypeSize[$from];
1278 $outsize = $this->stringTypeSize[$to];
1279 $inlength = strlen($in);
1282 for ($i = 0; $i < $inlength;) {
1283 if ($inlength - $i < $insize) {
1287 // Get an input character as a 32-bit value.
1288 $c = ord($in[$i++]);
1291 $c = ($c << 8) | ord($in[$i++]);
1292 $c = ($c << 8) | ord($in[$i++]);
1294 $c = ($c << 8) | ord($in[$i++]);
1297 case ($c & 0x80) == 0x00:
1299 case ($c & 0x40) == 0x00:
1304 if ($bit > 25 || $i >= $inlength || (ord($in[$i]) & 0xC0) != 0x80) {
1307 $c = ($c << 6) | (ord($in[$i++]) & 0x3F);
1310 } while ($c & $bit);
1315 // Convert and append the character to output string.
1319 $v .= chr($c & 0xFF);
1321 $v .= chr($c & 0xFF);
1324 $v .= chr($c & 0xFF);
1327 $v .= chr($c & 0xFF);
1333 case ($c & 0x80000000) != 0:
1335 case $c >= 0x04000000:
1336 $v .= chr(0x80 | ($c & 0x3F));
1337 $c = ($c >> 6) | 0x04000000;
1338 case $c >= 0x00200000:
1339 $v .= chr(0x80 | ($c & 0x3F));
1340 $c = ($c >> 6) | 0x00200000;
1341 case $c >= 0x00010000:
1342 $v .= chr(0x80 | ($c & 0x3F));
1343 $c = ($c >> 6) | 0x00010000;
1344 case $c >= 0x00000800:
1345 $v .= chr(0x80 | ($c & 0x3F));
1346 $c = ($c >> 6) | 0x00000800;
1347 case $c >= 0x00000080:
1348 $v .= chr(0x80 | ($c & 0x3F));
1349 $c = ($c >> 6) | 0x000000C0;