10b3fd2d3SAndreas Gohr<?php 20b3fd2d3SAndreas Gohr/** 30b3fd2d3SAndreas Gohr * This file is part of the FreeDSx ASN1 package. 40b3fd2d3SAndreas Gohr * 50b3fd2d3SAndreas Gohr * (c) Chad Sikorra <Chad.Sikorra@gmail.com> 60b3fd2d3SAndreas Gohr * 70b3fd2d3SAndreas Gohr * For the full copyright and license information, please view the LICENSE 80b3fd2d3SAndreas Gohr * file that was distributed with this source code. 90b3fd2d3SAndreas Gohr */ 100b3fd2d3SAndreas Gohr 110b3fd2d3SAndreas Gohrnamespace FreeDSx\Asn1\Encoder; 120b3fd2d3SAndreas Gohr 130b3fd2d3SAndreas Gohruse FreeDSx\Asn1\Exception\EncoderException; 140b3fd2d3SAndreas Gohruse FreeDSx\Asn1\Type\AbstractTimeType; 150b3fd2d3SAndreas Gohruse FreeDSx\Asn1\Type\AbstractType; 160b3fd2d3SAndreas Gohruse FreeDSx\Asn1\Type\SetOfType; 17*dad993c5SAndreas Gohruse function count; 18*dad993c5SAndreas Gohruse function end; 19*dad993c5SAndreas Gohruse function ord; 20*dad993c5SAndreas Gohruse function reset; 21*dad993c5SAndreas Gohruse function str_pad; 22*dad993c5SAndreas Gohruse function strcmp; 23*dad993c5SAndreas Gohruse function strlen; 24*dad993c5SAndreas Gohruse function usort; 250b3fd2d3SAndreas Gohr 260b3fd2d3SAndreas Gohr/** 270b3fd2d3SAndreas Gohr * Common restrictions on CER and DER encoding. 280b3fd2d3SAndreas Gohr * 290b3fd2d3SAndreas Gohr * @author Chad Sikorra <Chad.Sikorra@gmail.com> 300b3fd2d3SAndreas Gohr */ 310b3fd2d3SAndreas Gohrtrait CerDerTrait 320b3fd2d3SAndreas Gohr{ 330b3fd2d3SAndreas Gohr /** 340b3fd2d3SAndreas Gohr * @param int $length 350b3fd2d3SAndreas Gohr * @param int $unused 360b3fd2d3SAndreas Gohr * @return string 370b3fd2d3SAndreas Gohr * @throws EncoderException 380b3fd2d3SAndreas Gohr */ 390b3fd2d3SAndreas Gohr protected function binaryToBitString(int $length, int $unused): string 400b3fd2d3SAndreas Gohr { 41*dad993c5SAndreas Gohr if ($unused && $length && ord($this->binary[$this->pos + ($length - 1)]) !== 0 && ((8 - $length) << ord($this->binary[$this->pos + ($length - 1)])) !== 0) { 420b3fd2d3SAndreas Gohr throw new EncoderException(sprintf( 430b3fd2d3SAndreas Gohr 'The last %s unused bits of the bit string must be 0, but they are not.', 440b3fd2d3SAndreas Gohr $unused 450b3fd2d3SAndreas Gohr )); 460b3fd2d3SAndreas Gohr } 470b3fd2d3SAndreas Gohr 480b3fd2d3SAndreas Gohr return parent::binaryToBitString($length, $unused); 490b3fd2d3SAndreas Gohr } 500b3fd2d3SAndreas Gohr 510b3fd2d3SAndreas Gohr /** 520b3fd2d3SAndreas Gohr * @return bool 530b3fd2d3SAndreas Gohr * @throws EncoderException 540b3fd2d3SAndreas Gohr */ 550b3fd2d3SAndreas Gohr protected function decodeBoolean(): bool 560b3fd2d3SAndreas Gohr { 570b3fd2d3SAndreas Gohr if (!($this->binary[$this->pos] === self::BOOL_FALSE || $this->binary[$this->pos] === self::BOOL_TRUE)) { 580b3fd2d3SAndreas Gohr throw new EncoderException(sprintf('The encoded boolean must be 0 or 255, received "%s".', ord($this->binary[$this->pos]))); 590b3fd2d3SAndreas Gohr } 600b3fd2d3SAndreas Gohr 610b3fd2d3SAndreas Gohr return parent::decodeBoolean(); 620b3fd2d3SAndreas Gohr } 630b3fd2d3SAndreas Gohr 640b3fd2d3SAndreas Gohr /** 650b3fd2d3SAndreas Gohr * {@inheritdoc} 660b3fd2d3SAndreas Gohr * @throws EncoderException 670b3fd2d3SAndreas Gohr */ 680b3fd2d3SAndreas Gohr protected function encodeTime(AbstractTimeType $type, string $format) 690b3fd2d3SAndreas Gohr { 700b3fd2d3SAndreas Gohr $this->validateTimeType($type); 710b3fd2d3SAndreas Gohr 720b3fd2d3SAndreas Gohr return parent::encodeTime($type, $format); 730b3fd2d3SAndreas Gohr } 740b3fd2d3SAndreas Gohr 750b3fd2d3SAndreas Gohr /** 760b3fd2d3SAndreas Gohr * {@inheritdoc} 770b3fd2d3SAndreas Gohr * @throws EncoderException 780b3fd2d3SAndreas Gohr */ 790b3fd2d3SAndreas Gohr protected function validateDateFormat(array $matches, array $matchMap) 800b3fd2d3SAndreas Gohr { 810b3fd2d3SAndreas Gohr if (isset($matchMap['fractions']) && isset($matches[$matchMap['fractions']]) && $matches[$matchMap['fractions']] !== '') { 820b3fd2d3SAndreas Gohr if ($matches[$matchMap['fractions']][-1] === '0') { 830b3fd2d3SAndreas Gohr throw new EncoderException('Trailing zeros must be omitted from Generalized Time types, but it is not.'); 840b3fd2d3SAndreas Gohr } 850b3fd2d3SAndreas Gohr } 860b3fd2d3SAndreas Gohr } 870b3fd2d3SAndreas Gohr 880b3fd2d3SAndreas Gohr /** 890b3fd2d3SAndreas Gohr * @param AbstractTimeType $type 900b3fd2d3SAndreas Gohr * @throws EncoderException 910b3fd2d3SAndreas Gohr */ 920b3fd2d3SAndreas Gohr protected function validateTimeType(AbstractTimeType $type) 930b3fd2d3SAndreas Gohr { 940b3fd2d3SAndreas Gohr if ($type->getTimeZoneFormat() !== AbstractTimeType::TZ_UTC) { 950b3fd2d3SAndreas Gohr throw new EncoderException(sprintf( 960b3fd2d3SAndreas Gohr 'Time must end in a Z, but it does not. It is set to "%s".', 970b3fd2d3SAndreas Gohr $type->getTimeZoneFormat() 980b3fd2d3SAndreas Gohr )); 990b3fd2d3SAndreas Gohr } 1000b3fd2d3SAndreas Gohr $dtFormat = $type->getDateTimeFormat(); 1010b3fd2d3SAndreas Gohr if (!($dtFormat === AbstractTimeType::FORMAT_SECONDS || $dtFormat === AbstractTimeType::FORMAT_FRACTIONS)) { 1020b3fd2d3SAndreas Gohr throw new EncoderException(sprintf( 1030b3fd2d3SAndreas Gohr 'Time must be specified to the seconds, but it is specified to "%s".', 1040b3fd2d3SAndreas Gohr $dtFormat 1050b3fd2d3SAndreas Gohr )); 1060b3fd2d3SAndreas Gohr } 1070b3fd2d3SAndreas Gohr } 1080b3fd2d3SAndreas Gohr 1090b3fd2d3SAndreas Gohr /** 1100b3fd2d3SAndreas Gohr * X.690 Section 11.6 1110b3fd2d3SAndreas Gohr * 1120b3fd2d3SAndreas Gohr * The encodings of the component values of a set-of value shall appear in ascending order, the encodings being 1130b3fd2d3SAndreas Gohr * compared as octet strings with the shorter components being padded at their trailing end with 0-octets. 1140b3fd2d3SAndreas Gohr * 1150b3fd2d3SAndreas Gohr * NOTE – The padding octets are for comparison purposes only and do not appear in the encodings. 1160b3fd2d3SAndreas Gohr * 1170b3fd2d3SAndreas Gohr * --------- 1180b3fd2d3SAndreas Gohr * 1190b3fd2d3SAndreas Gohr * 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 1200b3fd2d3SAndreas Gohr * did find in "ASN.1 Complete" (John Larmouth) contains seemingly several encoding errors: 1210b3fd2d3SAndreas Gohr * 1220b3fd2d3SAndreas Gohr * - Length is not encoded correctly for the SET OF element. 1230b3fd2d3SAndreas Gohr * - The integer 10 is encoded incorrectly. 1240b3fd2d3SAndreas Gohr * - The sort is in descending order of the encoded value (in opposition to X.690 11.6), though in ascending 1250b3fd2d3SAndreas Gohr * order of the literal integer values. 1260b3fd2d3SAndreas Gohr * 1270b3fd2d3SAndreas Gohr * So I'm hesitant to trust that. Perhaps there's an example elsewhere to be used? Tests around this are hard to 1280b3fd2d3SAndreas Gohr * come by in ASN.1 libraries for some reason. 1290b3fd2d3SAndreas Gohr * 1300b3fd2d3SAndreas Gohr * @todo Is this assumed ordering correct? Confirmation needed. This could probably be simplified too. 1310b3fd2d3SAndreas Gohr * @param SetOfType $setOf 1320b3fd2d3SAndreas Gohr * @return string 1330b3fd2d3SAndreas Gohr */ 1340b3fd2d3SAndreas Gohr protected function encodeSetOf(SetOfType $setOf) 1350b3fd2d3SAndreas Gohr { 136*dad993c5SAndreas Gohr if (count($setOf->getChildren()) === 0) { 1370b3fd2d3SAndreas Gohr return ''; 1380b3fd2d3SAndreas Gohr } 1390b3fd2d3SAndreas Gohr $children = []; 1400b3fd2d3SAndreas Gohr 1410b3fd2d3SAndreas Gohr # Encode each child and record the length, we need it later 1420b3fd2d3SAndreas Gohr foreach ($setOf as $type) { 1430b3fd2d3SAndreas Gohr $child = ['original' => $this->encode($type)]; 144*dad993c5SAndreas Gohr $child['length'] = strlen($child['original']); 1450b3fd2d3SAndreas Gohr $children[] = $child; 1460b3fd2d3SAndreas Gohr } 1470b3fd2d3SAndreas Gohr 1480b3fd2d3SAndreas Gohr # Sort the encoded types by length first to determine the padding needed. 149*dad993c5SAndreas Gohr usort($children, function ($a, $b) { 1500b3fd2d3SAndreas Gohr /* @var AbstractType $a 1510b3fd2d3SAndreas Gohr * @var AbstractType $b */ 1520b3fd2d3SAndreas Gohr return $a['length'] < $b['length'] ? -1 : 1; 1530b3fd2d3SAndreas Gohr }); 1540b3fd2d3SAndreas Gohr 1550b3fd2d3SAndreas Gohr # Get the last child (ie. the longest), and put the array back to normal. 156*dad993c5SAndreas Gohr $child = end($children); 1570b3fd2d3SAndreas Gohr $padding = $child ['length']; 158*dad993c5SAndreas Gohr reset($children); 1590b3fd2d3SAndreas Gohr 1600b3fd2d3SAndreas Gohr # Sort by padding the items and comparing them. 161*dad993c5SAndreas Gohr usort($children, function ($a, $b) use ($padding) { 162*dad993c5SAndreas Gohr return strcmp( 163*dad993c5SAndreas Gohr str_pad($a['original'], $padding, "\x00"), 164*dad993c5SAndreas Gohr str_pad($b['original'], $padding, "\x00") 1650b3fd2d3SAndreas Gohr ); 1660b3fd2d3SAndreas Gohr }); 1670b3fd2d3SAndreas Gohr 1680b3fd2d3SAndreas Gohr # Reconstruct the byte string from the order obtained. 1690b3fd2d3SAndreas Gohr $bytes = ''; 1700b3fd2d3SAndreas Gohr foreach ($children as $child) { 1710b3fd2d3SAndreas Gohr $bytes .= $child['original']; 1720b3fd2d3SAndreas Gohr } 1730b3fd2d3SAndreas Gohr 1740b3fd2d3SAndreas Gohr return $bytes; 1750b3fd2d3SAndreas Gohr } 1760b3fd2d3SAndreas Gohr} 177