1<?php
2/*
3 * This file is part of the PHPASN1 library.
4 *
5 * Copyright © Friedrich Große <friedrich.grosse@gmail.com>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10
11namespace FG\ASN1;
12
13use Exception;
14
15/**
16 * The Identifier encodes the ASN.1 tag (class and number) of the type of a data value.
17 *
18 * Every identifier whose number is in the range 0 to 30 has the following structure:
19 *
20 * Bits:    8  7    6    5  4  3  2  1
21 *       | Class | P/C |   Tag number  |
22 *       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
23 *
24 * Bits 8 and 7 define the class of this type ( Universal, Application, Context-specific or Private).
25 * Bit 6 encoded whether this type is primitive or constructed
26 * The remaining bits 5 - 1 encode the tag number
27 */
28class Identifier
29{
30    const CLASS_UNIVERSAL        = 0x00;
31    const CLASS_APPLICATION      = 0x01;
32    const CLASS_CONTEXT_SPECIFIC = 0x02;
33    const CLASS_PRIVATE          = 0x03;
34
35    const EOC               = 0x00; // unsupported for now
36    const BOOLEAN           = 0x01;
37    const INTEGER           = 0x02;
38    const BITSTRING         = 0x03;
39    const OCTETSTRING       = 0x04;
40    const NULL              = 0x05;
41    const OBJECT_IDENTIFIER = 0x06;
42    const OBJECT_DESCRIPTOR = 0x07;
43    const EXTERNAL          = 0x08; // unsupported for now
44    const REAL              = 0x09; // unsupported for now
45    const ENUMERATED        = 0x0A;
46    const EMBEDDED_PDV      = 0x0B; // unsupported for now
47    const UTF8_STRING       = 0x0C;
48    const RELATIVE_OID      = 0x0D;
49    // value 0x0E and 0x0F are reserved for future use
50
51    const SEQUENCE          = 0x30;
52    const SET               = 0x31;
53    const NUMERIC_STRING    = 0x12;
54    const PRINTABLE_STRING  = 0x13;
55    const T61_STRING        = 0x14; // sometimes referred to as TeletextString
56    const VIDEOTEXT_STRING  = 0x15;
57    const IA5_STRING        = 0x16;
58    const UTC_TIME          = 0x17;
59    const GENERALIZED_TIME  = 0x18;
60    const GRAPHIC_STRING    = 0x19;
61    const VISIBLE_STRING    = 0x1A;
62    const GENERAL_STRING    = 0x1B;
63    const UNIVERSAL_STRING  = 0x1C;
64    const CHARACTER_STRING  = 0x1D; // Unrestricted character type
65    const BMP_STRING        = 0x1E;
66
67    const LONG_FORM         = 0x1F;
68    const IS_CONSTRUCTED    = 0x20;
69
70    /**
71     * Creates an identifier. Short form identifiers are returned as integers
72     * for BC, long form identifiers will be returned as a string of octets.
73     *
74     * @param int $class
75     * @param bool $isConstructed
76     * @param int $tagNumber
77     *
78     * @throws Exception if the given arguments are invalid
79     *
80     * @return int|string
81     */
82    public static function create($class, $isConstructed, $tagNumber)
83    {
84        if (!is_numeric($class) || $class < self::CLASS_UNIVERSAL || $class > self::CLASS_PRIVATE) {
85            throw new Exception(sprintf('Invalid class %d given', $class));
86        }
87
88        if (!is_bool($isConstructed)) {
89            throw new Exception("\$isConstructed must be a boolean value ($isConstructed given)");
90        }
91
92        $tagNumber = self::makeNumeric($tagNumber);
93        if ($tagNumber < 0) {
94            throw new Exception(sprintf('Invalid $tagNumber %d given. You can only use positive integers.', $tagNumber));
95        }
96
97        if ($tagNumber < self::LONG_FORM) {
98            return ($class << 6) | ($isConstructed << 5) | $tagNumber;
99        }
100
101        $firstOctet = ($class << 6) | ($isConstructed << 5) | self::LONG_FORM;
102
103        // Tag numbers formatted in long form are base-128 encoded. See X.609#8.1.2.4
104        return chr($firstOctet).Base128::encode($tagNumber);
105    }
106
107    public static function isConstructed($identifierOctet)
108    {
109        return ($identifierOctet & self::IS_CONSTRUCTED) === self::IS_CONSTRUCTED;
110    }
111
112    public static function isLongForm($identifierOctet)
113    {
114        return ($identifierOctet & self::LONG_FORM) === self::LONG_FORM;
115    }
116
117    /**
118     * Return the name of the mapped ASN.1 type with a preceding "ASN.1 ".
119     *
120     * Example: ASN.1 Octet String
121     *
122     * @see Identifier::getShortName()
123     *
124     * @param int|string $identifier
125     *
126     * @return string
127     */
128    public static function getName($identifier)
129    {
130        $identifierOctet = self::makeNumeric($identifier);
131
132        $typeName = static::getShortName($identifier);
133
134        if (($identifierOctet & self::LONG_FORM) < self::LONG_FORM) {
135            $typeName = "ASN.1 {$typeName}";
136        }
137
138        return $typeName;
139    }
140
141    /**
142     * Return the short version of the type name.
143     *
144     * If the given identifier octet can be mapped to a known universal type this will
145     * return its name. Else Identifier::getClassDescription() is used to retrieve
146     * information about the identifier.
147     *
148     * @see Identifier::getName()
149     * @see Identifier::getClassDescription()
150     *
151     * @param int|string $identifier
152     *
153     * @return string
154     */
155    public static function getShortName($identifier)
156    {
157        $identifierOctet = self::makeNumeric($identifier);
158
159        switch ($identifierOctet) {
160            case self::EOC:
161                return 'End-of-contents octet';
162            case self::BOOLEAN:
163                return 'Boolean';
164            case self::INTEGER:
165                return 'Integer';
166            case self::BITSTRING:
167                return 'Bit String';
168            case self::OCTETSTRING:
169                return 'Octet String';
170            case self::NULL:
171                return 'NULL';
172            case self::OBJECT_IDENTIFIER:
173                return 'Object Identifier';
174            case self::OBJECT_DESCRIPTOR:
175                return 'Object Descriptor';
176            case self::EXTERNAL:
177                return 'External Type';
178            case self::REAL:
179                return 'Real';
180            case self::ENUMERATED:
181                return 'Enumerated';
182            case self::EMBEDDED_PDV:
183                return 'Embedded PDV';
184            case self::UTF8_STRING:
185                return 'UTF8 String';
186            case self::RELATIVE_OID:
187                return 'Relative OID';
188            case self::SEQUENCE:
189                return 'Sequence';
190            case self::SET:
191                return 'Set';
192            case self::NUMERIC_STRING:
193                return 'Numeric String';
194            case self::PRINTABLE_STRING:
195                return 'Printable String';
196            case self::T61_STRING:
197                return 'T61 String';
198            case self::VIDEOTEXT_STRING:
199                return 'Videotext String';
200            case self::IA5_STRING:
201                return 'IA5 String';
202            case self::UTC_TIME:
203                return 'UTC Time';
204            case self::GENERALIZED_TIME:
205                return 'Generalized Time';
206            case self::GRAPHIC_STRING:
207                return 'Graphic String';
208            case self::VISIBLE_STRING:
209                return 'Visible String';
210            case self::GENERAL_STRING:
211                return 'General String';
212            case self::UNIVERSAL_STRING:
213                return 'Universal String';
214            case self::CHARACTER_STRING:
215                return 'Character String';
216            case self::BMP_STRING:
217                return 'BMP String';
218
219            case 0x0E:
220                return 'RESERVED (0x0E)';
221            case 0x0F:
222                return 'RESERVED (0x0F)';
223
224            case self::LONG_FORM:
225            default:
226                $classDescription = self::getClassDescription($identifier);
227
228                if (is_int($identifier)) {
229                    $identifier = chr($identifier);
230                }
231
232                return "$classDescription (0x".strtoupper(bin2hex($identifier)).')';
233        }
234    }
235
236    /**
237     * Returns a textual description of the information encoded in a given identifier octet.
238     *
239     * The first three (most significant) bytes are evaluated to determine if this is a
240     * constructed or primitive type and if it is either universal, application, context-specific or
241     * private.
242     *
243     * Example:
244     *     Constructed context-specific
245     *     Primitive universal
246     *
247     * @param int|string $identifier
248     *
249     * @return string
250     */
251    public static function getClassDescription($identifier)
252    {
253        $identifierOctet = self::makeNumeric($identifier);
254
255        if (self::isConstructed($identifierOctet)) {
256            $classDescription = 'Constructed ';
257        } else {
258            $classDescription = 'Primitive ';
259        }
260        $classBits = $identifierOctet >> 6;
261        switch ($classBits) {
262            case self::CLASS_UNIVERSAL:
263                $classDescription .= 'universal';
264                break;
265            case self::CLASS_APPLICATION:
266                $classDescription .= 'application';
267                break;
268            case self::CLASS_CONTEXT_SPECIFIC:
269                $tagNumber = self::getTagNumber($identifier);
270                $classDescription = "[$tagNumber] Context-specific";
271                break;
272            case self::CLASS_PRIVATE:
273                $classDescription .= 'private';
274                break;
275
276            default:
277                return "INVALID IDENTIFIER OCTET: {$identifierOctet}";
278        }
279
280        return $classDescription;
281    }
282
283    /**
284     * @param int|string $identifier
285     *
286     * @return int
287     */
288    public static function getTagNumber($identifier)
289    {
290        $firstOctet = self::makeNumeric($identifier);
291        $tagNumber = $firstOctet & self::LONG_FORM;
292
293        if ($tagNumber < self::LONG_FORM) {
294            return $tagNumber;
295        }
296
297        if (is_numeric($identifier)) {
298            $identifier = chr($identifier);
299        }
300        return Base128::decode(substr($identifier, 1));
301    }
302
303    public static function isUniversalClass($identifier)
304    {
305        $identifier = self::makeNumeric($identifier);
306
307        return $identifier >> 6 == self::CLASS_UNIVERSAL;
308    }
309
310    public static function isApplicationClass($identifier)
311    {
312        $identifier = self::makeNumeric($identifier);
313
314        return $identifier >> 6 == self::CLASS_APPLICATION;
315    }
316
317    public static function isContextSpecificClass($identifier)
318    {
319        $identifier = self::makeNumeric($identifier);
320
321        return $identifier >> 6 == self::CLASS_CONTEXT_SPECIFIC;
322    }
323
324    public static function isPrivateClass($identifier)
325    {
326        $identifier = self::makeNumeric($identifier);
327
328        return $identifier >> 6 == self::CLASS_PRIVATE;
329    }
330
331    private static function makeNumeric($identifierOctet)
332    {
333        if (!is_numeric($identifierOctet)) {
334            return ord($identifierOctet);
335        } else {
336            return $identifierOctet;
337        }
338    }
339}
340