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 DateTime; 14use DateTimeZone; 15use FreeDSx\Asn1\Exception\EncoderException; 16use FreeDSx\Asn1\Exception\InvalidArgumentException; 17use FreeDSx\Asn1\Exception\PartialPduException; 18use FreeDSx\Asn1\Type\AbstractStringType; 19use FreeDSx\Asn1\Type\AbstractTimeType; 20use FreeDSx\Asn1\Type\AbstractType; 21use FreeDSx\Asn1\Type as EncodedType; 22use FreeDSx\Asn1\Type\BitStringType; 23use FreeDSx\Asn1\Type\BooleanType; 24use FreeDSx\Asn1\Type\EnumeratedType; 25use FreeDSx\Asn1\Type\GeneralizedTimeType; 26use FreeDSx\Asn1\Type\IncompleteType; 27use FreeDSx\Asn1\Type\IntegerType; 28use FreeDSx\Asn1\Type\NullType; 29use FreeDSx\Asn1\Type\OidType; 30use FreeDSx\Asn1\Type\RealType; 31use FreeDSx\Asn1\Type\RelativeOidType; 32use FreeDSx\Asn1\Type\SetOfType; 33use FreeDSx\Asn1\Type\SetType; 34use FreeDSx\Asn1\Type\UtcTimeType; 35use function bin2hex; 36use function bindec; 37use function chr; 38use function count; 39use function decbin; 40use function dechex; 41use function explode; 42use function extension_loaded; 43use function floor; 44use function gmp_abs; 45use function gmp_add; 46use function gmp_and; 47use function gmp_div; 48use function gmp_export; 49use function gmp_import; 50use function gmp_init; 51use function gmp_intval; 52use function gmp_mul; 53use function gmp_neg; 54use function gmp_or; 55use function gmp_pow; 56use function gmp_strval; 57use function gmp_sub; 58use function hex2bin; 59use function hexdec; 60use function is_float; 61use function is_int; 62use function is_numeric; 63use function is_string; 64use function ord; 65use function preg_match; 66use function rtrim; 67use function sprintf; 68use function str_pad; 69use function strlen; 70use function strpos; 71use function substr; 72 73/** 74 * Basic Encoding Rules (BER) encoder. 75 * 76 * @author Chad Sikorra <Chad.Sikorra@gmail.com> 77 */ 78class BerEncoder implements EncoderInterface 79{ 80 /** 81 * Used to represent a bool false binary string. 82 */ 83 protected const BOOL_FALSE = "\x00"; 84 85 /** 86 * Used to represent a bool true binary string. 87 */ 88 protected const BOOL_TRUE = "\xff"; 89 90 /** 91 * Anything greater than this we assume we may need to deal with a bigint in an OIDs second component. 92 */ 93 protected const MAX_SECOND_COMPONENT = PHP_INT_MAX - 80; 94 95 /** 96 * @var array 97 */ 98 protected $tagMap = [ 99 AbstractType::TAG_CLASS_APPLICATION => [], 100 AbstractType::TAG_CLASS_CONTEXT_SPECIFIC => [], 101 AbstractType::TAG_CLASS_PRIVATE => [], 102 ]; 103 104 protected $tmpTagMap = []; 105 106 /** 107 * @var array 108 */ 109 protected $options = [ 110 'bitstring_padding' => '0', 111 ]; 112 113 /** 114 * @var bool 115 */ 116 protected $isGmpAvailable; 117 118 /** 119 * @var int 120 */ 121 protected $pos; 122 123 /** 124 * @var int|null 125 */ 126 protected $lastPos; 127 128 /** 129 * @var int 130 */ 131 protected $maxLen; 132 133 /** 134 * @var string|null 135 */ 136 protected $binary; 137 138 /** 139 * @param array $options 140 */ 141 public function __construct(array $options = []) 142 { 143 $this->isGmpAvailable = extension_loaded('gmp'); 144 $this->setOptions($options); 145 } 146 147 /** 148 * {@inheritdoc} 149 */ 150 public function decode($binary, array $tagMap = []): AbstractType 151 { 152 $this->startEncoding($binary, $tagMap); 153 if ($this->maxLen === 0) { 154 throw new InvalidArgumentException('The data to decode cannot be empty.'); 155 } elseif ($this->maxLen === 1) { 156 throw new PartialPduException('Received only 1 byte of data.'); 157 } 158 $type = $this->decodeBytes(true); 159 $this->stopEncoding(); 160 161 return $type; 162 } 163 164 /** 165 * {@inheritdoc} 166 */ 167 public function complete(IncompleteType $type, int $tagType, array $tagMap = []): AbstractType 168 { 169 $lastPos = $this->lastPos; 170 $this->startEncoding($type->getValue(), $tagMap); 171 $newType = $this->decodeBytes(false, $tagType, $this->maxLen, $type->getIsConstructed(), AbstractType::TAG_CLASS_UNIVERSAL); 172 $this->stopEncoding(); 173 $newType->setTagNumber($type->getTagNumber()) 174 ->setTagClass($type->getTagClass()); 175 $this->lastPos = $lastPos; 176 177 return $newType; 178 } 179 180 /** 181 * {@inheritdoc} 182 */ 183 public function encode(AbstractType $type): string 184 { 185 switch ($type) { 186 case $type instanceof BooleanType: 187 $bytes = $type->getValue() ? self::BOOL_TRUE : self::BOOL_FALSE; 188 break; 189 case $type instanceof IntegerType: 190 case $type instanceof EnumeratedType: 191 $bytes = $this->encodeInteger($type); 192 break; 193 case $type instanceof RealType: 194 $bytes = $this->encodeReal($type); 195 break; 196 case $type instanceof AbstractStringType: 197 $bytes = $type->getValue(); 198 break; 199 case $type instanceof SetOfType: 200 $bytes = $this->encodeSetOf($type); 201 break; 202 case $type instanceof SetType: 203 $bytes = $this->encodeSet($type); 204 break; 205 case $type->getIsConstructed(): 206 $bytes = $this->encodeConstructedType(...$type->getChildren()); 207 break; 208 case $type instanceof BitStringType: 209 $bytes = $this->encodeBitString($type); 210 break; 211 case $type instanceof OidType: 212 $bytes = $this->encodeOid($type); 213 break; 214 case $type instanceof RelativeOidType: 215 $bytes = $this->encodeRelativeOid($type); 216 break; 217 case $type instanceof GeneralizedTimeType: 218 $bytes = $this->encodeGeneralizedTime($type); 219 break; 220 case $type instanceof UtcTimeType: 221 $bytes = $this->encodeUtcTime($type); 222 break; 223 case $type instanceof NullType: 224 $bytes = ''; 225 break; 226 default: 227 throw new EncoderException(sprintf( 228 'The type "%s" is not currently supported.', 229 get_class($type) 230 )); 231 } 232 $length = strlen($bytes); 233 $bytes = ($length < 128) ? chr($length) . $bytes : $this->encodeLongDefiniteLength($length) . $bytes; 234 235 # The first byte of a tag always contains the class (bits 8 and 7) and whether it is constructed (bit 6). 236 $tag = $type->getTagClass() | ($type->getIsConstructed() ? AbstractType::CONSTRUCTED_TYPE : 0); 237 238 $this->validateNumericInt($type->getTagNumber()); 239 # For a high tag (>=31) we flip the first 5 bits on (0x1f) to make the first byte, then the subsequent bytes is 240 # the VLV encoding of the tag number. 241 if ($type->getTagNumber() >= 31) { 242 $bytes = chr($tag | 0x1f) . $this->intToVlqBytes($type->getTagNumber()) . $bytes; 243 # For a tag less than 31, everything fits comfortably into a single byte. 244 } else { 245 $bytes = chr($tag | $type->getTagNumber()) . $bytes; 246 } 247 248 return $bytes; 249 } 250 251 /** 252 * Map universal types to specific tag class values when decoding. 253 * 254 * @param int $class 255 * @param array $map 256 * @return $this 257 */ 258 public function setTagMap(int $class, array $map) 259 { 260 if (isset($this->tagMap[$class])) { 261 $this->tagMap[$class] = $map; 262 } 263 264 return $this; 265 } 266 267 /** 268 * Get the options for the encoder. 269 * 270 * @return array 271 */ 272 public function getOptions(): array 273 { 274 return $this->options; 275 } 276 277 /** 278 * Set the options for the encoder. 279 * 280 * @param array $options 281 * @return $this 282 */ 283 public function setOptions(array $options) 284 { 285 if (isset($options['bitstring_padding']) && is_string($options['bitstring_padding'])) { 286 $this->options['bitstring_padding'] = $options['bitstring_padding']; 287 } 288 289 return $this; 290 } 291 292 /** 293 * @return int|null 294 */ 295 public function getLastPosition(): ?int 296 { 297 return $this->lastPos; 298 } 299 300 protected function startEncoding(string $binary, array $tagMap): void 301 { 302 $this->tmpTagMap = $tagMap + $this->tagMap; 303 $this->binary = $binary; 304 $this->lastPos = null; 305 $this->pos = 0; 306 $this->maxLen = strlen($this->binary); 307 } 308 309 protected function stopEncoding(): void 310 { 311 $this->tmpTagMap = []; 312 $this->binary = null; 313 $this->maxLen = 0; 314 $this->lastPos = $this->pos; 315 $this->pos = 0; 316 } 317 318 /** 319 * @param bool $isRoot 320 * @param null|int $tagType 321 * @param null|int $length 322 * @param null|bool $isConstructed 323 * @param null|int $class 324 * @return AbstractType 325 * @throws EncoderException 326 * @throws PartialPduException 327 */ 328 protected function decodeBytes(bool $isRoot = false, $tagType = null, $length = null, $isConstructed = null, $class = null): AbstractType 329 { 330 $tagNumber = $tagType; 331 if ($tagType === null) { 332 $tag = ord($this->binary[$this->pos++]); 333 $class = $tag & 0xc0; 334 $isConstructed = (bool)($tag & AbstractType::CONSTRUCTED_TYPE); 335 $tagNumber = $tag & ~0xe0; 336 337 # Less than or equal to 30 is a low tag number represented in a single byte. 338 # A high tag number is determined using VLQ (like the OID identifier encoding) of the subsequent bytes. 339 if ($tagNumber > 30) { 340 try { 341 $tagNumber = $this->getVlqBytesToInt(); 342 # It's possible we only got part of the VLQ for the high tag, as there is no way to know its ending length. 343 } catch (EncoderException $e) { 344 if ($isRoot) { 345 throw new PartialPduException( 346 'Not enough data to decode the high tag number. No ending byte encountered for the VLQ bytes.' 347 ); 348 } 349 throw $e; 350 } 351 } 352 353 $length = ord($this->binary[$this->pos++]); 354 if ($length === 128) { 355 throw new EncoderException('Indefinite length encoding is not currently supported.'); 356 } 357 if ($length > 128) { 358 $length = $this->decodeLongDefiniteLength($length); 359 } 360 $tagType = ($class === AbstractType::TAG_CLASS_UNIVERSAL) ? $tagNumber : ($this->tmpTagMap[$class][$tagNumber] ?? null); 361 362 if (($this->maxLen - $this->pos) < $length) { 363 $message = sprintf( 364 'The expected byte length was %s, but received %s.', 365 $length, 366 ($this->maxLen - $this->pos) 367 ); 368 if ($isRoot) { 369 throw new PartialPduException($message); 370 } else { 371 throw new EncoderException($message); 372 } 373 } 374 375 if ($tagType === null) { 376 $type = new IncompleteType(substr($this->binary, $this->pos, $length), $tagNumber, $class, $isConstructed); 377 $this->pos += $length; 378 379 return $type; 380 } 381 } 382 383 # Yes...this huge switch statement should be a separate method. However, it is faster inline when decoding 384 # lots of data (such as thousands of ASN.1 structures at a time). 385 switch ($tagType) { 386 case AbstractType::TAG_TYPE_BOOLEAN: 387 if ($length !== 1 || $isConstructed) { 388 throw new EncoderException('The encoded boolean type is malformed.'); 389 } 390 $type = EncodedType\BooleanType::withTag($tagNumber, $class, $this->decodeBoolean()); 391 break; 392 case AbstractType::TAG_TYPE_NULL: 393 if ($length !== 0 || $isConstructed) { 394 throw new EncoderException('The encoded null type is malformed.'); 395 } 396 $type = EncodedType\NullType::withTag($tagNumber, $class); 397 break; 398 case AbstractType::TAG_TYPE_INTEGER: 399 if ($isConstructed) { 400 throw new EncoderException('The encoded integer type is malformed.'); 401 } 402 $type = EncodedType\IntegerType::withTag($tagNumber, $class, $this->decodeInteger($length)); 403 break; 404 case AbstractType::TAG_TYPE_ENUMERATED: 405 if ($isConstructed) { 406 throw new EncoderException('The encoded enumerated type is malformed.'); 407 } 408 $type = EncodedType\EnumeratedType::withTag($tagNumber, $class, $this->decodeInteger($length)); 409 break; 410 case AbstractType::TAG_TYPE_REAL: 411 if ($isConstructed) { 412 throw new EncoderException('The encoded real type is malformed.'); 413 } 414 $type = RealType::withTag($tagNumber, $class, $this->decodeReal($length)); 415 break; 416 case AbstractType::TAG_TYPE_BIT_STRING: 417 $type = EncodedType\BitStringType::withTag($tagNumber, $class, $isConstructed, $this->decodeBitString($length)); 418 break; 419 case AbstractType::TAG_TYPE_OID: 420 if ($isConstructed) { 421 throw new EncoderException('The encoded OID type is malformed.'); 422 } 423 $type = OidType::withTag($tagNumber, $class, $this->decodeOid($length)); 424 break; 425 case AbstractType::TAG_TYPE_RELATIVE_OID: 426 if ($isConstructed) { 427 throw new EncoderException('The encoded relative OID type is malformed.'); 428 } 429 $type = RelativeOidType::withTag($tagNumber, $class, $this->decodeRelativeOid($length)); 430 break; 431 case AbstractType::TAG_TYPE_GENERALIZED_TIME: 432 $type = EncodedType\GeneralizedTimeType::withTag($tagNumber, $class, $isConstructed, ...$this->decodeGeneralizedTime($length)); 433 break; 434 case AbstractType::TAG_TYPE_UTC_TIME: 435 $type = EncodedType\UtcTimeType::withTag($tagNumber, $class, $isConstructed, ...$this->decodeUtcTime($length)); 436 break; 437 case AbstractType::TAG_TYPE_OCTET_STRING: 438 $type = EncodedType\OctetStringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length)); 439 $this->pos += $length; 440 break; 441 case AbstractType::TAG_TYPE_GENERAL_STRING: 442 $type = EncodedType\GeneralStringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length)); 443 $this->pos += $length; 444 break; 445 case AbstractType::TAG_TYPE_VISIBLE_STRING: 446 $type = EncodedType\VisibleStringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length)); 447 $this->pos += $length; 448 break; 449 case AbstractType::TAG_TYPE_BMP_STRING: 450 $type = EncodedType\BmpStringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length)); 451 $this->pos += $length; 452 break; 453 case AbstractType::TAG_TYPE_CHARACTER_STRING: 454 $type = EncodedType\CharacterStringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length)); 455 $this->pos += $length; 456 break; 457 case AbstractType::TAG_TYPE_UNIVERSAL_STRING: 458 $type = EncodedType\UniversalStringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length)); 459 $this->pos += $length; 460 break; 461 case AbstractType::TAG_TYPE_GRAPHIC_STRING: 462 $type = EncodedType\GraphicStringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length)); 463 $this->pos += $length; 464 break; 465 case AbstractType::TAG_TYPE_VIDEOTEX_STRING: 466 $type = EncodedType\VideotexStringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length)); 467 $this->pos += $length; 468 break; 469 case AbstractType::TAG_TYPE_TELETEX_STRING: 470 $type = EncodedType\TeletexStringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length)); 471 $this->pos += $length; 472 break; 473 case AbstractType::TAG_TYPE_PRINTABLE_STRING: 474 $type = EncodedType\PrintableStringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length)); 475 $this->pos += $length; 476 break; 477 case AbstractType::TAG_TYPE_NUMERIC_STRING: 478 $type = EncodedType\NumericStringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length)); 479 $this->pos += $length; 480 break; 481 case AbstractType::TAG_TYPE_IA5_STRING: 482 $type = EncodedType\IA5StringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length)); 483 $this->pos += $length; 484 break; 485 case AbstractType::TAG_TYPE_UTF8_STRING: 486 $type = EncodedType\Utf8StringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length)); 487 $this->pos += $length; 488 break; 489 case AbstractType::TAG_TYPE_SEQUENCE: 490 if (!$isConstructed) { 491 throw new EncoderException('The encoded sequence type is malformed.'); 492 } 493 $type = EncodedType\SequenceType::withTag($tagNumber, $class, $this->decodeConstructedType($length)); 494 break; 495 case AbstractType::TAG_TYPE_SET: 496 if (!$isConstructed) { 497 throw new EncoderException('The encoded set type is malformed.'); 498 } 499 $type = EncodedType\SetType::withTag($tagNumber, $class, $this->decodeConstructedType($length)); 500 break; 501 default: 502 throw new EncoderException(sprintf('Unable to decode value to a type for tag %s.', $tagType)); 503 } 504 505 return $type; 506 } 507 508 /** 509 * @param int $length 510 * @return int 511 * @throws EncoderException 512 * @throws PartialPduException 513 */ 514 protected function decodeLongDefiniteLength(int $length): int 515 { 516 # The length of the length bytes is in the first 7 bits. So remove the MSB to get the value. 517 $lengthOfLength = $length & ~0x80; 518 519 # The value of 127 is marked as reserved in the spec 520 if ($lengthOfLength === 127) { 521 throw new EncoderException('The decoded length cannot be equal to 127 bytes.'); 522 } 523 if ($lengthOfLength > ($this->maxLen - $this->pos)) { 524 throw new PartialPduException('Not enough data to decode the length.'); 525 } 526 $endAt = $this->pos + $lengthOfLength; 527 528 # Base 256 encoded 529 $length = 0; 530 for ($this->pos; $this->pos < $endAt; $this->pos++) { 531 $length = $length * 256 + ord($this->binary[$this->pos]); 532 } 533 534 return $length; 535 } 536 537 /** 538 * Given what should be VLQ bytes represent an int, get the int and the length of bytes. 539 * 540 * @return string|int 541 * @throws EncoderException 542 */ 543 protected function getVlqBytesToInt() 544 { 545 $value = 0; 546 $lshift = 0; 547 $isBigInt = false; 548 549 for ($this->pos; $this->pos < $this->maxLen; $this->pos++) { 550 if (!$isBigInt) { 551 $lshift = $value << 7; 552 # An overflow bitshift will result in a negative number or zero. 553 # This will check if GMP is available and flip it to a bigint safe method in one shot. 554 if ($value > 0 && $lshift <= 0) { 555 $isBigInt = true; 556 $this->throwIfBigIntGmpNeeded(true); 557 $value = gmp_init($value); 558 } 559 } 560 if ($isBigInt) { 561 $lshift = gmp_mul($value, gmp_pow('2', 7)); 562 } 563 $orVal = (ord($this->binary[$this->pos]) & 0x7f); 564 if ($isBigInt) { 565 $value = gmp_or($lshift, gmp_init($orVal)); 566 } else { 567 $value = $lshift | $orVal; 568 } 569 # We have reached the last byte if the MSB is not set. 570 if ((ord($this->binary[$this->pos]) & 0x80) === 0) { 571 $this->pos++; 572 573 return $isBigInt ? gmp_strval($value) : $value; 574 } 575 } 576 577 throw new EncoderException('Expected an ending byte to decode a VLQ, but none was found.'); 578 } 579 580 /** 581 * Get the bytes that represent variable length quantity. 582 * 583 * @param string|int $int 584 * @return string 585 * @throws EncoderException 586 */ 587 protected function intToVlqBytes($int) 588 { 589 $bigint = is_float($int + 0); 590 $this->throwIfBigIntGmpNeeded($bigint); 591 592 if ($bigint) { 593 $int = gmp_init($int); 594 $bytes = chr(gmp_intval(gmp_and(gmp_init(0x7f), $int))); 595 $int = gmp_div($int, gmp_pow(2, 7)); 596 $intVal = gmp_intval($int); 597 } else { 598 $bytes = chr(0x7f & $int); 599 $int >>= 7; 600 $intVal = $int; 601 } 602 603 while ($intVal > 0) { 604 if ($bigint) { 605 $bytes = chr(gmp_intval(gmp_or(gmp_and(gmp_init(0x7f), $int), gmp_init(0x80)))) . $bytes; 606 $int = gmp_div($int, gmp_pow('2', 7)); 607 $intVal = gmp_intval($int); 608 } else { 609 $bytes = chr((0x7f & $int) | 0x80) . $bytes; 610 $int >>= 7; 611 $intVal = $int; 612 } 613 } 614 615 return $bytes; 616 } 617 618 /** 619 * @param string|integer $integer 620 * @throws EncoderException 621 */ 622 protected function validateNumericInt($integer): void 623 { 624 if (is_int($integer)) { 625 return; 626 } 627 if (is_string($integer) && is_numeric($integer) && strpos($integer, '.') === false) { 628 return; 629 } 630 631 throw new EncoderException(sprintf( 632 'The value to encode for "%s" must be numeric.', 633 $integer 634 )); 635 } 636 637 /** 638 * @param int $num 639 * @return string 640 * @throws EncoderException 641 */ 642 protected function encodeLongDefiniteLength(int $num) 643 { 644 $bytes = ''; 645 while ($num) { 646 $bytes = (chr((int) ($num % 256))) . $bytes; 647 $num = (int) ($num / 256); 648 } 649 650 $length = strlen($bytes); 651 if ($length >= 127) { 652 throw new EncoderException('The encoded length cannot be greater than or equal to 127 bytes'); 653 } 654 655 return chr(0x80 | $length) . $bytes; 656 } 657 658 /** 659 * @param BitStringType $type 660 * @return string 661 */ 662 protected function encodeBitString(BitStringType $type) 663 { 664 $data = $type->getValue(); 665 $length = strlen($data); 666 $unused = 0; 667 if ($length % 8) { 668 $unused = 8 - ($length % 8); 669 $data = str_pad($data, $length + $unused, $this->options['bitstring_padding']); 670 $length = strlen($data); 671 } 672 673 $bytes = chr($unused); 674 for ($i = 0; $i < $length / 8; $i++) { 675 $bytes .= chr(bindec(substr($data, $i * 8, 8))); 676 } 677 678 return $bytes; 679 } 680 681 /** 682 * @param RelativeOidType $type 683 * @return string 684 * @throws EncoderException 685 */ 686 protected function encodeRelativeOid(RelativeOidType $type) 687 { 688 $oids = explode('.', $type->getValue()); 689 690 $bytes = ''; 691 foreach ($oids as $oid) { 692 $bytes .= $this->intToVlqBytes($oid); 693 } 694 695 return $bytes; 696 } 697 698 /** 699 * @param OidType $type 700 * @return string 701 * @throws EncoderException 702 */ 703 protected function encodeOid(OidType $type) 704 { 705 /** @var int[] $oids */ 706 $oids = explode('.', $type->getValue()); 707 $length = count($oids); 708 if ($length < 2) { 709 throw new EncoderException(sprintf( 710 'To encode the OID it must have at least 2 components: %s', 711 $type->getValue() 712 )); 713 } 714 if ($oids[0] > 2) { 715 throw new EncoderException(sprintf( 716 'The value of the first OID component cannot be greater than 2. Received: %s', 717 $oids[0] 718 )); 719 } 720 721 # The first and second components of the OID are represented using the formula: (X * 40) + Y 722 if ($oids[1] > self::MAX_SECOND_COMPONENT) { 723 $this->throwIfBigIntGmpNeeded(true); 724 $firstAndSecond = gmp_strval(gmp_add((string)($oids[0] * 40), $oids[1])); 725 } else { 726 $firstAndSecond = ($oids[0] * 40) + $oids[1]; 727 } 728 $bytes = $this->intToVlqBytes($firstAndSecond); 729 730 for ($i = 2; $i < $length; $i++) { 731 $bytes .= $this->intToVlqBytes($oids[$i]); 732 } 733 734 return $bytes; 735 } 736 737 /** 738 * @param GeneralizedTimeType $type 739 * @return string 740 * @throws EncoderException 741 */ 742 protected function encodeGeneralizedTime(GeneralizedTimeType $type) 743 { 744 return $this->encodeTime($type, 'YmdH'); 745 } 746 747 /** 748 * @param UtcTimeType $type 749 * @return string 750 * @throws EncoderException 751 */ 752 protected function encodeUtcTime(UtcTimeType $type) 753 { 754 return $this->encodeTime($type, 'ymdH'); 755 } 756 757 /** 758 * @param AbstractTimeType $type 759 * @param string $format 760 * @return string 761 * @throws EncoderException 762 */ 763 protected function encodeTime(AbstractTimeType $type, string $format) 764 { 765 if ($type->getDateTimeFormat() === GeneralizedTimeType::FORMAT_SECONDS || $type->getDateTimeFormat() === GeneralizedTimeType::FORMAT_FRACTIONS) { 766 $format .= 'is'; 767 } elseif ($type->getDateTimeFormat() === GeneralizedTimeType::FORMAT_MINUTES) { 768 $format .= 'i'; 769 } 770 771 # Is it possible to construct a datetime object in this way? Seems better to be safe with this check. 772 if ($type->getValue()->format('H') === '24') { 773 throw new EncoderException('Midnight must only be specified by 00, not 24.'); 774 } 775 776 return $this->formatDateTime( 777 clone $type->getValue(), 778 $type->getDateTimeFormat(), 779 $type->getTimeZoneFormat(), 780 $format 781 ); 782 } 783 784 /** 785 * @param \DateTime $dateTime 786 * @param string $dateTimeFormat 787 * @param string $tzFormat 788 * @param string $format 789 * @return string 790 */ 791 protected function formatDateTime(DateTime $dateTime, string $dateTimeFormat, string $tzFormat, string $format) 792 { 793 if ($tzFormat === GeneralizedTimeType::TZ_LOCAL) { 794 $dateTime->setTimezone(new DateTimeZone(date_default_timezone_get())); 795 } elseif ($tzFormat === GeneralizedTimeType::TZ_UTC) { 796 $dateTime->setTimezone(new DateTimeZone('UTC')); 797 } 798 $value = $dateTime->format($format); 799 800 # Fractions need special formatting, so we cannot directly include them in the format above. 801 $ms = ''; 802 if ($dateTimeFormat === GeneralizedTimeType::FORMAT_FRACTIONS) { 803 $ms = (string) rtrim($dateTime->format('u'), '0'); 804 } 805 806 $tz = ''; 807 if ($tzFormat === GeneralizedTimeType::TZ_UTC) { 808 $tz = 'Z'; 809 } elseif ($tzFormat === GeneralizedTimeType::TZ_DIFF) { 810 $tz = $dateTime->format('O'); 811 } 812 813 return $value . ($ms !== '' ? '.' . $ms : '') . $tz; 814 } 815 816 /** 817 * @param IntegerType|EnumeratedType $type 818 * @return string 819 * @throws EncoderException 820 */ 821 protected function encodeInteger(AbstractType $type): string 822 { 823 $int = $type->getValue(); 824 $this->validateNumericInt($int); 825 $isBigInt = $type->isBigInt(); 826 $isNegative = ($int < 0); 827 $this->throwIfBigIntGmpNeeded($isBigInt); 828 if ($isNegative) { 829 $int = $isBigInt ? gmp_abs($type->getValue()) : ($int * -1); 830 } 831 832 # Subtract one for Two's Complement... 833 if ($isNegative) { 834 $int = $isBigInt ? gmp_sub($int, '1') : $int - 1; 835 } 836 837 if ($isBigInt) { 838 $bytes = gmp_export($int); 839 } else { 840 # dechex can produce uneven hex while hex2bin requires it to be even 841 $hex = dechex($int); 842 $bytes = hex2bin((strlen($hex) % 2) === 0 ? $hex : '0' . $hex); 843 } 844 845 # Two's Complement, invert the bits... 846 if ($isNegative) { 847 $len = strlen($bytes); 848 for ($i = 0; $i < $len; $i++) { 849 /** @var string $bytes */ 850 $bytes[$i] = ~$bytes[$i]; 851 } 852 } 853 854 # MSB == Most Significant Bit. The one used for the sign. 855 $msbSet = (bool) (ord($bytes[0]) & 0x80); 856 if (!$isNegative && $msbSet) { 857 $bytes = self::BOOL_FALSE . $bytes; 858 } elseif ($isNegative && !$msbSet) { 859 $bytes = self::BOOL_TRUE . $bytes; 860 } 861 862 return $bytes; 863 } 864 865 /** 866 * @param RealType $type 867 * @return string 868 * @throws EncoderException 869 */ 870 protected function encodeReal(RealType $type) 871 { 872 $real = $type->getValue(); 873 874 # If the value is zero, the contents are omitted 875 if ($real === ((float) 0)) { 876 return ''; 877 } 878 # If this is infinity, then a single octet of 0x40 is used. 879 if ($real === INF) { 880 return "\x40"; 881 } 882 # If this is negative infinity, then a single octet of 0x41 is used. 883 if ($real === -INF) { 884 return "\x41"; 885 } 886 887 // @todo Real type encoding/decoding is rather complex. Need to implement this yet. 888 throw new EncoderException('Real type encoding of this value not yet implemented.'); 889 } 890 891 /** 892 * @param int $length 893 * @return array 894 * @throws EncoderException 895 */ 896 protected function decodeGeneralizedTime($length): array 897 { 898 return $this->decodeTime('YmdH', GeneralizedTimeType::TIME_REGEX, GeneralizedTimeType::REGEX_MAP, $length); 899 } 900 901 /** 902 * @param int $length 903 * @return array 904 * @throws EncoderException 905 */ 906 protected function decodeUtcTime($length): array 907 { 908 return $this->decodeTime('ymdH', UtcTimeType::TIME_REGEX, UtcTimeType::REGEX_MAP, $length); 909 } 910 911 /** 912 * @param string $format 913 * @param string $regex 914 * @param array $matchMap 915 * @param int $length 916 * @return array 917 * @throws EncoderException 918 */ 919 protected function decodeTime(string $format, string $regex, array $matchMap, $length): array 920 { 921 $bytes = substr($this->binary, $this->pos, $length); 922 $this->pos += $length; 923 if (!preg_match($regex, $bytes, $matches)) { 924 throw new EncoderException('The datetime format is invalid and cannot be decoded.'); 925 } 926 if ($matches[$matchMap['hours']] === '24') { 927 throw new EncoderException('Midnight must only be specified by 00, but got 24.'); 928 } 929 $tzFormat = AbstractTimeType::TZ_LOCAL; 930 $dtFormat = AbstractTimeType::FORMAT_HOURS; 931 932 # Minutes 933 if (isset($matches[$matchMap['minutes']]) && $matches[$matchMap['minutes']] !== '') { 934 $dtFormat = AbstractTimeType::FORMAT_MINUTES; 935 $format .= 'i'; 936 } 937 # Seconds 938 if (isset($matches[$matchMap['seconds']]) && $matches[$matchMap['seconds']] !== '') { 939 $dtFormat = AbstractTimeType::FORMAT_SECONDS; 940 $format .= 's'; 941 } 942 # Fractions of a second 943 if (isset($matchMap['fractions']) && isset($matches[$matchMap['fractions']]) && $matches[$matchMap['fractions']] !== '') { 944 $dtFormat = AbstractTimeType::FORMAT_FRACTIONS; 945 $format .= '.u'; 946 } 947 # Timezone 948 if (isset($matches[$matchMap['timezone']]) && $matches[$matchMap['timezone']] !== '') { 949 $tzFormat = $matches[$matchMap['timezone']] === 'Z' ? AbstractTimeType::TZ_UTC : AbstractTimeType::TZ_DIFF; 950 $format .= 'T'; 951 } 952 $this->validateDateFormat($matches, $matchMap); 953 954 $dateTime = DateTime::createFromFormat($format, $bytes); 955 if ($dateTime === false) { 956 throw new EncoderException('Unable to decode time to a DateTime object.'); 957 } 958 $bytes = null; 959 960 return [$dateTime, $dtFormat, $tzFormat]; 961 } 962 963 /** 964 * Some encodings have specific restrictions. Allow them to override and validate this. 965 * 966 * @param array $matches 967 * @param array $matchMap 968 */ 969 protected function validateDateFormat(array $matches, array $matchMap) 970 { 971 } 972 973 /** 974 * @param int $length 975 * @return string 976 * @throws EncoderException 977 */ 978 protected function decodeOid($length): string 979 { 980 if ($length === 0) { 981 throw new EncoderException('Zero length not permitted for an OID type.'); 982 } 983 # We need to get the first part here, as it's used to determine the first 2 components. 984 $startedAt = $this->pos; 985 $firstPart = $this->getVlqBytesToInt(); 986 987 if ($firstPart < 80) { 988 $oid = floor($firstPart / 40) . '.' . ($firstPart % 40); 989 } else { 990 $isBigInt = ($firstPart > PHP_INT_MAX); 991 $this->throwIfBigIntGmpNeeded($isBigInt); 992 # In this case, the first identifier is always 2. 993 # But there is no limit on the value of the second identifier. 994 $oid = '2.' . ($isBigInt ? gmp_strval(gmp_sub($firstPart, '80')) : (int)$firstPart - 80); 995 } 996 997 # We could potentially have nothing left to decode at this point. 998 $oidLength = $length - ($this->pos - $startedAt); 999 $subIdentifiers = ($oidLength === 0) ? '' : '.' . $this->decodeRelativeOid($oidLength); 1000 1001 return $oid . $subIdentifiers; 1002 } 1003 1004 /** 1005 * @param int $length 1006 * @return string 1007 * @throws EncoderException 1008 */ 1009 protected function decodeRelativeOid($length): string 1010 { 1011 if ($length === 0) { 1012 throw new EncoderException('Zero length not permitted for an OID type.'); 1013 } 1014 $oid = ''; 1015 $endAt = $this->pos + $length; 1016 1017 while ($this->pos < $endAt) { 1018 $oid .= ($oid === '' ? '' : '.') . $this->getVlqBytesToInt(); 1019 } 1020 1021 return $oid; 1022 } 1023 1024 /** 1025 * @return bool 1026 */ 1027 protected function decodeBoolean(): bool 1028 { 1029 return ($this->binary[$this->pos++] !== self::BOOL_FALSE); 1030 } 1031 1032 /** 1033 * @param int $length 1034 * @return string 1035 * @throws EncoderException 1036 */ 1037 protected function decodeBitString($length): string 1038 { 1039 # The first byte represents the number of unused bits at the end. 1040 $unused = ord($this->binary[$this->pos++]); 1041 1042 if ($unused > 7) { 1043 throw new EncoderException(sprintf( 1044 'The unused bits in a bit string must be between 0 and 7, got: %s', 1045 $unused 1046 )); 1047 } 1048 if ($unused > 0 && $length < 1) { 1049 throw new EncoderException(sprintf( 1050 'If the bit string is empty the unused bits must be set to 0. However, it is set to %s with %s octets.', 1051 $unused, 1052 $length 1053 )); 1054 } 1055 $length--; 1056 1057 return $this->binaryToBitString($length, $unused); 1058 } 1059 1060 /** 1061 * @param int $length 1062 * @param int $unused 1063 * @return string 1064 */ 1065 protected function binaryToBitString(int $length, int $unused): string 1066 { 1067 $bitstring = ''; 1068 $endAt = $this->pos + $length; 1069 1070 for ($this->pos; $this->pos < $endAt; $this->pos++) { 1071 $octet = sprintf("%08d", decbin(ord($this->binary[$this->pos]))); 1072 if ($this->pos === ($endAt - 1) && $unused) { 1073 $bitstring .= substr($octet, 0, ($unused * -1)); 1074 } else { 1075 $bitstring .= $octet; 1076 } 1077 } 1078 1079 return $bitstring; 1080 } 1081 1082 /** 1083 * @param int $length 1084 * @return string|int number 1085 * @throws EncoderException 1086 */ 1087 protected function decodeInteger($length) 1088 { 1089 if ($length === 0) { 1090 throw new EncoderException('Zero length not permitted for an integer type.'); 1091 } 1092 $bytes = substr($this->binary, $this->pos, $length); 1093 $this->pos += $length; 1094 1095 $isNegative = (ord($bytes[0]) & 0x80); 1096 # Need to reverse Two's Complement. Invert the bits... 1097 if ($isNegative) { 1098 for ($i = 0; $i < $length; $i++) { 1099 $bytes[$i] = ~$bytes[$i]; 1100 } 1101 } 1102 $int = hexdec(bin2hex($bytes)); 1103 1104 $isBigInt = is_float($int); 1105 $this->throwIfBigIntGmpNeeded($isBigInt); 1106 if ($isBigInt) { 1107 $int = gmp_import($bytes); 1108 } 1109 $bytes = null; 1110 1111 # Complete Two's Complement by adding 1 and turning it negative... 1112 if ($isNegative) { 1113 $int = $isBigInt ? gmp_neg(gmp_add($int, '1')) : ($int + 1) * -1; 1114 } 1115 1116 return $isBigInt ? gmp_strval($int) : $int; 1117 } 1118 1119 /** 1120 * @param bool $isBigInt 1121 * @throws EncoderException 1122 */ 1123 protected function throwIfBigIntGmpNeeded(bool $isBigInt): void 1124 { 1125 if ($isBigInt && !$this->isGmpAvailable) { 1126 throw new EncoderException(sprintf( 1127 'An integer higher than PHP_INT_MAX int (%s) was encountered and the GMP extension is not loaded.', 1128 PHP_INT_MAX 1129 )); 1130 } 1131 } 1132 1133 /** 1134 * @param int $length 1135 * @return float 1136 * @throws EncoderException 1137 */ 1138 protected function decodeReal($length): float 1139 { 1140 if ($length === 0) { 1141 return 0; 1142 } 1143 1144 $ident = ord($this->binary[$this->pos++]); 1145 if ($ident === 0x40) { 1146 return INF; 1147 } 1148 if ($ident === 0x41) { 1149 return -INF; 1150 } 1151 1152 // @todo Real type encoding/decoding is rather complex. Need to implement this yet. 1153 throw new EncoderException('The real type encoding encountered is not yet implemented.'); 1154 } 1155 1156 /** 1157 * Encoding subsets may require specific ordering on set types. Allow this to be overridden. 1158 * 1159 * @param SetType $set 1160 * @return string 1161 * @throws EncoderException 1162 */ 1163 protected function encodeSet(SetType $set) 1164 { 1165 return $this->encodeConstructedType(...$set->getChildren()); 1166 } 1167 1168 /** 1169 * Encoding subsets may require specific ordering on set of types. Allow this to be overridden. 1170 * 1171 * @param SetOfType $set 1172 * @return string 1173 * @throws EncoderException 1174 */ 1175 protected function encodeSetOf(SetOfType $set) 1176 { 1177 return $this->encodeConstructedType(...$set->getChildren()); 1178 } 1179 1180 /** 1181 * @param AbstractType ...$types 1182 * @return string 1183 * @throws EncoderException 1184 */ 1185 protected function encodeConstructedType(AbstractType ...$types) 1186 { 1187 $bytes = ''; 1188 1189 foreach ($types as $type) { 1190 $bytes .= $this->encode($type); 1191 } 1192 1193 return $bytes; 1194 } 1195 1196 /** 1197 * @param int $length 1198 * @return array 1199 * @throws EncoderException 1200 * @throws PartialPduException 1201 */ 1202 protected function decodeConstructedType($length) 1203 { 1204 $children = []; 1205 $endAt = $this->pos + $length; 1206 1207 while ($this->pos < $endAt) { 1208 $children[] = $this->decodeBytes(); 1209 } 1210 1211 return $children; 1212 } 1213} 1214