* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace FreeDSx\Asn1\Encoder; use FreeDSx\Asn1\Exception\EncoderException; use FreeDSx\Asn1\Type\AbstractTimeType; use FreeDSx\Asn1\Type\AbstractType; use FreeDSx\Asn1\Type\SetOfType; use function count; use function end; use function ord; use function reset; use function str_pad; use function strcmp; use function strlen; use function usort; /** * Common restrictions on CER and DER encoding. * * @author Chad Sikorra */ trait CerDerTrait { /** * @param int $length * @param int $unused * @return string * @throws EncoderException */ protected function binaryToBitString(int $length, int $unused): string { if ($unused && $length && ord($this->binary[$this->pos + ($length - 1)]) !== 0 && ((8 - $length) << ord($this->binary[$this->pos + ($length - 1)])) !== 0) { throw new EncoderException(sprintf( 'The last %s unused bits of the bit string must be 0, but they are not.', $unused )); } return parent::binaryToBitString($length, $unused); } /** * @return bool * @throws EncoderException */ protected function decodeBoolean(): bool { if (!($this->binary[$this->pos] === self::BOOL_FALSE || $this->binary[$this->pos] === self::BOOL_TRUE)) { throw new EncoderException(sprintf('The encoded boolean must be 0 or 255, received "%s".', ord($this->binary[$this->pos]))); } return parent::decodeBoolean(); } /** * {@inheritdoc} * @throws EncoderException */ protected function encodeTime(AbstractTimeType $type, string $format) { $this->validateTimeType($type); return parent::encodeTime($type, $format); } /** * {@inheritdoc} * @throws EncoderException */ protected function validateDateFormat(array $matches, array $matchMap) { if (isset($matchMap['fractions']) && isset($matches[$matchMap['fractions']]) && $matches[$matchMap['fractions']] !== '') { if ($matches[$matchMap['fractions']][-1] === '0') { throw new EncoderException('Trailing zeros must be omitted from Generalized Time types, but it is not.'); } } } /** * @param AbstractTimeType $type * @throws EncoderException */ protected function validateTimeType(AbstractTimeType $type) { if ($type->getTimeZoneFormat() !== AbstractTimeType::TZ_UTC) { throw new EncoderException(sprintf( 'Time must end in a Z, but it does not. It is set to "%s".', $type->getTimeZoneFormat() )); } $dtFormat = $type->getDateTimeFormat(); if (!($dtFormat === AbstractTimeType::FORMAT_SECONDS || $dtFormat === AbstractTimeType::FORMAT_FRACTIONS)) { throw new EncoderException(sprintf( 'Time must be specified to the seconds, but it is specified to "%s".', $dtFormat )); } } /** * X.690 Section 11.6 * * The encodings of the component values of a set-of value shall appear in ascending order, the encodings being * compared as octet strings with the shorter components being padded at their trailing end with 0-octets. * * NOTE – The padding octets are for comparison purposes only and do not appear in the encodings. * * --------- * * 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 * did find in "ASN.1 Complete" (John Larmouth) contains seemingly several encoding errors: * * - Length is not encoded correctly for the SET OF element. * - The integer 10 is encoded incorrectly. * - The sort is in descending order of the encoded value (in opposition to X.690 11.6), though in ascending * order of the literal integer values. * * So I'm hesitant to trust that. Perhaps there's an example elsewhere to be used? Tests around this are hard to * come by in ASN.1 libraries for some reason. * * @todo Is this assumed ordering correct? Confirmation needed. This could probably be simplified too. * @param SetOfType $setOf * @return string */ protected function encodeSetOf(SetOfType $setOf) { if (count($setOf->getChildren()) === 0) { return ''; } $children = []; # Encode each child and record the length, we need it later foreach ($setOf as $type) { $child = ['original' => $this->encode($type)]; $child['length'] = strlen($child['original']); $children[] = $child; } # Sort the encoded types by length first to determine the padding needed. usort($children, function ($a, $b) { /* @var AbstractType $a * @var AbstractType $b */ return $a['length'] < $b['length'] ? -1 : 1; }); # Get the last child (ie. the longest), and put the array back to normal. $child = end($children); $padding = $child ['length']; reset($children); # Sort by padding the items and comparing them. usort($children, function ($a, $b) use ($padding) { return strcmp( str_pad($a['original'], $padding, "\x00"), str_pad($b['original'], $padding, "\x00") ); }); # Reconstruct the byte string from the order obtained. $bytes = ''; foreach ($children as $child) { $bytes .= $child['original']; } return $bytes; } }