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