1<?php
2/**
3 * This file is part of the FreeDSx ASN1 package.
4 *
5 * (c) Chad Sikorra <Chad.Sikorra@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 FreeDSx\Asn1\Encoder;
12
13use FreeDSx\Asn1\Exception\EncoderException;
14use FreeDSx\Asn1\Type\AbstractTimeType;
15use FreeDSx\Asn1\Type\AbstractType;
16use FreeDSx\Asn1\Type\BooleanType;
17use FreeDSx\Asn1\Type\SetOfType;
18
19/**
20 * Common restrictions on CER and DER encoding.
21 *
22 * @author Chad Sikorra <Chad.Sikorra@gmail.com>
23 */
24trait CerDerTrait
25{
26    /**
27     * @param int $length
28     * @param int $unused
29     * @return string
30     * @throws EncoderException
31     */
32    protected function binaryToBitString(int $length, int $unused) : string
33    {
34        if ($unused && $length && \ord($this->binary[$this->pos + ($length- 1)]) !== 0 && ((8 - $length) << \ord($this->binary[$this->pos + ($length- 1)])) !== 0) {
35            throw new EncoderException(sprintf(
36                'The last %s unused bits of the bit string must be 0, but they are not.',
37                $unused
38            ));
39        }
40
41        return parent::binaryToBitString($length, $unused);
42    }
43
44    /**
45     * @return bool
46     * @throws EncoderException
47     */
48    protected function decodeBoolean() : bool
49    {
50        if (!($this->binary[$this->pos] === self::BOOL_FALSE || $this->binary[$this->pos] === self::BOOL_TRUE)) {
51            throw new EncoderException(sprintf('The encoded boolean must be 0 or 255, received "%s".', ord($this->binary[$this->pos])));
52        }
53
54        return parent::decodeBoolean();
55    }
56
57    /**
58     * {@inheritdoc}
59     * @throws EncoderException
60     */
61    protected function encodeTime(AbstractTimeType $type, string $format)
62    {
63        $this->validateTimeType($type);
64
65        return parent::encodeTime($type, $format);
66    }
67
68    /**
69     * {@inheritdoc}
70     * @throws EncoderException
71     */
72    protected function validateDateFormat(array $matches, array $matchMap)
73    {
74        if (isset($matchMap['fractions']) && isset($matches[$matchMap['fractions']]) && $matches[$matchMap['fractions']] !== '') {
75            if ($matches[$matchMap['fractions']][-1] === '0') {
76                throw new EncoderException('Trailing zeros must be omitted from Generalized Time types, but it is not.');
77            }
78        }
79    }
80
81    /**
82     * @param AbstractTimeType $type
83     * @throws EncoderException
84     */
85    protected function validateTimeType(AbstractTimeType $type)
86    {
87        if ($type->getTimeZoneFormat() !== AbstractTimeType::TZ_UTC) {
88            throw new EncoderException(sprintf(
89                'Time must end in a Z, but it does not. It is set to "%s".',
90                $type->getTimeZoneFormat()
91            ));
92        }
93        $dtFormat = $type->getDateTimeFormat();
94        if (!($dtFormat === AbstractTimeType::FORMAT_SECONDS || $dtFormat === AbstractTimeType::FORMAT_FRACTIONS)) {
95            throw new EncoderException(sprintf(
96                'Time must be specified to the seconds, but it is specified to "%s".',
97                $dtFormat
98            ));
99        }
100    }
101
102    /**
103     * X.690 Section 11.6
104     *
105     * The encodings of the component values of a set-of value shall appear in ascending order, the encodings being
106     * compared as octet strings with the shorter components being padded at their trailing end with 0-octets.
107     *
108     *   NOTE – The padding octets are for comparison purposes only and do not appear in the encodings.
109     *
110     * ---------
111     *
112     * It's very hard to find examples, but it's not clear to me from the wording if I have this correct. The example I
113     * did find in "ASN.1 Complete" (John Larmouth) contains seemingly several encoding errors:
114     *
115     *    - Length is not encoded correctly for the SET OF element.
116     *    - The integer 10 is encoded incorrectly.
117     *    - The sort is in descending order of the encoded value (in opposition to X.690 11.6), though in ascending
118     *      order of the literal integer values.
119     *
120     * So I'm hesitant to trust that. Perhaps there's an example elsewhere to be used? Tests around this are hard to
121     * come by in ASN.1 libraries for some reason.
122     *
123     * @todo Is this assumed ordering correct? Confirmation needed. This could probably be simplified too.
124     * @param SetOfType $setOf
125     * @return string
126     */
127    protected function encodeSetOf(SetOfType $setOf)
128    {
129        if (\count($setOf->getChildren()) === 0) {
130            return '';
131        }
132        $children = [];
133
134        # Encode each child and record the length, we need it later
135        foreach ($setOf as $type) {
136            $child = ['original' => $this->encode($type)];
137            $child['length'] = \strlen($child['original']);
138            $children[] = $child;
139        }
140
141        # Sort the encoded types by length first to determine the padding needed.
142        \usort($children, function ($a, $b) {
143            /* @var AbstractType $a
144             * @var AbstractType $b */
145            return $a['length'] < $b['length'] ? -1 : 1;
146        });
147
148        # Get the last child (ie. the longest), and put the array back to normal.
149        $child = \end($children);
150        $padding = $child ['length'];
151        \reset($children);
152
153        # Sort by padding the items and comparing them.
154        \usort($children, function($a, $b) use ($padding) {
155            return \strcmp(
156                \str_pad($a['original'], $padding, "\x00"),
157                \str_pad($b['original'], $padding, "\x00")
158            );
159        });
160
161        # Reconstruct the byte string from the order obtained.
162        $bytes = '';
163        foreach ($children as $child) {
164            $bytes .= $child['original'];
165        }
166
167        return $bytes;
168    }
169}
170