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