<?php
 declare(strict_types=1); namespace SpomkyLabs\Pki\ASN1\Component; use Brick\Math\BigInteger; use SpomkyLabs\Pki\ASN1\Exception\DecodeException; use SpomkyLabs\Pki\ASN1\Feature\Encodable; use SpomkyLabs\Pki\ASN1\Util\BigInt; use function array_key_exists; use function mb_strlen; use function ord; final class Identifier implements Encodable { final public const CLASS_UNIVERSAL = 0b00; final public const CLASS_APPLICATION = 0b01; final public const CLASS_CONTEXT_SPECIFIC = 0b10; final public const CLASS_PRIVATE = 0b11; final public const PRIMITIVE = 0b0; final public const CONSTRUCTED = 0b1; private const MAP_CLASS_TO_NAME = [ self::CLASS_UNIVERSAL => 'UNIVERSAL', self::CLASS_APPLICATION => 'APPLICATION', self::CLASS_CONTEXT_SPECIFIC => 'CONTEXT SPECIFIC', self::CLASS_PRIVATE => 'PRIVATE', ]; private int $_class; private readonly int $_pc; private BigInt $_tag; private function __construct(int $class, int $pc, BigInteger|int $tag) { $this->_class = 0b11 & $class; $this->_pc = 0b1 & $pc; $this->_tag = BigInt::create($tag); } public static function create(int $class, int $pc, BigInteger|int $tag): self { return new self($class, $pc, $tag); } public static function fromDER(string $data, ?int &$offset = null): self { $idx = $offset ?? 0; $datalen = mb_strlen($data, '8bit'); if ($idx >= $datalen) { throw new DecodeException('Invalid offset.'); } $byte = ord($data[$idx++]); $class = (0b11000000 & $byte) >> 6; $pc = (0b00100000 & $byte) >> 5; $tag = (0b00011111 & $byte); if ($tag === 0x1f) { $tag = self::decodeLongFormTag($data, $idx); } if (isset($offset)) { $offset = $idx; } return self::create($class, $pc, $tag); } public function toDER(): string { $bytes = []; $byte = $this->_class << 6 | $this->_pc << 5; $tag = $this->_tag->getValue(); if ($tag->isLessThan(0x1f)) { $bytes[] = $byte | $tag->toInt(); } else { $bytes[] = $byte | 0x1f; $octets = []; for (; $tag->isGreaterThan(0); $tag = $tag->shiftedRight(7)) { $octets[] = 0x80 | $tag->and(0x7f)->toInt(); } $octets[0] &= 0x7f; foreach (array_reverse($octets) as $octet) { $bytes[] = $octet; } } return pack('C*', ...$bytes); } public function typeClass(): int { return $this->_class; } public function pc(): int { return $this->_pc; } public function tag(): string { return $this->_tag->base10(); } public function intTag(): int { return $this->_tag->toInt(); } public function isUniversal(): bool { return $this->_class === self::CLASS_UNIVERSAL; } public function isApplication(): bool { return $this->_class === self::CLASS_APPLICATION; } public function isContextSpecific(): bool { return $this->_class === self::CLASS_CONTEXT_SPECIFIC; } public function isPrivate(): bool { return $this->_class === self::CLASS_PRIVATE; } public function isPrimitive(): bool { return $this->_pc === self::PRIMITIVE; } public function isConstructed(): bool { return $this->_pc === self::CONSTRUCTED; } public function withClass(int $class): self { $obj = clone $this; $obj->_class = 0b11 & $class; return $obj; } public function withTag(int $tag): self { $obj = clone $this; $obj->_tag = BigInt::create($tag); return $obj; } public static function classToName(int $class): string { if (! array_key_exists($class, self::MAP_CLASS_TO_NAME)) { return "CLASS {$class}"; } return self::MAP_CLASS_TO_NAME[$class]; } private static function decodeLongFormTag(string $data, int &$offset): BigInteger { $datalen = mb_strlen($data, '8bit'); $tag = BigInteger::of(0); while (true) { if ($offset >= $datalen) { throw new DecodeException('Unexpected end of data while decoding long form identifier.'); } $byte = ord($data[$offset++]); $tag = $tag->shiftedLeft(7); $tag = $tag->or(0x7f & $byte); if ((0x80 & $byte) === 0) { break; } } return $tag; } } 