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