first commit

This commit is contained in:
2025-06-17 11:53:18 +02:00
commit 9f0f7ba12b
8804 changed files with 1369176 additions and 0 deletions

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Spomky-Labs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace CBOR;
use function chr;
use Stringable;
abstract class AbstractCBORObject implements CBORObject, Stringable
{
public function __construct(
private int $majorType,
protected int $additionalInformation
) {
}
public function __toString(): string
{
return chr($this->majorType << 5 | $this->additionalInformation);
}
public function getMajorType(): int
{
return $this->majorType;
}
public function getAdditionalInformation(): int
{
return $this->additionalInformation;
}
}

View File

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace CBOR;
/**
* @see \CBOR\Test\ByteStringObjectTest
*/
final class ByteStringObject extends AbstractCBORObject implements Normalizable
{
private const MAJOR_TYPE = self::MAJOR_TYPE_BYTE_STRING;
private string $value;
private ?string $length = null;
public function __construct(string $data)
{
[$additionalInformation, $length] = LengthCalculator::getLengthOfString($data);
parent::__construct(self::MAJOR_TYPE, $additionalInformation);
$this->length = $length;
$this->value = $data;
}
public function __toString(): string
{
$result = parent::__toString();
if ($this->length !== null) {
$result .= $this->length;
}
return $result . $this->value;
}
public static function create(string $data): self
{
return new self($data);
}
public function getValue(): string
{
return $this->value;
}
public function getLength(): int
{
return mb_strlen($this->value, '8bit');
}
public function normalize(): string
{
return $this->value;
}
}

View File

@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace CBOR;
interface CBORObject
{
public const MAJOR_TYPE_UNSIGNED_INTEGER = 0b000;
public const MAJOR_TYPE_NEGATIVE_INTEGER = 0b001;
public const MAJOR_TYPE_BYTE_STRING = 0b010;
public const MAJOR_TYPE_TEXT_STRING = 0b011;
public const MAJOR_TYPE_LIST = 0b100;
public const MAJOR_TYPE_MAP = 0b101;
public const MAJOR_TYPE_TAG = 0b110;
public const MAJOR_TYPE_OTHER_TYPE = 0b111;
public const LENGTH_1_BYTE = 0b00011000;
public const LENGTH_2_BYTES = 0b00011001;
public const LENGTH_4_BYTES = 0b00011010;
public const LENGTH_8_BYTES = 0b00011011;
public const LENGTH_INDEFINITE = 0b00011111;
public const FUTURE_USE_1 = 0b00011100;
public const FUTURE_USE_2 = 0b00011101;
public const FUTURE_USE_3 = 0b00011110;
public const OBJECT_FALSE = 20;
public const OBJECT_TRUE = 21;
public const OBJECT_NULL = 22;
public const OBJECT_UNDEFINED = 23;
public const OBJECT_SIMPLE_VALUE = 24;
public const OBJECT_HALF_PRECISION_FLOAT = 25;
public const OBJECT_SINGLE_PRECISION_FLOAT = 26;
public const OBJECT_DOUBLE_PRECISION_FLOAT = 27;
public const OBJECT_BREAK = 0b00011111;
public const TAG_STANDARD_DATETIME = 0;
public const TAG_EPOCH_DATETIME = 1;
public const TAG_UNSIGNED_BIG_NUM = 2;
public const TAG_NEGATIVE_BIG_NUM = 3;
public const TAG_DECIMAL_FRACTION = 4;
public const TAG_BIG_FLOAT = 5;
public const TAG_ENCODED_BASE64_URL = 21;
public const TAG_ENCODED_BASE64 = 22;
public const TAG_ENCODED_BASE16 = 23;
public const TAG_ENCODED_CBOR = 24;
public const TAG_URI = 32;
public const TAG_BASE64_URL = 33;
public const TAG_BASE64 = 34;
public const TAG_MIME = 36;
public const TAG_CBOR = 55799;
public function __toString(): string;
public function getMajorType(): int;
public function getAdditionalInformation(): int;
}

View File

@ -0,0 +1,239 @@
<?php
declare(strict_types=1);
namespace CBOR;
use CBOR\OtherObject\BreakObject;
use CBOR\OtherObject\DoublePrecisionFloatObject;
use CBOR\OtherObject\FalseObject;
use CBOR\OtherObject\HalfPrecisionFloatObject;
use CBOR\OtherObject\NullObject;
use CBOR\OtherObject\OtherObjectManager;
use CBOR\OtherObject\OtherObjectManagerInterface;
use CBOR\OtherObject\SimpleObject;
use CBOR\OtherObject\SinglePrecisionFloatObject;
use CBOR\OtherObject\TrueObject;
use CBOR\OtherObject\UndefinedObject;
use CBOR\Tag\Base16EncodingTag;
use CBOR\Tag\Base64EncodingTag;
use CBOR\Tag\Base64Tag;
use CBOR\Tag\Base64UrlEncodingTag;
use CBOR\Tag\Base64UrlTag;
use CBOR\Tag\BigFloatTag;
use CBOR\Tag\CBOREncodingTag;
use CBOR\Tag\CBORTag;
use CBOR\Tag\DatetimeTag;
use CBOR\Tag\DecimalFractionTag;
use CBOR\Tag\MimeTag;
use CBOR\Tag\NegativeBigIntegerTag;
use CBOR\Tag\TagManager;
use CBOR\Tag\TagManagerInterface;
use CBOR\Tag\TimestampTag;
use CBOR\Tag\UnsignedBigIntegerTag;
use CBOR\Tag\UriTag;
use InvalidArgumentException;
use function ord;
use RuntimeException;
use const STR_PAD_LEFT;
final class Decoder implements DecoderInterface
{
private TagManagerInterface $tagObjectManager;
private OtherObjectManagerInterface $otherTypeManager;
public function __construct(
?TagManagerInterface $tagObjectManager = null,
?OtherObjectManagerInterface $otherTypeManager = null
) {
$this->tagObjectManager = $tagObjectManager ?? $this->generateTagManager();
$this->otherTypeManager = $otherTypeManager ?? $this->generateOtherObjectManager();
}
public static function create(
?TagManagerInterface $tagObjectManager = null,
?OtherObjectManagerInterface $otherTypeManager = null
): self {
return new self($tagObjectManager, $otherTypeManager);
}
public function decode(Stream $stream): CBORObject
{
return $this->process($stream, false);
}
private function process(Stream $stream, bool $breakable): CBORObject
{
$ib = ord($stream->read(1));
$mt = $ib >> 5;
$ai = $ib & 0b00011111;
$val = null;
switch ($ai) {
case CBORObject::LENGTH_1_BYTE: //24
case CBORObject::LENGTH_2_BYTES: //25
case CBORObject::LENGTH_4_BYTES: //26
case CBORObject::LENGTH_8_BYTES: //27
$val = $stream->read(2 ** ($ai & 0b00000111));
break;
case CBORObject::FUTURE_USE_1: //28
case CBORObject::FUTURE_USE_2: //29
case CBORObject::FUTURE_USE_3: //30
throw new InvalidArgumentException(sprintf(
'Cannot parse the data. Found invalid Additional Information "%s" (%d).',
str_pad(decbin($ai), 8, '0', STR_PAD_LEFT),
$ai
));
case CBORObject::LENGTH_INDEFINITE: //31
return $this->processInfinite($stream, $mt, $breakable);
}
return $this->processFinite($stream, $mt, $ai, $val);
}
private function processFinite(Stream $stream, int $mt, int $ai, ?string $val): CBORObject
{
switch ($mt) {
case CBORObject::MAJOR_TYPE_UNSIGNED_INTEGER: //0
return UnsignedIntegerObject::createObjectForValue($ai, $val);
case CBORObject::MAJOR_TYPE_NEGATIVE_INTEGER: //1
return NegativeIntegerObject::createObjectForValue($ai, $val);
case CBORObject::MAJOR_TYPE_BYTE_STRING: //2
$length = $val === null ? $ai : Utils::binToInt($val);
return ByteStringObject::create($stream->read($length));
case CBORObject::MAJOR_TYPE_TEXT_STRING: //3
$length = $val === null ? $ai : Utils::binToInt($val);
return TextStringObject::create($stream->read($length));
case CBORObject::MAJOR_TYPE_LIST: //4
$object = ListObject::create();
$nbItems = $val === null ? $ai : Utils::binToInt($val);
for ($i = 0; $i < $nbItems; ++$i) {
$object->add($this->process($stream, false));
}
return $object;
case CBORObject::MAJOR_TYPE_MAP: //5
$object = MapObject::create();
$nbItems = $val === null ? $ai : Utils::binToInt($val);
for ($i = 0; $i < $nbItems; ++$i) {
$object->add($this->process($stream, false), $this->process($stream, false));
}
return $object;
case CBORObject::MAJOR_TYPE_TAG: //6
return $this->tagObjectManager->createObjectForValue($ai, $val, $this->process($stream, false));
case CBORObject::MAJOR_TYPE_OTHER_TYPE: //7
return $this->otherTypeManager->createObjectForValue($ai, $val);
default:
throw new RuntimeException(sprintf(
'Unsupported major type "%s" (%d).',
str_pad(decbin($mt), 5, '0', STR_PAD_LEFT),
$mt
)); // Should never append
}
}
private function processInfinite(Stream $stream, int $mt, bool $breakable): CBORObject
{
switch ($mt) {
case CBORObject::MAJOR_TYPE_BYTE_STRING: //2
$object = IndefiniteLengthByteStringObject::create();
while (! ($it = $this->process($stream, true)) instanceof BreakObject) {
if (! $it instanceof ByteStringObject) {
throw new RuntimeException(
'Unable to parse the data. Infinite Byte String object can only get Byte String objects.'
);
}
$object->add($it);
}
return $object;
case CBORObject::MAJOR_TYPE_TEXT_STRING: //3
$object = IndefiniteLengthTextStringObject::create();
while (! ($it = $this->process($stream, true)) instanceof BreakObject) {
if (! $it instanceof TextStringObject) {
throw new RuntimeException(
'Unable to parse the data. Infinite Text String object can only get Text String objects.'
);
}
$object->add($it);
}
return $object;
case CBORObject::MAJOR_TYPE_LIST: //4
$object = IndefiniteLengthListObject::create();
$it = $this->process($stream, true);
while (! $it instanceof BreakObject) {
$object->add($it);
$it = $this->process($stream, true);
}
return $object;
case CBORObject::MAJOR_TYPE_MAP: //5
$object = IndefiniteLengthMapObject::create();
while (! ($it = $this->process($stream, true)) instanceof BreakObject) {
$object->add($it, $this->process($stream, false));
}
return $object;
case CBORObject::MAJOR_TYPE_OTHER_TYPE: //7
if (! $breakable) {
throw new InvalidArgumentException('Cannot parse the data. No enclosing indefinite.');
}
return BreakObject::create();
case CBORObject::MAJOR_TYPE_UNSIGNED_INTEGER: //0
case CBORObject::MAJOR_TYPE_NEGATIVE_INTEGER: //1
case CBORObject::MAJOR_TYPE_TAG: //6
default:
throw new InvalidArgumentException(sprintf(
'Cannot parse the data. Found infinite length for Major Type "%s" (%d).',
str_pad(decbin($mt), 5, '0', STR_PAD_LEFT),
$mt
));
}
}
private function generateTagManager(): TagManagerInterface
{
return TagManager::create()
->add(DatetimeTag::class)
->add(TimestampTag::class)
->add(UnsignedBigIntegerTag::class)
->add(NegativeBigIntegerTag::class)
->add(DecimalFractionTag::class)
->add(BigFloatTag::class)
->add(Base64UrlEncodingTag::class)
->add(Base64EncodingTag::class)
->add(Base16EncodingTag::class)
->add(CBOREncodingTag::class)
->add(UriTag::class)
->add(Base64UrlTag::class)
->add(Base64Tag::class)
->add(MimeTag::class)
->add(CBORTag::class)
;
}
private function generateOtherObjectManager(): OtherObjectManagerInterface
{
return OtherObjectManager::create()
->add(BreakObject::class)
->add(SimpleObject::class)
->add(FalseObject::class)
->add(TrueObject::class)
->add(NullObject::class)
->add(UndefinedObject::class)
->add(HalfPrecisionFloatObject::class)
->add(SinglePrecisionFloatObject::class)
->add(DoublePrecisionFloatObject::class)
;
}
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace CBOR;
interface DecoderInterface
{
public function decode(Stream $stream): CBORObject;
}

View File

@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace CBOR;
/**
* @see \CBOR\Test\IndefiniteLengthByteStringObjectTest
*/
final class IndefiniteLengthByteStringObject extends AbstractCBORObject implements Normalizable
{
private const MAJOR_TYPE = self::MAJOR_TYPE_BYTE_STRING;
private const ADDITIONAL_INFORMATION = self::LENGTH_INDEFINITE;
/**
* @var ByteStringObject[]
*/
private array $chunks = [];
public function __construct()
{
parent::__construct(self::MAJOR_TYPE, self::ADDITIONAL_INFORMATION);
}
public function __toString(): string
{
$result = parent::__toString();
foreach ($this->chunks as $chunk) {
$result .= $chunk->__toString();
}
return $result . "\xFF";
}
public static function create(): self
{
return new self();
}
public function add(ByteStringObject $chunk): self
{
$this->chunks[] = $chunk;
return $this;
}
public function append(string $chunk): self
{
$this->add(ByteStringObject::create($chunk));
return $this;
}
public function getValue(): string
{
$result = '';
foreach ($this->chunks as $chunk) {
$result .= $chunk->getValue();
}
return $result;
}
public function getLength(): int
{
$length = 0;
foreach ($this->chunks as $chunk) {
$length += $chunk->getLength();
}
return $length;
}
public function normalize(): string
{
$result = '';
foreach ($this->chunks as $chunk) {
$result .= $chunk->normalize();
}
return $result;
}
}

View File

@ -0,0 +1,137 @@
<?php
declare(strict_types=1);
namespace CBOR;
use function array_key_exists;
use ArrayAccess;
use ArrayIterator;
use InvalidArgumentException;
use Iterator;
use IteratorAggregate;
/**
* @phpstan-implements ArrayAccess<int, CBORObject>
* @phpstan-implements IteratorAggregate<int, CBORObject>
* @final
*/
class IndefiniteLengthListObject extends AbstractCBORObject implements IteratorAggregate, Normalizable, ArrayAccess
{
private const MAJOR_TYPE = self::MAJOR_TYPE_LIST;
private const ADDITIONAL_INFORMATION = self::LENGTH_INDEFINITE;
/**
* @var CBORObject[]
*/
private array $data = [];
public function __construct()
{
parent::__construct(self::MAJOR_TYPE, self::ADDITIONAL_INFORMATION);
}
public function __toString(): string
{
$result = parent::__toString();
foreach ($this->data as $object) {
$result .= (string) $object;
}
return $result . "\xFF";
}
public static function create(): self
{
return new self();
}
/**
* @return mixed[]
*/
public function normalize(): array
{
return array_map(
static fn (CBORObject $object) => $object instanceof Normalizable ? $object->normalize() : $object,
$this->data
);
}
public function add(CBORObject $item): self
{
$this->data[] = $item;
return $this;
}
public function has(int $index): bool
{
return array_key_exists($index, $this->data);
}
public function remove(int $index): self
{
if (! $this->has($index)) {
return $this;
}
unset($this->data[$index]);
$this->data = array_values($this->data);
return $this;
}
public function get(int $index): CBORObject
{
if (! $this->has($index)) {
throw new InvalidArgumentException('Index not found.');
}
return $this->data[$index];
}
public function set(int $index, CBORObject $object): self
{
if (! $this->has($index)) {
throw new InvalidArgumentException('Index not found.');
}
$this->data[$index] = $object;
return $this;
}
/**
* @return Iterator<int, CBORObject>
*/
public function getIterator(): Iterator
{
return new ArrayIterator($this->data);
}
public function offsetExists($offset): bool
{
return $this->has($offset);
}
public function offsetGet($offset): CBORObject
{
return $this->get($offset);
}
public function offsetSet($offset, $value): void
{
if ($offset === null) {
$this->add($value);
return;
}
$this->set($offset, $value);
}
public function offsetUnset($offset): void
{
$this->remove($offset);
}
}

View File

@ -0,0 +1,149 @@
<?php
declare(strict_types=1);
namespace CBOR;
use function array_key_exists;
use ArrayAccess;
use ArrayIterator;
use InvalidArgumentException;
use Iterator;
use IteratorAggregate;
/**
* @phpstan-implements ArrayAccess<int, CBORObject>
* @phpstan-implements IteratorAggregate<int, MapItem>
* @final
*/
class IndefiniteLengthMapObject extends AbstractCBORObject implements IteratorAggregate, Normalizable, ArrayAccess
{
private const MAJOR_TYPE = self::MAJOR_TYPE_MAP;
private const ADDITIONAL_INFORMATION = self::LENGTH_INDEFINITE;
/**
* @var MapItem[]
*/
private array $data = [];
public function __construct()
{
parent::__construct(self::MAJOR_TYPE, self::ADDITIONAL_INFORMATION);
}
public function __toString(): string
{
$result = parent::__toString();
foreach ($this->data as $object) {
$result .= (string) $object->getKey();
$result .= (string) $object->getValue();
}
return $result . "\xFF";
}
public static function create(): self
{
return new self();
}
public function add(CBORObject $key, CBORObject $value): self
{
if (! $key instanceof Normalizable) {
throw new InvalidArgumentException('Invalid key. Shall be normalizable');
}
$this->data[$key->normalize()] = MapItem::create($key, $value);
return $this;
}
public function has(int|string $key): bool
{
return array_key_exists($key, $this->data);
}
public function remove(int|string $index): self
{
if (! $this->has($index)) {
return $this;
}
unset($this->data[$index]);
$this->data = array_values($this->data);
return $this;
}
public function get(int|string $index): CBORObject
{
if (! $this->has($index)) {
throw new InvalidArgumentException('Index not found.');
}
return $this->data[$index]->getValue();
}
public function set(MapItem $object): self
{
$key = $object->getKey();
if (! $key instanceof Normalizable) {
throw new InvalidArgumentException('Invalid key. Shall be normalizable');
}
$this->data[$key->normalize()] = $object;
return $this;
}
/**
* @return Iterator<int, MapItem>
*/
public function getIterator(): Iterator
{
return new ArrayIterator($this->data);
}
/**
* @return mixed[]
*/
public function normalize(): array
{
return array_reduce($this->data, static function (array $carry, MapItem $item): array {
$key = $item->getKey();
if (! $key instanceof Normalizable) {
throw new InvalidArgumentException('Invalid key. Shall be normalizable');
}
$valueObject = $item->getValue();
$carry[$key->normalize()] = $valueObject instanceof Normalizable ? $valueObject->normalize() : $valueObject;
return $carry;
}, []);
}
public function offsetExists($offset): bool
{
return $this->has($offset);
}
public function offsetGet($offset): CBORObject
{
return $this->get($offset);
}
public function offsetSet($offset, $value): void
{
if (! $offset instanceof CBORObject) {
throw new InvalidArgumentException('Invalid key');
}
if (! $value instanceof CBORObject) {
throw new InvalidArgumentException('Invalid value');
}
$this->set(MapItem::create($offset, $value));
}
public function offsetUnset($offset): void
{
$this->remove($offset);
}
}

View File

@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace CBOR;
/**
* @see \CBOR\Test\IndefiniteLengthTextStringObjectTest
*/
final class IndefiniteLengthTextStringObject extends AbstractCBORObject implements Normalizable
{
private const MAJOR_TYPE = self::MAJOR_TYPE_TEXT_STRING;
private const ADDITIONAL_INFORMATION = self::LENGTH_INDEFINITE;
/**
* @var TextStringObject[]
*/
private array $data = [];
public function __construct()
{
parent::__construct(self::MAJOR_TYPE, self::ADDITIONAL_INFORMATION);
}
public function __toString(): string
{
$result = parent::__toString();
foreach ($this->data as $object) {
$result .= (string) $object;
}
return $result . "\xFF";
}
public static function create(): self
{
return new self();
}
public function add(TextStringObject $chunk): self
{
$this->data[] = $chunk;
return $this;
}
public function append(string $chunk): self
{
$this->add(TextStringObject::create($chunk));
return $this;
}
public function getValue(): string
{
$result = '';
foreach ($this->data as $object) {
$result .= $object->getValue();
}
return $result;
}
public function getLength(): int
{
$length = 0;
foreach ($this->data as $object) {
$length += $object->getLength();
}
return $length;
}
public function normalize(): string
{
$result = '';
foreach ($this->data as $object) {
$result .= $object->normalize();
}
return $result;
}
}

View File

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace CBOR;
use Brick\Math\BigInteger;
use function chr;
use function count;
use InvalidArgumentException;
use const STR_PAD_LEFT;
final class LengthCalculator
{
/**
* @return array{int, null|string}
*/
public static function getLengthOfString(string $data): array
{
$length = mb_strlen($data, '8bit');
return self::computeLength($length);
}
/**
* @param array<int|string, mixed> $data
*
* @return array{int, null|string}
*/
public static function getLengthOfArray(array $data): array
{
$length = count($data);
return self::computeLength($length);
}
/**
* @return array{int, null|string}
*/
private static function computeLength(int $length): array
{
return match (true) {
$length <= 23 => [$length, null],
$length <= 0xFF => [24, chr($length)],
$length <= 0xFFFF => [25, self::hex2bin(dechex($length))],
$length <= 0xFFFFFFFF => [26, self::hex2bin(dechex($length))],
BigInteger::of($length)->isLessThan(BigInteger::fromBase('FFFFFFFFFFFFFFFF', 16)) => [
27,
self::hex2bin(dechex($length)),
],
default => [31, null],
};
}
private static function hex2bin(string $data): string
{
$data = str_pad($data, (int) (2 ** ceil(log(mb_strlen($data, '8bit'), 2))), '0', STR_PAD_LEFT);
$result = hex2bin($data);
if ($result === false) {
throw new InvalidArgumentException('Unable to convert the data');
}
return $result;
}
}

View File

@ -0,0 +1,165 @@
<?php
declare(strict_types=1);
namespace CBOR;
use function array_key_exists;
use ArrayAccess;
use ArrayIterator;
use function count;
use Countable;
use InvalidArgumentException;
use Iterator;
use IteratorAggregate;
/**
* @phpstan-implements ArrayAccess<int, CBORObject>
* @phpstan-implements IteratorAggregate<int, CBORObject>
* @see \CBOR\Test\ListObjectTest
*/
class ListObject extends AbstractCBORObject implements Countable, IteratorAggregate, Normalizable, ArrayAccess
{
private const MAJOR_TYPE = self::MAJOR_TYPE_LIST;
/**
* @var CBORObject[]
*/
private array $data;
private ?string $length = null;
/**
* @param CBORObject[] $data
*/
public function __construct(array $data = [])
{
[$additionalInformation, $length] = LengthCalculator::getLengthOfArray($data);
array_map(static function ($item): void {
if (! $item instanceof CBORObject) {
throw new InvalidArgumentException('The list must contain only CBORObject objects.');
}
}, $data);
parent::__construct(self::MAJOR_TYPE, $additionalInformation);
$this->data = array_values($data);
$this->length = $length;
}
public function __toString(): string
{
$result = parent::__toString();
if ($this->length !== null) {
$result .= $this->length;
}
foreach ($this->data as $object) {
$result .= (string) $object;
}
return $result;
}
/**
* @param CBORObject[] $data
*/
public static function create(array $data = []): self
{
return new self($data);
}
public function add(CBORObject $object): self
{
$this->data[] = $object;
[$this->additionalInformation, $this->length] = LengthCalculator::getLengthOfArray($this->data);
return $this;
}
public function has(int $index): bool
{
return array_key_exists($index, $this->data);
}
public function remove(int $index): self
{
if (! $this->has($index)) {
return $this;
}
unset($this->data[$index]);
$this->data = array_values($this->data);
[$this->additionalInformation, $this->length] = LengthCalculator::getLengthOfArray($this->data);
return $this;
}
public function get(int $index): CBORObject
{
if (! $this->has($index)) {
throw new InvalidArgumentException('Index not found.');
}
return $this->data[$index];
}
public function set(int $index, CBORObject $object): self
{
if (! $this->has($index)) {
throw new InvalidArgumentException('Index not found.');
}
$this->data[$index] = $object;
[$this->additionalInformation, $this->length] = LengthCalculator::getLengthOfArray($this->data);
return $this;
}
/**
* @return array<int, mixed>
*/
public function normalize(): array
{
return array_map(
static fn (CBORObject $object) => $object instanceof Normalizable ? $object->normalize() : $object,
$this->data
);
}
public function count(): int
{
return count($this->data);
}
/**
* @return Iterator<int, CBORObject>
*/
public function getIterator(): Iterator
{
return new ArrayIterator($this->data);
}
public function offsetExists($offset): bool
{
return $this->has($offset);
}
public function offsetGet($offset): CBORObject
{
return $this->get($offset);
}
public function offsetSet($offset, $value): void
{
if ($offset === null) {
$this->add($value);
return;
}
$this->set($offset, $value);
}
public function offsetUnset($offset): void
{
$this->remove($offset);
}
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace CBOR;
class MapItem
{
public function __construct(
private CBORObject $key,
private CBORObject $value
) {
}
public static function create(CBORObject $key, CBORObject $value): self
{
return new self($key, $value);
}
public function getKey(): CBORObject
{
return $this->key;
}
public function getValue(): CBORObject
{
return $this->value;
}
}

View File

@ -0,0 +1,180 @@
<?php
declare(strict_types=1);
namespace CBOR;
use function array_key_exists;
use ArrayAccess;
use ArrayIterator;
use function count;
use Countable;
use InvalidArgumentException;
use Iterator;
use IteratorAggregate;
/**
* @phpstan-implements ArrayAccess<int, CBORObject>
* @phpstan-implements IteratorAggregate<int, MapItem>
*/
final class MapObject extends AbstractCBORObject implements Countable, IteratorAggregate, Normalizable, ArrayAccess
{
private const MAJOR_TYPE = self::MAJOR_TYPE_MAP;
/**
* @var MapItem[]
*/
private array $data;
private ?string $length = null;
/**
* @param MapItem[] $data
*/
public function __construct(array $data = [])
{
[$additionalInformation, $length] = LengthCalculator::getLengthOfArray($data);
array_map(static function ($item): void {
if (! $item instanceof MapItem) {
throw new InvalidArgumentException('The list must contain only MapItem objects.');
}
}, $data);
parent::__construct(self::MAJOR_TYPE, $additionalInformation);
$this->data = $data;
$this->length = $length;
}
public function __toString(): string
{
$result = parent::__toString();
if ($this->length !== null) {
$result .= $this->length;
}
foreach ($this->data as $object) {
$result .= $object->getKey()
->__toString()
;
$result .= $object->getValue()
->__toString()
;
}
return $result;
}
/**
* @param MapItem[] $data
*/
public static function create(array $data = []): self
{
return new self($data);
}
public function add(CBORObject $key, CBORObject $value): self
{
if (! $key instanceof Normalizable) {
throw new InvalidArgumentException('Invalid key. Shall be normalizable');
}
$this->data[$key->normalize()] = MapItem::create($key, $value);
[$this->additionalInformation, $this->length] = LengthCalculator::getLengthOfArray($this->data);
return $this;
}
public function has(int|string $key): bool
{
return array_key_exists($key, $this->data);
}
public function remove(int|string $index): self
{
if (! $this->has($index)) {
return $this;
}
unset($this->data[$index]);
$this->data = array_values($this->data);
[$this->additionalInformation, $this->length] = LengthCalculator::getLengthOfArray($this->data);
return $this;
}
public function get(int|string $index): CBORObject
{
if (! $this->has($index)) {
throw new InvalidArgumentException('Index not found.');
}
return $this->data[$index]->getValue();
}
public function set(MapItem $object): self
{
$key = $object->getKey();
if (! $key instanceof Normalizable) {
throw new InvalidArgumentException('Invalid key. Shall be normalizable');
}
$this->data[$key->normalize()] = $object;
[$this->additionalInformation, $this->length] = LengthCalculator::getLengthOfArray($this->data);
return $this;
}
public function count(): int
{
return count($this->data);
}
/**
* @return Iterator<int, MapItem>
*/
public function getIterator(): Iterator
{
return new ArrayIterator($this->data);
}
/**
* @return array<int|string, mixed>
*/
public function normalize(): array
{
return array_reduce($this->data, static function (array $carry, MapItem $item): array {
$key = $item->getKey();
if (! $key instanceof Normalizable) {
throw new InvalidArgumentException('Invalid key. Shall be normalizable');
}
$valueObject = $item->getValue();
$carry[$key->normalize()] = $valueObject instanceof Normalizable ? $valueObject->normalize() : $valueObject;
return $carry;
}, []);
}
public function offsetExists($offset): bool
{
return $this->has($offset);
}
public function offsetGet($offset): CBORObject
{
return $this->get($offset);
}
public function offsetSet($offset, $value): void
{
if (! $offset instanceof CBORObject) {
throw new InvalidArgumentException('Invalid key');
}
if (! $value instanceof CBORObject) {
throw new InvalidArgumentException('Invalid value');
}
$this->set(MapItem::create($offset, $value));
}
public function offsetUnset($offset): void
{
$this->remove($offset);
}
}

View File

@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace CBOR;
use Brick\Math\BigInteger;
use InvalidArgumentException;
use const STR_PAD_LEFT;
final class NegativeIntegerObject extends AbstractCBORObject implements Normalizable
{
private const MAJOR_TYPE = self::MAJOR_TYPE_NEGATIVE_INTEGER;
public function __construct(
int $additionalInformation,
private ?string $data
) {
parent::__construct(self::MAJOR_TYPE, $additionalInformation);
}
public function __toString(): string
{
$result = parent::__toString();
if ($this->data !== null) {
$result .= $this->data;
}
return $result;
}
public static function createObjectForValue(int $additionalInformation, ?string $data): self
{
return new self($additionalInformation, $data);
}
public static function create(int $value): self
{
return self::createFromString((string) $value);
}
public static function createFromString(string $value): self
{
$integer = BigInteger::of($value);
return self::createBigInteger($integer);
}
public function getValue(): string
{
if ($this->data === null) {
return (string) (-1 - $this->additionalInformation);
}
$result = Utils::binToBigInteger($this->data);
$minusOne = BigInteger::of(-1);
return $minusOne->minus($result)
->toBase(10)
;
}
public function normalize(): string
{
return $this->getValue();
}
private static function createBigInteger(BigInteger $integer): self
{
if ($integer->isGreaterThanOrEqualTo(BigInteger::zero())) {
throw new InvalidArgumentException('The value must be a negative integer.');
}
$minusOne = BigInteger::of(-1);
$computed_value = $minusOne->minus($integer);
switch (true) {
case $computed_value->isLessThan(BigInteger::of(24)):
$ai = $computed_value->toInt();
$data = null;
break;
case $computed_value->isLessThan(BigInteger::fromBase('FF', 16)):
$ai = 24;
$data = self::hex2bin(str_pad($computed_value->toBase(16), 2, '0', STR_PAD_LEFT));
break;
case $computed_value->isLessThan(BigInteger::fromBase('FFFF', 16)):
$ai = 25;
$data = self::hex2bin(str_pad($computed_value->toBase(16), 4, '0', STR_PAD_LEFT));
break;
case $computed_value->isLessThan(BigInteger::fromBase('FFFFFFFF', 16)):
$ai = 26;
$data = self::hex2bin(str_pad($computed_value->toBase(16), 8, '0', STR_PAD_LEFT));
break;
default:
throw new InvalidArgumentException(
'Out of range. Please use NegativeBigIntegerTag tag with ByteStringObject object instead.'
);
}
return new self($ai, $data);
}
private static function hex2bin(string $data): string
{
$result = hex2bin($data);
if ($result === false) {
throw new InvalidArgumentException('Unable to convert the data');
}
return $result;
}
}

View File

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace CBOR;
interface Normalizable
{
/**
* @return mixed|null
*/
public function normalize();
}

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace CBOR;
use CBOR\OtherObject\OtherObjectInterface;
abstract class OtherObject extends AbstractCBORObject implements OtherObjectInterface
{
private const MAJOR_TYPE = self::MAJOR_TYPE_OTHER_TYPE;
public function __construct(
int $additionalInformation,
protected ?string $data
) {
parent::__construct(self::MAJOR_TYPE, $additionalInformation);
}
public function __toString(): string
{
$result = parent::__toString();
if ($this->data !== null) {
$result .= $this->data;
}
return $result;
}
public function getContent(): ?string
{
return $this->data;
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace CBOR\OtherObject;
use CBOR\OtherObject as Base;
final class BreakObject extends Base
{
public function __construct()
{
parent::__construct(self::OBJECT_BREAK, null);
}
public static function create(): self
{
return new self();
}
public static function supportedAdditionalInformation(): array
{
return [self::OBJECT_BREAK];
}
public static function createFromLoadedData(int $additionalInformation, ?string $data): Base
{
return new self();
}
}

View File

@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace CBOR\OtherObject;
use Brick\Math\BigInteger;
use CBOR\Normalizable;
use CBOR\OtherObject as Base;
use CBOR\Utils;
use const INF;
use InvalidArgumentException;
use const NAN;
final class DoublePrecisionFloatObject extends Base implements Normalizable
{
public static function supportedAdditionalInformation(): array
{
return [self::OBJECT_DOUBLE_PRECISION_FLOAT];
}
public static function createFromLoadedData(int $additionalInformation, ?string $data): Base
{
return new self($additionalInformation, $data);
}
public static function create(string $value): self
{
if (mb_strlen($value, '8bit') !== 8) {
throw new InvalidArgumentException('The value is not a valid double precision floating point');
}
return new self(self::OBJECT_DOUBLE_PRECISION_FLOAT, $value);
}
public function normalize(): float|int
{
$exponent = $this->getExponent();
$mantissa = $this->getMantissa();
$sign = $this->getSign();
if ($exponent === 0) {
$val = $mantissa * 2 ** (-(1022 + 52));
} elseif ($exponent !== 0b11111111111) {
$val = ($mantissa + (1 << 52)) * 2 ** ($exponent - (1023 + 52));
} else {
$val = $mantissa === 0 ? INF : NAN;
}
return $sign * $val;
}
public function getExponent(): int
{
$data = $this->data;
Utils::assertString($data, 'Invalid data');
return Utils::binToBigInteger($data)->shiftedRight(52)->and(Utils::hexToBigInteger('7ff'))->toInt();
}
public function getMantissa(): int
{
$data = $this->data;
Utils::assertString($data, 'Invalid data');
return Utils::binToBigInteger($data)->and(Utils::hexToBigInteger('fffffffffffff'))->toInt();
}
public function getSign(): int
{
$data = $this->data;
Utils::assertString($data, 'Invalid data');
$sign = Utils::binToBigInteger($data)->shiftedRight(63);
return $sign->isEqualTo(BigInteger::one()) ? -1 : 1;
}
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace CBOR\OtherObject;
use CBOR\Normalizable;
use CBOR\OtherObject as Base;
final class FalseObject extends Base implements Normalizable
{
public function __construct()
{
parent::__construct(self::OBJECT_FALSE, null);
}
public static function create(): self
{
return new self();
}
public static function supportedAdditionalInformation(): array
{
return [self::OBJECT_FALSE];
}
public static function createFromLoadedData(int $additionalInformation, ?string $data): Base
{
return new self();
}
public function normalize(): bool
{
return false;
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace CBOR\OtherObject;
use CBOR\OtherObject as Base;
use InvalidArgumentException;
use function ord;
final class GenericObject extends Base
{
public static function supportedAdditionalInformation(): array
{
return [];
}
public static function createFromLoadedData(int $additionalInformation, ?string $data): Base
{
if ($data !== null && ord($data) < 32) {
throw new InvalidArgumentException('Invalid simple value. Content data should not be present.');
}
return new self($additionalInformation, $data);
}
}

View File

@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace CBOR\OtherObject;
use Brick\Math\BigInteger;
use CBOR\Normalizable;
use CBOR\OtherObject as Base;
use CBOR\Utils;
use const INF;
use InvalidArgumentException;
use const NAN;
final class HalfPrecisionFloatObject extends Base implements Normalizable
{
public static function supportedAdditionalInformation(): array
{
return [self::OBJECT_HALF_PRECISION_FLOAT];
}
public static function createFromLoadedData(int $additionalInformation, ?string $data): Base
{
return new self($additionalInformation, $data);
}
public static function create(string $value): self
{
if (mb_strlen($value, '8bit') !== 2) {
throw new InvalidArgumentException('The value is not a valid half precision floating point');
}
return new self(self::OBJECT_HALF_PRECISION_FLOAT, $value);
}
public function normalize(): float|int
{
$exponent = $this->getExponent();
$mantissa = $this->getMantissa();
$sign = $this->getSign();
if ($exponent === 0) {
$val = $mantissa * 2 ** (-24);
} elseif ($exponent !== 0b11111) {
$val = ($mantissa + (1 << 10)) * 2 ** ($exponent - 25);
} else {
$val = $mantissa === 0 ? INF : NAN;
}
return $sign * $val;
}
public function getExponent(): int
{
$data = $this->data;
Utils::assertString($data, 'Invalid data');
return Utils::binToBigInteger($data)->shiftedRight(10)->and(Utils::hexToBigInteger('1f'))->toInt();
}
public function getMantissa(): int
{
$data = $this->data;
Utils::assertString($data, 'Invalid data');
return Utils::binToBigInteger($data)->and(Utils::hexToBigInteger('3ff'))->toInt();
}
public function getSign(): int
{
$data = $this->data;
Utils::assertString($data, 'Invalid data');
$sign = Utils::binToBigInteger($data)->shiftedRight(15);
return $sign->isEqualTo(BigInteger::one()) ? -1 : 1;
}
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace CBOR\OtherObject;
use CBOR\Normalizable;
use CBOR\OtherObject as Base;
final class NullObject extends Base implements Normalizable
{
public function __construct()
{
parent::__construct(self::OBJECT_NULL, null);
}
public static function create(): self
{
return new self();
}
public static function supportedAdditionalInformation(): array
{
return [self::OBJECT_NULL];
}
public static function createFromLoadedData(int $additionalInformation, ?string $data): Base
{
return new self();
}
public function normalize(): ?string
{
return null;
}
}

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace CBOR\OtherObject;
use CBOR\CBORObject;
interface OtherObjectInterface extends CBORObject
{
/**
* @return int[]
*/
public static function supportedAdditionalInformation(): array;
public static function createFromLoadedData(int $additionalInformation, ?string $data): self;
}

View File

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace CBOR\OtherObject;
use function array_key_exists;
use CBOR\OtherObject;
use InvalidArgumentException;
class OtherObjectManager implements OtherObjectManagerInterface
{
/**
* @var string[]
*/
private array $classes = [];
public static function create(): self
{
return new self();
}
public function add(string $class): self
{
foreach ($class::supportedAdditionalInformation() as $ai) {
if ($ai < 0) {
throw new InvalidArgumentException('Invalid additional information.');
}
$this->classes[$ai] = $class;
}
return $this;
}
public function getClassForValue(int $value): string
{
return array_key_exists($value, $this->classes) ? $this->classes[$value] : GenericObject::class;
}
public function createObjectForValue(int $value, ?string $data): OtherObjectInterface
{
/** @var OtherObject $class */
$class = $this->getClassForValue($value);
return $class::createFromLoadedData($value, $data);
}
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace CBOR\OtherObject;
interface OtherObjectManagerInterface
{
public function createObjectForValue(int $value, ?string $data): OtherObjectInterface;
}

View File

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace CBOR\OtherObject;
use CBOR\Normalizable;
use CBOR\OtherObject as Base;
use CBOR\Utils;
use function chr;
use InvalidArgumentException;
use function ord;
final class SimpleObject extends Base implements Normalizable
{
public static function supportedAdditionalInformation(): array
{
return array_merge(range(0, 19), [24]);
}
public static function create(int $value): self|FalseObject|TrueObject|NullObject|UndefinedObject
{
switch (true) {
case $value >= 0 && $value <= 19:
return new self($value, null);
case $value === 20:
return FalseObject::create();
case $value === 21:
return TrueObject::create();
case $value === 22:
return NullObject::create();
case $value === 23:
return UndefinedObject::create();
case $value <= 31:
throw new InvalidArgumentException('Invalid simple value. Shall be between 32 and 255.');
case $value <= 255:
return new self(24, chr($value));
default:
throw new InvalidArgumentException('The value is not a valid simple value.');
}
}
public static function createFromLoadedData(int $additionalInformation, ?string $data): Base
{
if ($additionalInformation === 24) {
if ($data === null) {
throw new InvalidArgumentException('Invalid simple value. Content data is missing.');
}
if (mb_strlen($data, '8bit') !== 1) {
throw new InvalidArgumentException('Invalid simple value. Content data is too long.');
}
if (ord($data) < 32) {
throw new InvalidArgumentException('Invalid simple value. Content data must be between 32 and 255.');
}
} elseif ($additionalInformation < 20) {
if ($data !== null) {
throw new InvalidArgumentException('Invalid simple value. Content data should not be present.');
}
}
return new self($additionalInformation, $data);
}
public function normalize(): int
{
if ($this->data === null) {
return $this->getAdditionalInformation();
}
return Utils::binToInt($this->data);
}
}

View File

@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace CBOR\OtherObject;
use Brick\Math\BigInteger;
use CBOR\OtherObject as Base;
use CBOR\Utils;
use const INF;
use InvalidArgumentException;
use const NAN;
final class SinglePrecisionFloatObject extends Base
{
public static function supportedAdditionalInformation(): array
{
return [self::OBJECT_SINGLE_PRECISION_FLOAT];
}
public static function createFromLoadedData(int $additionalInformation, ?string $data): Base
{
return new self($additionalInformation, $data);
}
public static function create(string $value): self
{
if (mb_strlen($value, '8bit') !== 4) {
throw new InvalidArgumentException('The value is not a valid single precision floating point');
}
return new self(self::OBJECT_SINGLE_PRECISION_FLOAT, $value);
}
public function normalize(): float|int
{
$exponent = $this->getExponent();
$mantissa = $this->getMantissa();
$sign = $this->getSign();
if ($exponent === 0) {
$val = $mantissa * 2 ** (-(126 + 23));
} elseif ($exponent !== 0b11111111) {
$val = ($mantissa + (1 << 23)) * 2 ** ($exponent - (127 + 23));
} else {
$val = $mantissa === 0 ? INF : NAN;
}
return $sign * $val;
}
public function getExponent(): int
{
$data = $this->data;
Utils::assertString($data, 'Invalid data');
return Utils::binToBigInteger($data)->shiftedRight(23)->and(Utils::hexToBigInteger('ff'))->toInt();
}
public function getMantissa(): int
{
$data = $this->data;
Utils::assertString($data, 'Invalid data');
return Utils::binToBigInteger($data)->and(Utils::hexToBigInteger('7fffff'))->toInt();
}
public function getSign(): int
{
$data = $this->data;
Utils::assertString($data, 'Invalid data');
$sign = Utils::binToBigInteger($data)->shiftedRight(31);
return $sign->isEqualTo(BigInteger::one()) ? -1 : 1;
}
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace CBOR\OtherObject;
use CBOR\Normalizable;
use CBOR\OtherObject as Base;
final class TrueObject extends Base implements Normalizable
{
public function __construct()
{
parent::__construct(self::OBJECT_TRUE, null);
}
public static function create(): self
{
return new self();
}
public static function supportedAdditionalInformation(): array
{
return [self::OBJECT_TRUE];
}
public static function createFromLoadedData(int $additionalInformation, ?string $data): Base
{
return new self();
}
public function normalize(): bool
{
return true;
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace CBOR\OtherObject;
use CBOR\OtherObject as Base;
final class UndefinedObject extends Base
{
public function __construct()
{
parent::__construct(self::OBJECT_UNDEFINED, null);
}
public static function create(): self
{
return new self();
}
public static function supportedAdditionalInformation(): array
{
return [self::OBJECT_UNDEFINED];
}
public static function createFromLoadedData(int $additionalInformation, ?string $data): Base
{
return new self();
}
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace CBOR;
interface Stream
{
public function read(int $length): string;
}

View File

@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace CBOR;
use InvalidArgumentException;
use RuntimeException;
final class StringStream implements Stream
{
/**
* @var resource
*/
private $resource;
public function __construct(string $data)
{
$resource = fopen('php://memory', 'rb+');
if ($resource === false) {
throw new RuntimeException('Unable to open the memory');
}
$result = fwrite($resource, $data);
if ($result === false) {
throw new RuntimeException('Unable to write the memory');
}
$result = rewind($resource);
if ($result === false) {
throw new RuntimeException('Unable to rewind the memory');
}
$this->resource = $resource;
}
public static function create(string $data): self
{
return new self($data);
}
public function read(int $length): string
{
if ($length === 0) {
return '';
}
$alreadyRead = 0;
$data = '';
while ($alreadyRead < $length) {
$left = $length - $alreadyRead;
$sizeToRead = $left < 1024 && $left > 0 ? $left : 1024;
$newData = fread($this->resource, $sizeToRead);
$alreadyRead += $sizeToRead;
if ($newData === false) {
throw new RuntimeException('Unable to read the memory');
}
if (mb_strlen($newData, '8bit') < $sizeToRead) {
throw new InvalidArgumentException(sprintf(
'Out of range. Expected: %d, read: %d.',
$length,
mb_strlen($data, '8bit')
));
}
$data .= $newData;
}
if (mb_strlen($data, '8bit') !== $length) {
throw new InvalidArgumentException(sprintf(
'Out of range. Expected: %d, read: %d.',
$length,
mb_strlen($data, '8bit')
));
}
return $data;
}
}

View File

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace CBOR;
use CBOR\Tag\TagInterface;
use InvalidArgumentException;
abstract class Tag extends AbstractCBORObject implements TagInterface
{
private const MAJOR_TYPE = self::MAJOR_TYPE_TAG;
public function __construct(
int $additionalInformation,
protected ?string $data,
protected CBORObject $object
) {
parent::__construct(self::MAJOR_TYPE, $additionalInformation);
}
public function __toString(): string
{
$result = parent::__toString();
if ($this->data !== null) {
$result .= $this->data;
}
return $result . $this->object;
}
public function getData(): ?string
{
return $this->data;
}
public function getValue(): CBORObject
{
return $this->object;
}
/**
* @return array{int, null|string}
*/
protected static function determineComponents(int $tag): array
{
switch (true) {
case $tag < 0:
throw new InvalidArgumentException('The value must be a positive integer.');
case $tag < 24:
return [$tag, null];
case $tag < 0xFF:
return [24, self::hex2bin(dechex($tag))];
case $tag < 0xFFFF:
return [25, self::hex2bin(dechex($tag))];
case $tag < 0xFFFFFFFF:
return [26, self::hex2bin(dechex($tag))];
default:
throw new InvalidArgumentException(
'Out of range. Please use PositiveBigIntegerTag tag with ByteStringObject object instead.'
);
}
}
private static function hex2bin(string $data): string
{
$result = hex2bin($data);
if ($result === false) {
throw new InvalidArgumentException('Unable to convert the data');
}
return $result;
}
}

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace CBOR\Tag;
use CBOR\CBORObject;
use CBOR\Tag;
final class Base16EncodingTag extends Tag
{
public static function getTagId(): int
{
return self::TAG_ENCODED_BASE16;
}
public static function createFromLoadedData(int $additionalInformation, ?string $data, CBORObject $object): Tag
{
return new self($additionalInformation, $data, $object);
}
public static function create(CBORObject $object): Tag
{
[$ai, $data] = self::determineComponents(self::TAG_ENCODED_BASE16);
return new self($ai, $data, $object);
}
}

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace CBOR\Tag;
use CBOR\CBORObject;
use CBOR\Tag;
final class Base64EncodingTag extends Tag
{
public static function getTagId(): int
{
return self::TAG_ENCODED_BASE64;
}
public static function createFromLoadedData(int $additionalInformation, ?string $data, CBORObject $object): Tag
{
return new self($additionalInformation, $data, $object);
}
public static function create(CBORObject $object): Tag
{
[$ai, $data] = self::determineComponents(self::TAG_ENCODED_BASE64);
return new self($ai, $data, $object);
}
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace CBOR\Tag;
use CBOR\CBORObject;
use CBOR\IndefiniteLengthTextStringObject;
use CBOR\Tag;
use CBOR\TextStringObject;
use InvalidArgumentException;
final class Base64Tag extends Tag
{
public function __construct(int $additionalInformation, ?string $data, CBORObject $object)
{
if (! $object instanceof TextStringObject && ! $object instanceof IndefiniteLengthTextStringObject) {
throw new InvalidArgumentException('This tag only accepts a Text String object.');
}
parent::__construct($additionalInformation, $data, $object);
}
public static function getTagId(): int
{
return self::TAG_BASE64;
}
public static function createFromLoadedData(int $additionalInformation, ?string $data, CBORObject $object): Tag
{
return new self($additionalInformation, $data, $object);
}
public static function create(CBORObject $object): Tag
{
[$ai, $data] = self::determineComponents(self::TAG_BASE64);
return new self($ai, $data, $object);
}
}

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace CBOR\Tag;
use CBOR\CBORObject;
use CBOR\Tag;
final class Base64UrlEncodingTag extends Tag
{
public static function getTagId(): int
{
return self::TAG_ENCODED_BASE64_URL;
}
public static function createFromLoadedData(int $additionalInformation, ?string $data, CBORObject $object): Tag
{
return new self($additionalInformation, $data, $object);
}
public static function create(CBORObject $object): Tag
{
[$ai, $data] = self::determineComponents(self::TAG_ENCODED_BASE64_URL);
return new self($ai, $data, $object);
}
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace CBOR\Tag;
use CBOR\CBORObject;
use CBOR\IndefiniteLengthTextStringObject;
use CBOR\Tag;
use CBOR\TextStringObject;
use InvalidArgumentException;
final class Base64UrlTag extends Tag
{
public function __construct(int $additionalInformation, ?string $data, CBORObject $object)
{
if (! $object instanceof TextStringObject && ! $object instanceof IndefiniteLengthTextStringObject) {
throw new InvalidArgumentException('This tag only accepts a Text String object.');
}
parent::__construct($additionalInformation, $data, $object);
}
public static function getTagId(): int
{
return self::TAG_BASE64_URL;
}
public static function createFromLoadedData(int $additionalInformation, ?string $data, CBORObject $object): Tag
{
return new self($additionalInformation, $data, $object);
}
public static function create(CBORObject $object): Tag
{
[$ai, $data] = self::determineComponents(self::TAG_BASE64_URL);
return new self($ai, $data, $object);
}
}

View File

@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace CBOR\Tag;
use CBOR\CBORObject;
use CBOR\ListObject;
use CBOR\NegativeIntegerObject;
use CBOR\Normalizable;
use CBOR\Tag;
use CBOR\UnsignedIntegerObject;
use function count;
use function extension_loaded;
use InvalidArgumentException;
use RuntimeException;
final class BigFloatTag extends Tag implements Normalizable
{
public function __construct(int $additionalInformation, ?string $data, CBORObject $object)
{
if (! extension_loaded('bcmath')) {
throw new RuntimeException('The extension "bcmath" is required to use this tag');
}
if (! $object instanceof ListObject || count($object) !== 2) {
throw new InvalidArgumentException(
'This tag only accepts a ListObject object that contains an exponent and a mantissa.'
);
}
$e = $object->get(0);
if (! $e instanceof UnsignedIntegerObject && ! $e instanceof NegativeIntegerObject) {
throw new InvalidArgumentException('The exponent must be a Signed Integer or an Unsigned Integer object.');
}
$m = $object->get(1);
if (! $m instanceof UnsignedIntegerObject && ! $m instanceof NegativeIntegerObject && ! $m instanceof NegativeBigIntegerTag && ! $m instanceof UnsignedBigIntegerTag) {
throw new InvalidArgumentException(
'The mantissa must be a Positive or Negative Signed Integer or an Unsigned Integer object.'
);
}
parent::__construct($additionalInformation, $data, $object);
}
public static function getTagId(): int
{
return self::TAG_BIG_FLOAT;
}
public static function createFromLoadedData(int $additionalInformation, ?string $data, CBORObject $object): Tag
{
return new self($additionalInformation, $data, $object);
}
public static function create(CBORObject $object): Tag
{
[$ai, $data] = self::determineComponents(self::TAG_BIG_FLOAT);
return new self($ai, $data, $object);
}
public static function createFromExponentAndMantissa(CBORObject $e, CBORObject $m): Tag
{
$object = ListObject::create()
->add($e)
->add($m)
;
return self::create($object);
}
public function normalize()
{
/** @var ListObject $object */
$object = $this->object;
/** @var UnsignedIntegerObject|NegativeIntegerObject $e */
$e = $object->get(0);
/** @var UnsignedIntegerObject|NegativeIntegerObject|NegativeBigIntegerTag|UnsignedBigIntegerTag $m */
$m = $object->get(1);
return rtrim(bcmul($m->normalize(), bcpow('2', $e->normalize(), 100), 100), '0');
}
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace CBOR\Tag;
use CBOR\ByteStringObject;
use CBOR\CBORObject;
use CBOR\IndefiniteLengthByteStringObject;
use CBOR\Tag;
use InvalidArgumentException;
final class CBOREncodingTag extends Tag
{
public function __construct(int $additionalInformation, ?string $data, CBORObject $object)
{
if (! $object instanceof ByteStringObject && ! $object instanceof IndefiniteLengthByteStringObject) {
throw new InvalidArgumentException('This tag only accepts a Byte String object.');
}
parent::__construct($additionalInformation, $data, $object);
}
public static function getTagId(): int
{
return self::TAG_ENCODED_CBOR;
}
public static function createFromLoadedData(int $additionalInformation, ?string $data, CBORObject $object): Tag
{
return new self($additionalInformation, $data, $object);
}
public static function create(CBORObject $object): Tag
{
[$ai, $data] = self::determineComponents(self::TAG_ENCODED_CBOR);
return new self($ai, $data, $object);
}
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace CBOR\Tag;
use CBOR\CBORObject;
use CBOR\Normalizable;
use CBOR\Tag;
final class CBORTag extends Tag implements Normalizable
{
public static function getTagId(): int
{
return self::TAG_CBOR;
}
public static function createFromLoadedData(int $additionalInformation, ?string $data, CBORObject $object): Tag
{
return new self($additionalInformation, $data, $object);
}
public static function create(CBORObject $object): Tag
{
[$ai, $data] = self::determineComponents(self::TAG_CBOR);
return new self($ai, $data, $object);
}
/**
* @return mixed|CBORObject|null
*/
public function normalize()
{
return $this->object instanceof Normalizable ? $this->object->normalize() : $this->object;
}
}

View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace CBOR\Tag;
use CBOR\CBORObject;
use CBOR\IndefiniteLengthTextStringObject;
use CBOR\Normalizable;
use CBOR\Tag;
use CBOR\TextStringObject;
use const DATE_RFC3339;
use DateTimeImmutable;
use DateTimeInterface;
use InvalidArgumentException;
/**
* @see \CBOR\Test\Tag\DatetimeTagTest
*/
final class DatetimeTag extends Tag implements Normalizable
{
public function __construct(int $additionalInformation, ?string $data, CBORObject $object)
{
if (! $object instanceof TextStringObject && ! $object instanceof IndefiniteLengthTextStringObject) {
throw new InvalidArgumentException('This tag only accepts a Byte String object.');
}
parent::__construct($additionalInformation, $data, $object);
}
public static function getTagId(): int
{
return self::TAG_STANDARD_DATETIME;
}
public static function createFromLoadedData(int $additionalInformation, ?string $data, CBORObject $object): Tag
{
return new self($additionalInformation, $data, $object);
}
public static function create(CBORObject $object): Tag
{
[$ai, $data] = self::determineComponents(self::TAG_STANDARD_DATETIME);
return new self($ai, $data, $object);
}
public function normalize(): DateTimeInterface
{
/** @var TextStringObject|IndefiniteLengthTextStringObject $object */
$object = $this->object;
$result = DateTimeImmutable::createFromFormat(DATE_RFC3339, $object->normalize());
if ($result !== false) {
return $result;
}
$formatted = DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.uP', $object->normalize());
if ($formatted === false) {
throw new InvalidArgumentException('Invalid data. Cannot be converted into a datetime object');
}
return $formatted;
}
}

View File

@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace CBOR\Tag;
use CBOR\CBORObject;
use CBOR\ListObject;
use CBOR\NegativeIntegerObject;
use CBOR\Normalizable;
use CBOR\Tag;
use CBOR\UnsignedIntegerObject;
use function count;
use function extension_loaded;
use InvalidArgumentException;
use RuntimeException;
final class DecimalFractionTag extends Tag implements Normalizable
{
public function __construct(int $additionalInformation, ?string $data, CBORObject $object)
{
if (! extension_loaded('bcmath')) {
throw new RuntimeException('The extension "bcmath" is required to use this tag');
}
if (! $object instanceof ListObject || count($object) !== 2) {
throw new InvalidArgumentException(
'This tag only accepts a ListObject object that contains an exponent and a mantissa.'
);
}
$e = $object->get(0);
if (! $e instanceof UnsignedIntegerObject && ! $e instanceof NegativeIntegerObject) {
throw new InvalidArgumentException('The exponent must be a Signed Integer or an Unsigned Integer object.');
}
$m = $object->get(1);
if (! $m instanceof UnsignedIntegerObject && ! $m instanceof NegativeIntegerObject && ! $m instanceof NegativeBigIntegerTag && ! $m instanceof UnsignedBigIntegerTag) {
throw new InvalidArgumentException(
'The mantissa must be a Positive or Negative Signed Integer or an Unsigned Integer object.'
);
}
parent::__construct($additionalInformation, $data, $object);
}
public static function create(CBORObject $object): self
{
[$ai, $data] = self::determineComponents(self::TAG_DECIMAL_FRACTION);
return new self($ai, $data, $object);
}
public static function getTagId(): int
{
return self::TAG_DECIMAL_FRACTION;
}
public static function createFromLoadedData(int $additionalInformation, ?string $data, CBORObject $object): Tag
{
return new self($additionalInformation, $data, $object);
}
public static function createFromExponentAndMantissa(CBORObject $e, CBORObject $m): Tag
{
$object = ListObject::create()
->add($e)
->add($m)
;
return self::create($object);
}
public function normalize()
{
/** @var ListObject $object */
$object = $this->object;
/** @var UnsignedIntegerObject|NegativeIntegerObject $e */
$e = $object->get(0);
/** @var UnsignedIntegerObject|NegativeIntegerObject|NegativeBigIntegerTag|UnsignedBigIntegerTag $m */
$m = $object->get(1);
return rtrim(bcmul($m->normalize(), bcpow('10', $e->normalize(), 100), 100), '0');
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace CBOR\Tag;
use CBOR\CBORObject;
use CBOR\Tag;
final class GenericTag extends Tag
{
public static function getTagId(): int
{
return -1;
}
public static function createFromLoadedData(int $additionalInformation, ?string $data, CBORObject $object): Tag
{
return new self($additionalInformation, $data, $object);
}
}

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace CBOR\Tag;
use CBOR\CBORObject;
use CBOR\IndefiniteLengthTextStringObject;
use CBOR\Normalizable;
use CBOR\Tag;
use CBOR\TextStringObject;
use InvalidArgumentException;
/**
* @see \CBOR\Test\Tag\MimeTagTest
*/
final class MimeTag extends Tag implements Normalizable
{
public function __construct(int $additionalInformation, ?string $data, CBORObject $object)
{
if (! $object instanceof TextStringObject && ! $object instanceof IndefiniteLengthTextStringObject) {
throw new InvalidArgumentException('This tag only accepts a Byte String object.');
}
parent::__construct($additionalInformation, $data, $object);
}
public static function getTagId(): int
{
return self::TAG_MIME;
}
public static function createFromLoadedData(int $additionalInformation, ?string $data, CBORObject $object): Tag
{
return new self($additionalInformation, $data, $object);
}
public static function create(CBORObject $object): Tag
{
[$ai, $data] = self::determineComponents(self::TAG_MIME);
return new self($ai, $data, $object);
}
public function normalize(): string
{
/** @var TextStringObject|IndefiniteLengthTextStringObject $object */
$object = $this->object;
return $object->normalize();
}
}

View File

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace CBOR\Tag;
use Brick\Math\BigInteger;
use CBOR\ByteStringObject;
use CBOR\CBORObject;
use CBOR\IndefiniteLengthByteStringObject;
use CBOR\Normalizable;
use CBOR\Tag;
use InvalidArgumentException;
final class NegativeBigIntegerTag extends Tag implements Normalizable
{
public function __construct(int $additionalInformation, ?string $data, CBORObject $object)
{
if (! $object instanceof ByteStringObject && ! $object instanceof IndefiniteLengthByteStringObject) {
throw new InvalidArgumentException('This tag only accepts a Byte String object.');
}
parent::__construct($additionalInformation, $data, $object);
}
public static function getTagId(): int
{
return self::TAG_NEGATIVE_BIG_NUM;
}
public static function createFromLoadedData(int $additionalInformation, ?string $data, CBORObject $object): Tag
{
return new self($additionalInformation, $data, $object);
}
public static function create(CBORObject $object): Tag
{
[$ai, $data] = self::determineComponents(self::TAG_NEGATIVE_BIG_NUM);
return new self($ai, $data, $object);
}
public function normalize(): string
{
/** @var ByteStringObject|IndefiniteLengthByteStringObject $object */
$object = $this->object;
$integer = BigInteger::fromBase(bin2hex($object->getValue()), 16);
$minusOne = BigInteger::of(-1);
return $minusOne->minus($integer)
->toBase(10)
;
}
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace CBOR\Tag;
use CBOR\CBORObject;
interface TagInterface extends CBORObject
{
public static function getTagId(): int;
public function getValue(): CBORObject;
public static function createFromLoadedData(
int $additionalInformation,
?string $data,
CBORObject $object
): self;
}

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace CBOR\Tag;
use function array_key_exists;
use CBOR\CBORObject;
use CBOR\Tag;
use CBOR\Utils;
use InvalidArgumentException;
final class TagManager implements TagManagerInterface
{
/**
* @var string[]
*/
private array $classes = [];
public static function create(): self
{
return new self();
}
public function add(string $class): self
{
if ($class::getTagId() < 0) {
throw new InvalidArgumentException('Invalid tag ID.');
}
$this->classes[$class::getTagId()] = $class;
return $this;
}
public function getClassForValue(int $value): string
{
return array_key_exists($value, $this->classes) ? $this->classes[$value] : GenericTag::class;
}
public function createObjectForValue(int $additionalInformation, ?string $data, CBORObject $object): TagInterface
{
$value = $additionalInformation;
if ($additionalInformation >= 24) {
Utils::assertString($data, 'Invalid data');
$value = Utils::binToInt($data);
}
/** @var Tag $class */
$class = $this->getClassForValue($value);
return $class::createFromLoadedData($additionalInformation, $data, $object);
}
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace CBOR\Tag;
use CBOR\CBORObject;
interface TagManagerInterface
{
public function createObjectForValue(int $additionalInformation, ?string $data, CBORObject $object): TagInterface;
}

View File

@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace CBOR\Tag;
use CBOR\CBORObject;
use CBOR\NegativeIntegerObject;
use CBOR\Normalizable;
use CBOR\OtherObject\DoublePrecisionFloatObject;
use CBOR\OtherObject\HalfPrecisionFloatObject;
use CBOR\OtherObject\SinglePrecisionFloatObject;
use CBOR\Tag;
use CBOR\UnsignedIntegerObject;
use DateTimeImmutable;
use DateTimeInterface;
use InvalidArgumentException;
use const STR_PAD_RIGHT;
final class TimestampTag extends Tag implements Normalizable
{
public function __construct(int $additionalInformation, ?string $data, CBORObject $object)
{
if (! $object instanceof UnsignedIntegerObject && ! $object instanceof NegativeIntegerObject && ! $object instanceof HalfPrecisionFloatObject && ! $object instanceof SinglePrecisionFloatObject && ! $object instanceof DoublePrecisionFloatObject) {
throw new InvalidArgumentException('This tag only accepts integer-based or float-based objects.');
}
parent::__construct($additionalInformation, $data, $object);
}
public static function getTagId(): int
{
return self::TAG_EPOCH_DATETIME;
}
public static function createFromLoadedData(int $additionalInformation, ?string $data, CBORObject $object): Tag
{
return new self($additionalInformation, $data, $object);
}
public static function create(CBORObject $object): Tag
{
[$ai, $data] = self::determineComponents(self::TAG_EPOCH_DATETIME);
return new self($ai, $data, $object);
}
public function normalize(): DateTimeInterface
{
$object = $this->object;
switch (true) {
case $object instanceof UnsignedIntegerObject:
case $object instanceof NegativeIntegerObject:
$formatted = DateTimeImmutable::createFromFormat('U', $object->normalize());
break;
case $object instanceof HalfPrecisionFloatObject:
case $object instanceof SinglePrecisionFloatObject:
case $object instanceof DoublePrecisionFloatObject:
$value = (string) $object->normalize();
$parts = explode('.', $value);
if (isset($parts[1])) {
if (mb_strlen($parts[1], '8bit') > 6) {
$parts[1] = mb_substr($parts[1], 0, 6, '8bit');
} else {
$parts[1] = str_pad($parts[1], 6, '0', STR_PAD_RIGHT);
}
}
$formatted = DateTimeImmutable::createFromFormat('U.u', implode('.', $parts));
break;
default:
throw new InvalidArgumentException('Unable to normalize the object');
}
if ($formatted === false) {
throw new InvalidArgumentException('Invalid data. Cannot be converted into a datetime object');
}
return $formatted;
}
}

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace CBOR\Tag;
use CBOR\ByteStringObject;
use CBOR\CBORObject;
use CBOR\IndefiniteLengthByteStringObject;
use CBOR\Normalizable;
use CBOR\Tag;
use CBOR\Utils;
use InvalidArgumentException;
final class UnsignedBigIntegerTag extends Tag implements Normalizable
{
public function __construct(int $additionalInformation, ?string $data, CBORObject $object)
{
if (! $object instanceof ByteStringObject && ! $object instanceof IndefiniteLengthByteStringObject) {
throw new InvalidArgumentException('This tag only accepts a Byte String object.');
}
parent::__construct($additionalInformation, $data, $object);
}
public static function getTagId(): int
{
return self::TAG_UNSIGNED_BIG_NUM;
}
public static function createFromLoadedData(int $additionalInformation, ?string $data, CBORObject $object): Tag
{
return new self($additionalInformation, $data, $object);
}
public static function create(CBORObject $object): Tag
{
[$ai, $data] = self::determineComponents(self::TAG_UNSIGNED_BIG_NUM);
return new self($ai, $data, $object);
}
public function normalize(): string
{
/** @var ByteStringObject|IndefiniteLengthByteStringObject $object */
$object = $this->object;
return Utils::hexToString($object->normalize());
}
}

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace CBOR\Tag;
use CBOR\CBORObject;
use CBOR\IndefiniteLengthTextStringObject;
use CBOR\Normalizable;
use CBOR\Tag;
use CBOR\TextStringObject;
use InvalidArgumentException;
final class UriTag extends Tag implements Normalizable
{
public function __construct(int $additionalInformation, ?string $data, CBORObject $object)
{
if (! $object instanceof TextStringObject && ! $object instanceof IndefiniteLengthTextStringObject) {
throw new InvalidArgumentException('This tag only accepts a Text String object.');
}
parent::__construct($additionalInformation, $data, $object);
}
public static function getTagId(): int
{
return self::TAG_URI;
}
public static function createFromLoadedData(int $additionalInformation, ?string $data, CBORObject $object): Tag
{
return new self($additionalInformation, $data, $object);
}
public static function create(CBORObject $object): Tag
{
[$ai, $data] = self::determineComponents(self::TAG_URI);
return new self($ai, $data, $object);
}
public function normalize(): string
{
/** @var TextStringObject|IndefiniteLengthTextStringObject $object */
$object = $this->object;
return $object->normalize();
}
}

View File

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace CBOR;
/**
* @see \CBOR\Test\TextStringObjectTest
*/
final class TextStringObject extends AbstractCBORObject implements Normalizable
{
private const MAJOR_TYPE = self::MAJOR_TYPE_TEXT_STRING;
private ?string $length = null;
private string $data;
public function __construct(string $data)
{
[$additionalInformation, $length] = LengthCalculator::getLengthOfString($data);
parent::__construct(self::MAJOR_TYPE, $additionalInformation);
$this->data = $data;
$this->length = $length;
}
public function __toString(): string
{
$result = parent::__toString();
if ($this->length !== null) {
$result .= $this->length;
}
return $result . $this->data;
}
public static function create(string $data): self
{
return new self($data);
}
public function getValue(): string
{
return $this->data;
}
public function getLength(): int
{
return mb_strlen($this->data, 'utf8');
}
public function normalize(): string
{
return $this->data;
}
}

View File

@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace CBOR;
use Brick\Math\BigInteger;
use InvalidArgumentException;
use const STR_PAD_LEFT;
final class UnsignedIntegerObject extends AbstractCBORObject implements Normalizable
{
private const MAJOR_TYPE = self::MAJOR_TYPE_UNSIGNED_INTEGER;
public function __construct(
int $additionalInformation,
private ?string $data
) {
parent::__construct(self::MAJOR_TYPE, $additionalInformation);
}
public function __toString(): string
{
$result = parent::__toString();
if ($this->data !== null) {
$result .= $this->data;
}
return $result;
}
public static function createObjectForValue(int $additionalInformation, ?string $data): self
{
return new self($additionalInformation, $data);
}
public static function create(int $value): self
{
return self::createFromString((string) $value);
}
public static function createFromHex(string $value): self
{
$integer = BigInteger::fromBase($value, 16);
return self::createBigInteger($integer);
}
public static function createFromString(string $value): self
{
$integer = BigInteger::of($value);
return self::createBigInteger($integer);
}
public function getMajorType(): int
{
return self::MAJOR_TYPE;
}
public function getValue(): string
{
if ($this->data === null) {
return (string) $this->additionalInformation;
}
$integer = BigInteger::fromBase(bin2hex($this->data), 16);
return $integer->toBase(10);
}
public function normalize(): string
{
return $this->getValue();
}
private static function createBigInteger(BigInteger $integer): self
{
if ($integer->isLessThan(BigInteger::zero())) {
throw new InvalidArgumentException('The value must be a positive integer.');
}
switch (true) {
case $integer->isLessThan(BigInteger::of(24)):
$ai = $integer->toInt();
$data = null;
break;
case $integer->isLessThan(BigInteger::fromBase('FF', 16)):
$ai = 24;
$data = self::hex2bin(str_pad($integer->toBase(16), 2, '0', STR_PAD_LEFT));
break;
case $integer->isLessThan(BigInteger::fromBase('FFFF', 16)):
$ai = 25;
$data = self::hex2bin(str_pad($integer->toBase(16), 4, '0', STR_PAD_LEFT));
break;
case $integer->isLessThan(BigInteger::fromBase('FFFFFFFF', 16)):
$ai = 26;
$data = self::hex2bin(str_pad($integer->toBase(16), 8, '0', STR_PAD_LEFT));
break;
default:
throw new InvalidArgumentException(
'Out of range. Please use PositiveBigIntegerTag tag with ByteStringObject object instead.'
);
}
return new self($ai, $data);
}
private static function hex2bin(string $data): string
{
$result = hex2bin($data);
if ($result === false) {
throw new InvalidArgumentException('Unable to convert the data');
}
return $result;
}
}

View File

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace CBOR;
use Brick\Math\BigInteger;
use InvalidArgumentException;
use function is_string;
/**
* @internal
*/
abstract class Utils
{
public static function binToInt(string $value): int
{
return self::binToBigInteger($value)->toInt();
}
public static function binToBigInteger(string $value): BigInteger
{
return self::hexToBigInteger(bin2hex($value));
}
public static function hexToInt(string $value): int
{
return self::hexToBigInteger($value)->toInt();
}
public static function hexToBigInteger(string $value): BigInteger
{
return BigInteger::fromBase($value, 16);
}
public static function hexToString(string $value): string
{
return BigInteger::fromBase(bin2hex($value), 16)->toBase(10);
}
public static function decode(string $data): string
{
$decoded = base64_decode(strtr($data, '-_', '+/'), true);
if ($decoded === false) {
throw new InvalidArgumentException('Invalid data provided');
}
return $decoded;
}
/**
* @param mixed|null $data
*/
public static function assertString($data, ?string $message = null): void
{
if (! is_string($data)) {
throw new InvalidArgumentException($message ?? '');
}
}
}

View File

@ -0,0 +1,22 @@
MIT License
Copyright (c) 2016-2019 Joni Eskelinen
Copyright (c) 2022 Spomky-Labs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,277 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Component;
use function array_key_exists;
use Brick\Math\BigInteger;
use function mb_strlen;
use function ord;
use SpomkyLabs\Pki\ASN1\Exception\DecodeException;
use SpomkyLabs\Pki\ASN1\Feature\Encodable;
use SpomkyLabs\Pki\ASN1\Util\BigInt;
/**
* Class to represent BER/DER identifier octets.
*/
final class Identifier implements Encodable
{
// Type class enumerations
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;
// P/C enumerations
final public const PRIMITIVE = 0b0;
final public const CONSTRUCTED = 0b1;
/**
* Mapping from type class to human readable name.
*
* @internal
*
* @var array<int, string>
*/
private const MAP_CLASS_TO_NAME = [
self::CLASS_UNIVERSAL => 'UNIVERSAL',
self::CLASS_APPLICATION => 'APPLICATION',
self::CLASS_CONTEXT_SPECIFIC => 'CONTEXT SPECIFIC',
self::CLASS_PRIVATE => 'PRIVATE',
];
/**
* Type class.
*/
private int $_class;
/**
* Primitive or Constructed.
*/
private readonly int $_pc;
/**
* Content type tag.
*/
private BigInt $_tag;
/**
* @param int $class Type class
* @param int $pc Primitive / Constructed
* @param BigInteger|int $tag Type tag number
*/
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);
}
/**
* Decode identifier component from DER data.
*
* @param string $data DER encoded data
* @param null|int $offset Reference to the variable that contains offset
* into the data where to start parsing.
* Variable is updated to the offset next to the
* parsed identifier. If null, start from offset 0.
*/
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++]);
// bits 8 and 7 (class)
// 0 = universal, 1 = application, 2 = context-specific, 3 = private
$class = (0b11000000 & $byte) >> 6;
// bit 6 (0 = primitive / 1 = constructed)
$pc = (0b00100000 & $byte) >> 5;
// bits 5 to 1 (tag number)
$tag = (0b00011111 & $byte);
// long-form identifier
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();
} // long-form identifier
else {
$bytes[] = $byte | 0x1f;
$octets = [];
for (; $tag->isGreaterThan(0); $tag = $tag->shiftedRight(7)) {
$octets[] = 0x80 | $tag->and(0x7f)->toInt();
}
// last octet has bit 8 set to zero
$octets[0] &= 0x7f;
foreach (array_reverse($octets) as $octet) {
$bytes[] = $octet;
}
}
return pack('C*', ...$bytes);
}
/**
* Get class of the type.
*/
public function typeClass(): int
{
return $this->_class;
}
public function pc(): int
{
return $this->_pc;
}
/**
* Get the tag number.
*
* @return string Base 10 integer string
*/
public function tag(): string
{
return $this->_tag->base10();
}
/**
* Get the tag as an integer.
*/
public function intTag(): int
{
return $this->_tag->toInt();
}
/**
* Check whether type is of an universal class.
*/
public function isUniversal(): bool
{
return $this->_class === self::CLASS_UNIVERSAL;
}
/**
* Check whether type is of an application class.
*/
public function isApplication(): bool
{
return $this->_class === self::CLASS_APPLICATION;
}
/**
* Check whether type is of a context specific class.
*/
public function isContextSpecific(): bool
{
return $this->_class === self::CLASS_CONTEXT_SPECIFIC;
}
/**
* Check whether type is of a private class.
*/
public function isPrivate(): bool
{
return $this->_class === self::CLASS_PRIVATE;
}
/**
* Check whether content is primitive type.
*/
public function isPrimitive(): bool
{
return $this->_pc === self::PRIMITIVE;
}
/**
* Check hether content is constructed type.
*/
public function isConstructed(): bool
{
return $this->_pc === self::CONSTRUCTED;
}
/**
* Get self with given type class.
*
* @param int $class One of `CLASS_*` enumerations
*/
public function withClass(int $class): self
{
$obj = clone $this;
$obj->_class = 0b11 & $class;
return $obj;
}
/**
* Get self with given type tag.
*
* @param int $tag Tag number
*/
public function withTag(int $tag): self
{
$obj = clone $this;
$obj->_tag = BigInt::create($tag);
return $obj;
}
/**
* Get human readable name of the type class.
*/
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];
}
/**
* Parse long form tag.
*
* @param string $data DER data
* @param int $offset Reference to the variable containing offset to data
*
* @return BigInteger Tag number
*/
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);
// last byte has bit 8 set to zero
if ((0x80 & $byte) === 0) {
break;
}
}
return $tag;
}
}

View File

@ -0,0 +1,204 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Component;
use Brick\Math\BigInteger;
use function count;
use DomainException;
use LogicException;
use function mb_strlen;
use function ord;
use SpomkyLabs\Pki\ASN1\Exception\DecodeException;
use SpomkyLabs\Pki\ASN1\Feature\Encodable;
use SpomkyLabs\Pki\ASN1\Util\BigInt;
/**
* Class to represent BER/DER length octets.
*/
final class Length implements Encodable
{
/**
* Length.
*/
private readonly BigInt $_length;
/**
* @param BigInteger|int $length Length
* @param bool $_indefinite Whether length is indefinite
*/
private function __construct(
BigInteger|int $length,
private readonly bool $_indefinite = false
) {
$this->_length = BigInt::create($length);
}
public static function create(BigInteger|int $length, bool $_indefinite = false): self
{
return new self($length, $_indefinite);
}
/**
* Decode length component from DER data.
*
* @param string $data DER encoded data
* @param null|int $offset Reference to the variable that contains offset
* into the data where to start parsing.
* Variable is updated to the offset next to the
* parsed length component. If null, start from offset 0.
*/
public static function fromDER(string $data, int &$offset = null): self
{
$idx = $offset ?? 0;
$datalen = mb_strlen($data, '8bit');
if ($idx >= $datalen) {
throw new DecodeException('Unexpected end of data while decoding length.');
}
$indefinite = false;
$byte = ord($data[$idx++]);
// bits 7 to 1
$length = (0x7f & $byte);
// long form
if ((0x80 & $byte) !== 0) {
if ($length === 0) {
$indefinite = true;
} else {
if ($idx + $length > $datalen) {
throw new DecodeException('Unexpected end of data while decoding long form length.');
}
$length = self::decodeLongFormLength($length, $data, $idx);
}
}
if (isset($offset)) {
$offset = $idx;
}
return self::create($length, $indefinite);
}
/**
* Decode length from DER.
*
* Throws an exception if length doesn't match with expected or if data doesn't contain enough bytes.
*
* Requirement of definite length is relaxed contrary to the specification (sect. 10.1).
*
* @param string $data DER data
* @param int $offset Reference to the offset variable
* @param null|int $expected Expected length, null to bypass checking
* @see self::fromDER
*/
public static function expectFromDER(string $data, int &$offset, int $expected = null): self
{
$idx = $offset;
$length = self::fromDER($data, $idx);
// if certain length was expected
if (isset($expected)) {
if ($length->isIndefinite()) {
throw new DecodeException(sprintf('Expected length %d, got indefinite.', $expected));
}
if ($expected !== $length->intLength()) {
throw new DecodeException(sprintf('Expected length %d, got %d.', $expected, $length->intLength()));
}
}
// check that enough data is available
if (! $length->isIndefinite()
&& mb_strlen($data, '8bit') < $idx + $length->intLength()) {
throw new DecodeException(
sprintf(
'Length %d overflows data, %d bytes left.',
$length->intLength(),
mb_strlen($data, '8bit') - $idx
)
);
}
$offset = $idx;
return $length;
}
public function toDER(): string
{
$bytes = [];
if ($this->_indefinite) {
$bytes[] = 0x80;
} else {
$num = $this->_length->getValue();
// long form
if ($num->isGreaterThan(127)) {
$octets = [];
for (; $num->isGreaterThan(0); $num = $num->shiftedRight(8)) {
$octets[] = BigInteger::of(0xff)->and($num)->toInt();
}
$count = count($octets);
// first octet must not be 0xff
if ($count >= 127) {
throw new DomainException('Too many length octets.');
}
$bytes[] = 0x80 | $count;
foreach (array_reverse($octets) as $octet) {
$bytes[] = $octet;
}
} // short form
else {
$bytes[] = $num->toInt();
}
}
return pack('C*', ...$bytes);
}
/**
* Get the length.
*
* @return string Length as an integer string
*/
public function length(): string
{
if ($this->_indefinite) {
throw new LogicException('Length is indefinite.');
}
return $this->_length->base10();
}
/**
* Get the length as an integer.
*/
public function intLength(): int
{
if ($this->_indefinite) {
throw new LogicException('Length is indefinite.');
}
return $this->_length->toInt();
}
/**
* Whether length is indefinite.
*/
public function isIndefinite(): bool
{
return $this->_indefinite;
}
/**
* Decode long form length.
*
* @param int $length Number of octets
* @param string $data Data
* @param int $offset reference to the variable containing offset to the data
*/
private static function decodeLongFormLength(int $length, string $data, int &$offset): BigInteger
{
// first octet must not be 0xff (spec 8.1.3.5c)
if ($length === 127) {
throw new DecodeException('Invalid number of length octets.');
}
$num = BigInteger::of(0);
while (--$length >= 0) {
$byte = ord($data[$offset++]);
$num = $num->shiftedLeft(8)
->or($byte);
}
return $num;
}
}

View File

@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1;
use BadMethodCallException;
use function mb_strlen;
use SpomkyLabs\Pki\ASN1\Component\Identifier;
use SpomkyLabs\Pki\ASN1\Component\Length;
use SpomkyLabs\Pki\ASN1\Feature\ElementBase;
/**
* Container for raw DER encoded data.
*
* May be inserted into structure without decoding first.
*/
final class DERData extends Element
{
/**
* DER encoded data.
*/
private readonly string $der;
/**
* Identifier of the underlying type.
*/
private readonly Identifier $identifier;
/**
* Offset to the content in DER data.
*/
private int $contentOffset = 0;
/**
* @param string $data DER encoded data
*/
private function __construct(string $data)
{
$this->identifier = Identifier::fromDER($data, $this->contentOffset);
// check that length encoding is valid
Length::expectFromDER($data, $this->contentOffset);
$this->der = $data;
parent::__construct($this->identifier->intTag());
}
public static function create(string $data): self
{
return new self($data);
}
public function typeClass(): int
{
return $this->identifier->typeClass();
}
public function isConstructed(): bool
{
return $this->identifier->isConstructed();
}
public function toDER(): string
{
return $this->der;
}
protected function encodedAsDER(): string
{
// if there's no content payload
if (mb_strlen($this->der, '8bit') === $this->contentOffset) {
return '';
}
return mb_substr($this->der, $this->contentOffset, null, '8bit');
}
protected static function decodeFromDER(Identifier $identifier, string $data, int &$offset): ElementBase
{
throw new BadMethodCallException(__METHOD__ . ' must be implemented in derived class.');
}
}

View File

@ -0,0 +1,474 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1;
use function array_key_exists;
use function mb_strlen;
use SpomkyLabs\Pki\ASN1\Component\Identifier;
use SpomkyLabs\Pki\ASN1\Component\Length;
use SpomkyLabs\Pki\ASN1\Feature\ElementBase;
use SpomkyLabs\Pki\ASN1\Type\Constructed;
use SpomkyLabs\Pki\ASN1\Type\Constructed\ConstructedString;
use SpomkyLabs\Pki\ASN1\Type\Constructed\Sequence;
use SpomkyLabs\Pki\ASN1\Type\Constructed\Set;
use SpomkyLabs\Pki\ASN1\Type\Primitive\BitString;
use SpomkyLabs\Pki\ASN1\Type\Primitive\BMPString;
use SpomkyLabs\Pki\ASN1\Type\Primitive\Boolean;
use SpomkyLabs\Pki\ASN1\Type\Primitive\CharacterString;
use SpomkyLabs\Pki\ASN1\Type\Primitive\Enumerated;
use SpomkyLabs\Pki\ASN1\Type\Primitive\EOC;
use SpomkyLabs\Pki\ASN1\Type\Primitive\GeneralizedTime;
use SpomkyLabs\Pki\ASN1\Type\Primitive\GeneralString;
use SpomkyLabs\Pki\ASN1\Type\Primitive\GraphicString;
use SpomkyLabs\Pki\ASN1\Type\Primitive\IA5String;
use SpomkyLabs\Pki\ASN1\Type\Primitive\Integer;
use SpomkyLabs\Pki\ASN1\Type\Primitive\NullType;
use SpomkyLabs\Pki\ASN1\Type\Primitive\NumericString;
use SpomkyLabs\Pki\ASN1\Type\Primitive\ObjectDescriptor;
use SpomkyLabs\Pki\ASN1\Type\Primitive\ObjectIdentifier;
use SpomkyLabs\Pki\ASN1\Type\Primitive\OctetString;
use SpomkyLabs\Pki\ASN1\Type\Primitive\PrintableString;
use SpomkyLabs\Pki\ASN1\Type\Primitive\Real;
use SpomkyLabs\Pki\ASN1\Type\Primitive\RelativeOID;
use SpomkyLabs\Pki\ASN1\Type\Primitive\T61String;
use SpomkyLabs\Pki\ASN1\Type\Primitive\UniversalString;
use SpomkyLabs\Pki\ASN1\Type\Primitive\UTCTime;
use SpomkyLabs\Pki\ASN1\Type\Primitive\UTF8String;
use SpomkyLabs\Pki\ASN1\Type\Primitive\VideotexString;
use SpomkyLabs\Pki\ASN1\Type\Primitive\VisibleString;
use SpomkyLabs\Pki\ASN1\Type\StringType;
use SpomkyLabs\Pki\ASN1\Type\Tagged\ApplicationType;
use SpomkyLabs\Pki\ASN1\Type\Tagged\ContextSpecificType;
use SpomkyLabs\Pki\ASN1\Type\Tagged\PrivateType;
use SpomkyLabs\Pki\ASN1\Type\TaggedType;
use SpomkyLabs\Pki\ASN1\Type\TimeType;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
use UnexpectedValueException;
/**
* Base class for all ASN.1 type elements.
*/
abstract class Element implements ElementBase
{
// Universal type tags
public const TYPE_EOC = 0x00;
public const TYPE_BOOLEAN = 0x01;
public const TYPE_INTEGER = 0x02;
public const TYPE_BIT_STRING = 0x03;
public const TYPE_OCTET_STRING = 0x04;
public const TYPE_NULL = 0x05;
public const TYPE_OBJECT_IDENTIFIER = 0x06;
public const TYPE_OBJECT_DESCRIPTOR = 0x07;
public const TYPE_EXTERNAL = 0x08;
public const TYPE_REAL = 0x09;
public const TYPE_ENUMERATED = 0x0a;
public const TYPE_EMBEDDED_PDV = 0x0b;
public const TYPE_UTF8_STRING = 0x0c;
public const TYPE_RELATIVE_OID = 0x0d;
public const TYPE_SEQUENCE = 0x10;
public const TYPE_SET = 0x11;
public const TYPE_NUMERIC_STRING = 0x12;
public const TYPE_PRINTABLE_STRING = 0x13;
public const TYPE_T61_STRING = 0x14;
public const TYPE_VIDEOTEX_STRING = 0x15;
public const TYPE_IA5_STRING = 0x16;
public const TYPE_UTC_TIME = 0x17;
public const TYPE_GENERALIZED_TIME = 0x18;
public const TYPE_GRAPHIC_STRING = 0x19;
public const TYPE_VISIBLE_STRING = 0x1a;
public const TYPE_GENERAL_STRING = 0x1b;
public const TYPE_UNIVERSAL_STRING = 0x1c;
public const TYPE_CHARACTER_STRING = 0x1d;
public const TYPE_BMP_STRING = 0x1e;
/**
* Pseudotype for all string types.
*
* May be used as an expectation parameter.
*
* @var int
*/
public const TYPE_STRING = -1;
/**
* Pseudotype for all time types.
*
* May be used as an expectation parameter.
*
* @var int
*/
public const TYPE_TIME = -2;
/**
* Pseudotype for constructed strings.
*
* May be used as an expectation parameter.
*
* @var int
*/
public const TYPE_CONSTRUCTED_STRING = -3;
/**
* Mapping from universal type tag to implementation class name.
*
* @internal
*
* @var array<int, string>
*/
private const MAP_TAG_TO_CLASS = [
self::TYPE_EOC => EOC::class,
self::TYPE_BOOLEAN => Boolean::class,
self::TYPE_INTEGER => Integer::class,
self::TYPE_BIT_STRING => BitString::class,
self::TYPE_OCTET_STRING => OctetString::class,
self::TYPE_NULL => NullType::class,
self::TYPE_OBJECT_IDENTIFIER => ObjectIdentifier::class,
self::TYPE_OBJECT_DESCRIPTOR => ObjectDescriptor::class,
self::TYPE_REAL => Real::class,
self::TYPE_ENUMERATED => Enumerated::class,
self::TYPE_UTF8_STRING => UTF8String::class,
self::TYPE_RELATIVE_OID => RelativeOID::class,
self::TYPE_SEQUENCE => Sequence::class,
self::TYPE_SET => Set::class,
self::TYPE_NUMERIC_STRING => NumericString::class,
self::TYPE_PRINTABLE_STRING => PrintableString::class,
self::TYPE_T61_STRING => T61String::class,
self::TYPE_VIDEOTEX_STRING => VideotexString::class,
self::TYPE_IA5_STRING => IA5String::class,
self::TYPE_UTC_TIME => UTCTime::class,
self::TYPE_GENERALIZED_TIME => GeneralizedTime::class,
self::TYPE_GRAPHIC_STRING => GraphicString::class,
self::TYPE_VISIBLE_STRING => VisibleString::class,
self::TYPE_GENERAL_STRING => GeneralString::class,
self::TYPE_UNIVERSAL_STRING => UniversalString::class,
self::TYPE_CHARACTER_STRING => CharacterString::class,
self::TYPE_BMP_STRING => BMPString::class,
];
/**
* Mapping from universal type tag to human-readable name.
*
* @internal
*
* @var array<int, string>
*/
private const MAP_TYPE_TO_NAME = [
self::TYPE_EOC => 'EOC',
self::TYPE_BOOLEAN => 'BOOLEAN',
self::TYPE_INTEGER => 'INTEGER',
self::TYPE_BIT_STRING => 'BIT STRING',
self::TYPE_OCTET_STRING => 'OCTET STRING',
self::TYPE_NULL => 'NULL',
self::TYPE_OBJECT_IDENTIFIER => 'OBJECT IDENTIFIER',
self::TYPE_OBJECT_DESCRIPTOR => 'ObjectDescriptor',
self::TYPE_EXTERNAL => 'EXTERNAL',
self::TYPE_REAL => 'REAL',
self::TYPE_ENUMERATED => 'ENUMERATED',
self::TYPE_EMBEDDED_PDV => 'EMBEDDED PDV',
self::TYPE_UTF8_STRING => 'UTF8String',
self::TYPE_RELATIVE_OID => 'RELATIVE-OID',
self::TYPE_SEQUENCE => 'SEQUENCE',
self::TYPE_SET => 'SET',
self::TYPE_NUMERIC_STRING => 'NumericString',
self::TYPE_PRINTABLE_STRING => 'PrintableString',
self::TYPE_T61_STRING => 'T61String',
self::TYPE_VIDEOTEX_STRING => 'VideotexString',
self::TYPE_IA5_STRING => 'IA5String',
self::TYPE_UTC_TIME => 'UTCTime',
self::TYPE_GENERALIZED_TIME => 'GeneralizedTime',
self::TYPE_GRAPHIC_STRING => 'GraphicString',
self::TYPE_VISIBLE_STRING => 'VisibleString',
self::TYPE_GENERAL_STRING => 'GeneralString',
self::TYPE_UNIVERSAL_STRING => 'UniversalString',
self::TYPE_CHARACTER_STRING => 'CHARACTER STRING',
self::TYPE_BMP_STRING => 'BMPString',
self::TYPE_STRING => 'Any String',
self::TYPE_TIME => 'Any Time',
self::TYPE_CONSTRUCTED_STRING => 'Constructed String',
];
/**
* @param bool $indefiniteLength Whether type shall be encoded with indefinite length.
*/
protected function __construct(
protected readonly int $typeTag,
protected bool $indefiniteLength = false
) {
}
abstract public function typeClass(): int;
abstract public function isConstructed(): bool;
/**
* Decode element from DER data.
*
* @param string $data DER encoded data
* @param null|int $offset Reference to the variable that contains offset
* into the data where to start parsing.
* Variable is updated to the offset next to the
* parsed element. If null, start from offset 0.
*/
public static function fromDER(string $data, int &$offset = null): static
{
$idx = $offset ?? 0;
// decode identifier
$identifier = Identifier::fromDER($data, $idx);
// determine class that implements type specific decoding
$cls = self::determineImplClass($identifier);
// decode remaining element
$element = $cls::decodeFromDER($identifier, $data, $idx);
// if called in the context of a concrete class, check
// that decoded type matches the type of calling class
$called_class = static::class;
if ($called_class !== self::class) {
if (! $element instanceof $called_class) {
throw new UnexpectedValueException(sprintf('%s expected, got %s.', $called_class, $element::class));
}
}
// update offset for the caller
if (isset($offset)) {
$offset = $idx;
}
return $element;
}
public function toDER(): string
{
$identifier = Identifier::create(
$this->typeClass(),
$this->isConstructed() ? Identifier::CONSTRUCTED : Identifier::PRIMITIVE,
$this->typeTag
);
$content = $this->encodedAsDER();
if ($this->indefiniteLength) {
$length = Length::create(0, true);
$eoc = EOC::create();
return $identifier->toDER() . $length->toDER() . $content . $eoc->toDER();
}
$length = Length::create(mb_strlen($content, '8bit'));
return $identifier->toDER() . $length->toDER() . $content;
}
public function tag(): int
{
return $this->typeTag;
}
public function isType(int $tag): bool
{
// if element is context specific
if ($this->typeClass() === Identifier::CLASS_CONTEXT_SPECIFIC) {
return false;
}
// negative tags identify an abstract pseudotype
if ($tag < 0) {
return $this->isPseudoType($tag);
}
return $this->isConcreteType($tag);
}
public function expectType(int $tag): ElementBase
{
if (! $this->isType($tag)) {
throw new UnexpectedValueException(
sprintf('%s expected, got %s.', self::tagToName($tag), $this->typeDescriptorString())
);
}
return $this;
}
public function isTagged(): bool
{
return $this instanceof TaggedType;
}
public function expectTagged(?int $tag = null): TaggedType
{
if (! $this->isTagged()) {
throw new UnexpectedValueException(
sprintf('Context specific element expected, got %s.', Identifier::classToName($this->typeClass()))
);
}
if (isset($tag) && $this->tag() !== $tag) {
throw new UnexpectedValueException(sprintf('Tag %d expected, got %d.', $tag, $this->tag()));
}
return $this;
}
/**
* Whether element has indefinite length.
*/
public function hasIndefiniteLength(): bool
{
return $this->indefiniteLength;
}
/**
* Get self with indefinite length encoding set.
*
* @param bool $indefinite True for indefinite length, false for definite length
*/
public function withIndefiniteLength(bool $indefinite = true): self
{
$obj = clone $this;
$obj->indefiniteLength = $indefinite;
return $obj;
}
final public function asElement(): self
{
return $this;
}
/**
* Get element decorated with `UnspecifiedType` object.
*/
public function asUnspecified(): UnspecifiedType
{
return UnspecifiedType::create($this);
}
/**
* Get human readable name for an universal tag.
*/
public static function tagToName(int $tag): string
{
if (! array_key_exists($tag, self::MAP_TYPE_TO_NAME)) {
return "TAG {$tag}";
}
return self::MAP_TYPE_TO_NAME[$tag];
}
/**
* Get the content encoded in DER.
*
* Returns the DER encoded content without identifier and length header octets.
*/
abstract protected function encodedAsDER(): string;
/**
* Decode type-specific element from DER.
*
* @param Identifier $identifier Pre-parsed identifier
* @param string $data DER data
* @param int $offset Offset in data to the next byte after identifier
*/
abstract protected static function decodeFromDER(Identifier $identifier, string $data, int &$offset): ElementBase;
/**
* Determine the class that implements the type.
*
* @return string Class name
*/
protected static function determineImplClass(Identifier $identifier): string
{
switch ($identifier->typeClass()) {
case Identifier::CLASS_UNIVERSAL:
$cls = self::determineUniversalImplClass($identifier->intTag());
// constructed strings may be present in BER
if ($identifier->isConstructed()
&& is_subclass_of($cls, StringType::class)) {
$cls = ConstructedString::class;
}
return $cls;
case Identifier::CLASS_CONTEXT_SPECIFIC:
return ContextSpecificType::class;
case Identifier::CLASS_APPLICATION:
return ApplicationType::class;
case Identifier::CLASS_PRIVATE:
return PrivateType::class;
}
throw new UnexpectedValueException(sprintf(
'%s %d not implemented.',
Identifier::classToName($identifier->typeClass()),
$identifier->tag()
));
}
/**
* Determine the class that implements an universal type of the given tag.
*
* @return string Class name
*/
protected static function determineUniversalImplClass(int $tag): string
{
if (! array_key_exists($tag, self::MAP_TAG_TO_CLASS)) {
throw new UnexpectedValueException("Universal tag {$tag} not implemented.");
}
return self::MAP_TAG_TO_CLASS[$tag];
}
/**
* Get textual description of the type for debugging purposes.
*/
protected function typeDescriptorString(): string
{
if ($this->typeClass() === Identifier::CLASS_UNIVERSAL) {
return self::tagToName($this->typeTag);
}
return sprintf('%s TAG %d', Identifier::classToName($this->typeClass()), $this->typeTag);
}
/**
* Check whether the element is a concrete type of given tag.
*/
private function isConcreteType(int $tag): bool
{
// if tag doesn't match
if ($this->tag() !== $tag) {
return false;
}
// if type is universal check that instance is of a correct class
if ($this->typeClass() === Identifier::CLASS_UNIVERSAL) {
$cls = self::determineUniversalImplClass($tag);
if (! $this instanceof $cls) {
return false;
}
}
return true;
}
/**
* Check whether the element is a pseudotype.
*/
private function isPseudoType(int $tag): bool
{
return match ($tag) {
self::TYPE_STRING => $this instanceof StringType,
self::TYPE_TIME => $this instanceof TimeType,
self::TYPE_CONSTRUCTED_STRING => $this instanceof ConstructedString,
default => false,
};
}
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Exception;
use RuntimeException;
/**
* Exception thrown on decoding errors.
*/
final class DecodeException extends RuntimeException
{
}

View File

@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Feature;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Type\TaggedType;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
/**
* Base interface for ASN.1 type elements.
*/
interface ElementBase extends Encodable
{
/**
* Get the class of the ASN.1 type.
*
* One of `Identifier::CLASS_*` constants.
*/
public function typeClass(): int;
/**
* Check whether the element is constructed.
*
* Otherwise it's primitive.
*/
public function isConstructed(): bool;
/**
* Get the tag of the element.
*
* Interpretation of the tag depends on the context. For example, it may represent a universal type tag or a tag of
* an implicitly or explicitly tagged type.
*/
public function tag(): int;
/**
* Check whether the element is a type of given tag.
*
* @param int $tag Type tag
*/
public function isType(int $tag): bool;
/**
* Check whether the element is a type of a given tag.
*
* Throws an exception if expectation fails.
*
* @param int $tag Type tag
*/
public function expectType(int $tag): self;
/**
* Check whether the element is tagged (context specific).
*/
public function isTagged(): bool;
/**
* Check whether the element is tagged (context specific) and optionally has a given tag.
*
* Throws an exception if the element is not tagged or tag differs from the expected.
*
* @param null|int $tag Optional type tag
*/
public function expectTagged(?int $tag = null): TaggedType;
/**
* Get the object as an abstract `Element` instance.
*/
public function asElement(): Element;
/**
* Get the object as an `UnspecifiedType` instance.
*/
public function asUnspecified(): UnspecifiedType;
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Feature;
/**
* Interface for classes that may be encoded to DER.
*/
interface Encodable
{
/**
* Encode object to DER.
*/
public function toDER(): string;
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Feature;
/**
* Interface for classes that may be cast to string.
*/
interface Stringable
{
/**
* Convert object to string.
*/
public function __toString(): string;
/**
* Get the string representation of the type.
*/
public function string(): string;
}

View File

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type;
use InvalidArgumentException;
use SpomkyLabs\Pki\ASN1\Element;
use Stringable;
/**
* Base class for all string types.
*/
abstract class BaseString extends Element implements StringType, Stringable
{
/**
* String value.
*/
private readonly string $string;
protected function __construct(int $typeTag, string $string)
{
parent::__construct($typeTag);
if (! $this->validateString($string)) {
throw new InvalidArgumentException(sprintf('Not a valid %s string.', self::tagToName($this->typeTag)));
}
$this->string = $string;
}
public function __toString(): string
{
return $this->string();
}
/**
* Get the string value.
*/
public function string(): string
{
return $this->string;
}
protected function encodedAsDER(): string
{
return $this->string;
}
/**
* Check whether string is valid for the concrete type.
*/
protected function validateString(string $string): bool
{
// Override in derived classes
return true;
}
}

View File

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type;
use DateTimeImmutable;
use SpomkyLabs\Pki\ASN1\Element;
use Stringable;
/**
* Base class for all types representing a point in time.
*/
abstract class BaseTime extends Element implements TimeType, Stringable
{
/**
* UTC timezone.
*
* @var string
*/
public const TZ_UTC = 'UTC';
protected function __construct(
int $typeTag,
protected readonly DateTimeImmutable $dateTime
) {
parent::__construct($typeTag);
}
public function __toString(): string
{
return $this->string();
}
/**
* Initialize from datetime string.
*
* @see http://php.net/manual/en/datetime.formats.php
*
* @param string $time Time string
*/
abstract public static function fromString(string $time): static;
/**
* Get the date and time.
*/
public function dateTime(): DateTimeImmutable
{
return $this->dateTime;
}
/**
* Get the date and time as a type specific string.
*/
public function string(): string
{
return $this->encodedAsDER();
}
}

View File

@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Constructed;
use function count;
use LogicException;
use SpomkyLabs\Pki\ASN1\Component\Identifier;
use SpomkyLabs\Pki\ASN1\Component\Length;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Exception\DecodeException;
use SpomkyLabs\Pki\ASN1\Type\StringType;
use SpomkyLabs\Pki\ASN1\Type\Structure;
use Stringable;
/**
* Implements constructed type of simple strings.
*
* Constructed strings only exist in BER encodings, and often with indefinite length. Generally constructed string must
* contain only elements that have the same type tag as the constructing element. For example: ``` OCTET STRING (cons) {
* OCTET STRING (prim) "ABC" OCTET STRING (prim) "DEF" } ``` Canonically this corresponds to a payload of "ABCDEF"
* string.
*
* From API standpoint this can also be seen as a string type (as it implements `StringType`), and thus
* `UnspecifiedType::asString()` method may return `ConstructedString` instances.
*/
final class ConstructedString extends Structure implements StringType, Stringable
{
public function __toString(): string
{
return $this->string();
}
/**
* Create from a list of string type elements.
*
* All strings must have the same type.
*/
public static function create(StringType ...$elements): self
{
if (count($elements) === 0) {
throw new LogicException('No elements, unable to determine type tag.');
}
$tag = $elements[0]->tag();
return self::createWithTag($tag, ...$elements);
}
/**
* Create from strings with a given type tag.
*
* Does not perform any validation on types.
*
* @param int $tag Type tag for the constructed string element
* @param StringType ...$elements Any number of elements
*/
public static function createWithTag(int $tag, StringType ...$elements): self
{
foreach ($elements as $el) {
if ($el->tag() !== $tag) {
throw new LogicException('All elements in constructed string must have the same type.');
}
}
return new self($tag, ...$elements);
}
/**
* Get a list of strings in this structure.
*
* @return string[]
*/
public function strings(): array
{
return array_map(static fn (Element $el): string => $el->string(), $this->elements);
}
/**
* Get the contained strings concatenated together.
*
* NOTE: It's unclear how bit strings with unused bits should be concatenated.
*/
public function string(): string
{
return implode('', $this->strings());
}
protected static function decodeFromDER(Identifier $identifier, string $data, int &$offset): self
{
if (! $identifier->isConstructed()) {
throw new DecodeException('Structured element must have constructed bit set.');
}
$idx = $offset;
$length = Length::expectFromDER($data, $idx);
if ($length->isIndefinite()) {
$type = self::decodeIndefiniteLength($identifier->intTag(), $data, $idx);
} else {
$type = self::decodeDefiniteLength($identifier->intTag(), $data, $idx, $length->intLength());
}
$offset = $idx;
return $type;
}
/**
* Decode elements for a definite length.
*
* @param string $data DER data
* @param int $offset Offset to data
* @param int $length Number of bytes to decode
*/
protected static function decodeDefiniteLength(int $typeTag, string $data, int &$offset, int $length): self
{
$idx = $offset;
$end = $idx + $length;
$elements = [];
while ($idx < $end) {
$elements[] = Element::fromDER($data, $idx);
// check that element didn't overflow length
if ($idx > $end) {
throw new DecodeException("Structure's content overflows length.");
}
}
$offset = $idx;
// return instance by static late binding
return self::createWithTag($typeTag, ...$elements);
}
/**
* Decode elements for an indefinite length.
*
* @param string $data DER data
* @param int $offset Offset to data
*/
protected static function decodeIndefiniteLength(int $typeTag, string $data, int &$offset): self
{
$idx = $offset;
$elements = [];
$end = mb_strlen($data, '8bit');
while (true) {
if ($idx >= $end) {
throw new DecodeException('Unexpected end of data while decoding indefinite length structure.');
}
$el = Element::fromDER($data, $idx);
if ($el->isType(self::TYPE_EOC)) {
break;
}
$elements[] = $el;
}
$offset = $idx;
$type = self::createWithTag($typeTag, ...$elements);
$type->indefiniteLength = true;
return $type;
}
}

View File

@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Constructed;
use SpomkyLabs\Pki\ASN1\Component\Identifier;
use SpomkyLabs\Pki\ASN1\Component\Length;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Exception\DecodeException;
use SpomkyLabs\Pki\ASN1\Type\Structure;
/**
* Implements *SEQUENCE* and *SEQUENCE OF* types.
*/
final class Sequence extends Structure
{
/**
* @param Element ...$elements Any number of elements
*/
public static function create(Element ...$elements): self
{
return new self(self::TYPE_SEQUENCE, ...$elements);
}
protected static function decodeFromDER(Identifier $identifier, string $data, int &$offset): self
{
if (! $identifier->isConstructed()) {
throw new DecodeException('Structured element must have constructed bit set.');
}
$idx = $offset;
$length = Length::expectFromDER($data, $idx);
if ($length->isIndefinite()) {
$type = self::decodeIndefiniteLength($data, $idx);
} else {
$type = self::decodeDefiniteLength($data, $idx, $length->intLength());
}
$offset = $idx;
return $type;
}
/**
* Decode elements for a definite length.
*
* @param string $data DER data
* @param int $offset Offset to data
* @param int $length Number of bytes to decode
*/
protected static function decodeDefiniteLength(string $data, int &$offset, int $length): self
{
$idx = $offset;
$end = $idx + $length;
$elements = [];
while ($idx < $end) {
$elements[] = Element::fromDER($data, $idx);
// check that element didn't overflow length
if ($idx > $end) {
throw new DecodeException("Structure's content overflows length.");
}
}
$offset = $idx;
// return instance by static late binding
return self::create(...$elements);
}
/**
* Decode elements for an indefinite length.
*
* @param string $data DER data
* @param int $offset Offset to data
*/
protected static function decodeIndefiniteLength(string $data, int &$offset): self
{
$idx = $offset;
$elements = [];
$end = mb_strlen($data, '8bit');
while (true) {
if ($idx >= $end) {
throw new DecodeException('Unexpected end of data while decoding indefinite length structure.');
}
$el = Element::fromDER($data, $idx);
if ($el->isType(self::TYPE_EOC)) {
break;
}
$elements[] = $el;
}
$offset = $idx;
$type = self::create(...$elements);
$type->indefiniteLength = true;
return $type;
}
}

View File

@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Constructed;
use SpomkyLabs\Pki\ASN1\Component\Identifier;
use SpomkyLabs\Pki\ASN1\Component\Length;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Exception\DecodeException;
use SpomkyLabs\Pki\ASN1\Feature\ElementBase;
use SpomkyLabs\Pki\ASN1\Type\Structure;
/**
* Implements *SET* and *SET OF* types.
*/
final class Set extends Structure
{
/**
* @param Element ...$elements Any number of elements
*/
public static function create(Element ...$elements): self
{
return new self(self::TYPE_SET, ...$elements);
}
/**
* Sort by canonical ascending order.
*
* Used for DER encoding of *SET* type.
*/
public function sortedSet(): self
{
$obj = clone $this;
usort(
$obj->elements,
function (Element $a, Element $b) {
if ($a->typeClass() !== $b->typeClass()) {
return $a->typeClass() < $b->typeClass() ? -1 : 1;
}
return $a->tag() <=> $b->tag();
}
);
return $obj;
}
/**
* Sort by encoding ascending order.
*
* Used for DER encoding of *SET OF* type.
*/
public function sortedSetOf(): self
{
$obj = clone $this;
usort(
$obj->elements,
function (Element $a, Element $b) {
$a_der = $a->toDER();
$b_der = $b->toDER();
return strcmp($a_der, $b_der);
}
);
return $obj;
}
/**
* @return self
*/
protected static function decodeFromDER(Identifier $identifier, string $data, int &$offset): ElementBase
{
if (! $identifier->isConstructed()) {
throw new DecodeException('Structured element must have constructed bit set.');
}
$idx = $offset;
$length = Length::expectFromDER($data, $idx);
if ($length->isIndefinite()) {
$type = self::decodeIndefiniteLength($data, $idx);
} else {
$type = self::decodeDefiniteLength($data, $idx, $length->intLength());
}
$offset = $idx;
return $type;
}
/**
* Decode elements for a definite length.
*
* @param string $data DER data
* @param int $offset Offset to data
* @param int $length Number of bytes to decode
*/
protected static function decodeDefiniteLength(string $data, int &$offset, int $length): ElementBase
{
$idx = $offset;
$end = $idx + $length;
$elements = [];
while ($idx < $end) {
$elements[] = Element::fromDER($data, $idx);
// check that element didn't overflow length
if ($idx > $end) {
throw new DecodeException("Structure's content overflows length.");
}
}
$offset = $idx;
// return instance by static late binding
return self::create(...$elements);
}
/**
* Decode elements for an indefinite length.
*
* @param string $data DER data
* @param int $offset Offset to data
*/
protected static function decodeIndefiniteLength(string $data, int &$offset): ElementBase
{
$idx = $offset;
$elements = [];
$end = mb_strlen($data, '8bit');
while (true) {
if ($idx >= $end) {
throw new DecodeException('Unexpected end of data while decoding indefinite length structure.');
}
$el = Element::fromDER($data, $idx);
if ($el->isType(self::TYPE_EOC)) {
break;
}
$elements[] = $el;
}
$offset = $idx;
$type = self::create(...$elements);
$type->indefiniteLength = true;
return $type;
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Primitive;
use function mb_strlen;
use SpomkyLabs\Pki\ASN1\Type\PrimitiveString;
use SpomkyLabs\Pki\ASN1\Type\UniversalClass;
/**
* Implements *BMPString* type.
*
* BMP stands for Basic Multilingual Plane. This is generally an Unicode string with UCS-2 encoding.
*/
final class BMPString extends PrimitiveString
{
use UniversalClass;
private function __construct(string $string)
{
parent::__construct(self::TYPE_BMP_STRING, $string);
}
public static function create(string $string): self
{
return new self($string);
}
protected function validateString(string $string): bool
{
// UCS-2 has fixed with of 2 octets (16 bits)
return mb_strlen($string, '8bit') % 2 === 0;
}
}

View File

@ -0,0 +1,191 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Primitive;
use Brick\Math\BigInteger;
use function chr;
use function mb_strlen;
use function ord;
use OutOfBoundsException;
use SpomkyLabs\Pki\ASN1\Component\Identifier;
use SpomkyLabs\Pki\ASN1\Component\Length;
use SpomkyLabs\Pki\ASN1\Exception\DecodeException;
use SpomkyLabs\Pki\ASN1\Feature\ElementBase;
use SpomkyLabs\Pki\ASN1\Type\BaseString;
use SpomkyLabs\Pki\ASN1\Type\PrimitiveType;
use SpomkyLabs\Pki\ASN1\Type\UniversalClass;
/**
* Implements *BIT STRING* type.
*/
final class BitString extends BaseString
{
use UniversalClass;
use PrimitiveType;
/**
* @param string $string Content octets
* @param int $unusedBits Number of unused bits in the last octet
*/
private function __construct(
string $string,
private readonly int $unusedBits = 0
) {
parent::__construct(self::TYPE_BIT_STRING, $string);
}
public static function create(string $string, int $_unusedBits = 0): self
{
return new self($string, $_unusedBits);
}
/**
* Get the number of bits in the string.
*/
public function numBits(): int
{
return mb_strlen($this->string(), '8bit') * 8 - $this->unusedBits;
}
/**
* Get the number of unused bits in the last octet of the string.
*/
public function unusedBits(): int
{
return $this->unusedBits;
}
/**
* Test whether bit is set.
*
* @param int $idx Bit index. Most significant bit of the first octet is index 0.
*/
public function testBit(int $idx): bool
{
// octet index
$oi = (int) floor($idx / 8);
// if octet is outside range
if ($oi < 0 || $oi >= mb_strlen($this->string(), '8bit')) {
throw new OutOfBoundsException('Index is out of bounds.');
}
// bit index
$bi = $idx % 8;
// if tested bit is last octet's unused bit
if ($oi === mb_strlen($this->string(), '8bit') - 1) {
if ($bi >= 8 - $this->unusedBits) {
throw new OutOfBoundsException('Index refers to an unused bit.');
}
}
$byte = $this->string()[$oi];
// index 0 is the most significant bit in byte
$mask = 0x01 << (7 - $bi);
return (ord($byte) & $mask) > 0;
}
/**
* Get range of bits.
*
* @param int $start Index of first bit
* @param int $length Number of bits in range
*
* @return string Integer of $length bits
*/
public function range(int $start, int $length): string
{
if ($length === 0) {
return '0';
}
if ($start + $length > $this->numBits()) {
throw new OutOfBoundsException('Not enough bits.');
}
$bits = BigInteger::of(0);
$idx = $start;
$end = $start + $length;
while (true) {
$bit = $this->testBit($idx) ? 1 : 0;
$bits = $bits->or($bit);
if (++$idx >= $end) {
break;
}
$bits = $bits->shiftedLeft(1);
}
return $bits->toBase(10);
}
/**
* Get a copy of the bit string with trailing zeroes removed.
*/
public function withoutTrailingZeroes(): self
{
// if bit string was empty
if ($this->string() === '') {
return self::create('');
}
$bits = $this->string();
// count number of empty trailing octets
$unused_octets = 0;
for ($idx = mb_strlen($bits, '8bit') - 1; $idx >= 0; --$idx, ++$unused_octets) {
if ($bits[$idx] !== "\x0") {
break;
}
}
// strip trailing octets
if ($unused_octets !== 0) {
$bits = mb_substr($bits, 0, -$unused_octets, '8bit');
}
// if bit string was full of zeroes
if ($bits === '') {
return self::create('');
}
// count number of trailing zeroes in the last octet
$unused_bits = 0;
$byte = ord($bits[mb_strlen($bits, '8bit') - 1]);
while (0 === ($byte & 0x01)) {
++$unused_bits;
$byte >>= 1;
}
return self::create($bits, $unused_bits);
}
protected function encodedAsDER(): string
{
$der = chr($this->unusedBits);
$der .= $this->string();
if ($this->unusedBits !== 0) {
$octet = $der[mb_strlen($der, '8bit') - 1];
// set unused bits to zero
$octet &= chr(0xff & ~((1 << $this->unusedBits) - 1));
$der[mb_strlen($der, '8bit') - 1] = $octet;
}
return $der;
}
protected static function decodeFromDER(Identifier $identifier, string $data, int &$offset): ElementBase
{
$idx = $offset;
$length = Length::expectFromDER($data, $idx);
if ($length->intLength() < 1) {
throw new DecodeException('Bit string length must be at least 1.');
}
$unused_bits = ord($data[$idx++]);
if ($unused_bits > 7) {
throw new DecodeException('Unused bits in a bit string must be less than 8.');
}
$str_len = $length->intLength() - 1;
if ($str_len !== 0) {
$str = mb_substr($data, $idx, $str_len, '8bit');
if ($unused_bits !== 0) {
$mask = (1 << $unused_bits) - 1;
if (($mask & ord($str[mb_strlen($str, '8bit') - 1])) !== 0) {
throw new DecodeException('DER encoded bit string must have zero padding.');
}
}
} else {
$str = '';
}
$offset = $idx + $str_len;
return self::create($str, $unused_bits);
}
}

View File

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Primitive;
use function chr;
use function ord;
use SpomkyLabs\Pki\ASN1\Component\Identifier;
use SpomkyLabs\Pki\ASN1\Component\Length;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Exception\DecodeException;
use SpomkyLabs\Pki\ASN1\Feature\ElementBase;
use SpomkyLabs\Pki\ASN1\Type\PrimitiveType;
use SpomkyLabs\Pki\ASN1\Type\UniversalClass;
/**
* Implements *BOOLEAN* type.
*/
final class Boolean extends Element
{
use UniversalClass;
use PrimitiveType;
private function __construct(
private readonly bool $_bool
) {
parent::__construct(self::TYPE_BOOLEAN);
}
public static function create(bool $_bool): self
{
return new self($_bool);
}
/**
* Get the value.
*/
public function value(): bool
{
return $this->_bool;
}
protected function encodedAsDER(): string
{
return $this->_bool ? chr(0xff) : chr(0);
}
protected static function decodeFromDER(Identifier $identifier, string $data, int &$offset): ElementBase
{
$idx = $offset;
Length::expectFromDER($data, $idx, 1);
$byte = ord($data[$idx++]);
if ($byte !== 0) {
if ($byte !== 0xff) {
throw new DecodeException('DER encoded boolean true must have all bits set to 1.');
}
}
$offset = $idx;
return self::create($byte !== 0);
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Primitive;
use SpomkyLabs\Pki\ASN1\Type\PrimitiveString;
use SpomkyLabs\Pki\ASN1\Type\UniversalClass;
/**
* Implements *CHARACTER STRING* type.
*/
final class CharacterString extends PrimitiveString
{
use UniversalClass;
private function __construct(string $string)
{
parent::__construct(self::TYPE_CHARACTER_STRING, $string);
}
public static function create(string $string): self
{
return new self($string);
}
}

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Primitive;
use SpomkyLabs\Pki\ASN1\Component\Identifier;
use SpomkyLabs\Pki\ASN1\Component\Length;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Exception\DecodeException;
use SpomkyLabs\Pki\ASN1\Feature\ElementBase;
use SpomkyLabs\Pki\ASN1\Type\PrimitiveType;
use SpomkyLabs\Pki\ASN1\Type\UniversalClass;
/**
* Implements *End-of-contents* type.
*/
final class EOC extends Element
{
use UniversalClass;
use PrimitiveType;
private function __construct()
{
parent::__construct(self::TYPE_EOC);
}
public static function create(): self
{
return new self();
}
protected function encodedAsDER(): string
{
return '';
}
protected static function decodeFromDER(Identifier $identifier, string $data, int &$offset): ElementBase
{
$idx = $offset;
if (! $identifier->isPrimitive()) {
throw new DecodeException('EOC value must be primitive.');
}
// EOC type has always zero length
Length::expectFromDER($data, $idx, 0);
$offset = $idx;
return self::create();
}
}

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Primitive;
use Brick\Math\BigInteger;
use SpomkyLabs\Pki\ASN1\Component\Identifier;
use SpomkyLabs\Pki\ASN1\Component\Length;
use SpomkyLabs\Pki\ASN1\Feature\ElementBase;
use SpomkyLabs\Pki\ASN1\Util\BigInt;
/**
* Implements *ENUMERATED* type.
*/
final class Enumerated extends Integer
{
public static function create(BigInteger|int|string $number): static
{
return new static($number, self::TYPE_ENUMERATED);
}
protected static function decodeFromDER(Identifier $identifier, string $data, int &$offset): ElementBase
{
$idx = $offset;
$length = Length::expectFromDER($data, $idx)->intLength();
$bytes = mb_substr($data, $idx, $length, '8bit');
$idx += $length;
$num = BigInt::fromSignedOctets($bytes)->getValue();
$offset = $idx;
// late static binding since enumerated extends integer type
return self::create($num);
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Primitive;
use SpomkyLabs\Pki\ASN1\Type\PrimitiveString;
use SpomkyLabs\Pki\ASN1\Type\UniversalClass;
/**
* Implements *GeneralString* type.
*/
final class GeneralString extends PrimitiveString
{
use UniversalClass;
private function __construct(string $string)
{
parent::__construct(self::TYPE_GENERAL_STRING, $string);
}
public static function create(string $string): self
{
return new self($string);
}
protected function validateString(string $string): bool
{
// allow everything
return true;
}
}

View File

@ -0,0 +1,133 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Primitive;
use DateTimeImmutable;
use DateTimeZone;
use function intval;
use function mb_strlen;
use SpomkyLabs\Pki\ASN1\Component\Identifier;
use SpomkyLabs\Pki\ASN1\Component\Length;
use SpomkyLabs\Pki\ASN1\Exception\DecodeException;
use SpomkyLabs\Pki\ASN1\Feature\ElementBase;
use SpomkyLabs\Pki\ASN1\Type\BaseTime;
use SpomkyLabs\Pki\ASN1\Type\PrimitiveType;
use SpomkyLabs\Pki\ASN1\Type\UniversalClass;
use Throwable;
use UnexpectedValueException;
/**
* Implements *GeneralizedTime* type.
*/
final class GeneralizedTime extends BaseTime
{
use UniversalClass;
use PrimitiveType;
/**
* Regular expression to parse date.
*
* DER restricts format to UTC timezone (Z suffix).
*
* @var string
*/
final public const REGEX = '#^' .
'(\d\d\d\d)' . // YYYY
'(\d\d)' . // MM
'(\d\d)' . // DD
'(\d\d)' . // hh
'(\d\d)' . // mm
'(\d\d)' . // ss
'(?:\.(\d+))?' . // frac
'Z' . // TZ
'$#';
/**
* Cached formatted date.
*/
private ?string $_formatted = null;
private function __construct(DateTimeImmutable $dt)
{
parent::__construct(self::TYPE_GENERALIZED_TIME, $dt);
}
/**
* Clear cached variables on clone.
*/
public function __clone()
{
$this->_formatted = null;
}
public static function create(DateTimeImmutable $dt): self
{
return new self($dt);
}
public static function fromString(string $time, ?string $tz = null): static
{
return new static(new DateTimeImmutable($time, self::createTimeZone($tz)));
}
protected function encodedAsDER(): string
{
if (! isset($this->_formatted)) {
$dt = $this->dateTime->setTimezone(new DateTimeZone('UTC'));
$this->_formatted = $dt->format('YmdHis');
// if fractions were used
$frac = $dt->format('u');
if (intval($frac) !== 0) {
$frac = rtrim($frac, '0');
$this->_formatted .= ".{$frac}";
}
// timezone
$this->_formatted .= 'Z';
}
return $this->_formatted;
}
protected static function decodeFromDER(Identifier $identifier, string $data, int &$offset): ElementBase
{
$idx = $offset;
$length = Length::expectFromDER($data, $idx)->intLength();
$str = mb_substr($data, $idx, $length, '8bit');
$idx += $length;
if (preg_match(self::REGEX, $str, $match) !== 1) {
throw new DecodeException('Invalid GeneralizedTime format.');
}
[, $year, $month, $day, $hour, $minute, $second] = $match;
// if fractions match, there's at least one digit
if (isset($match[7])) {
$frac = $match[7];
// DER restricts trailing zeroes in fractional seconds component
if ($frac[mb_strlen($frac, '8bit') - 1] === '0') {
throw new DecodeException('Fractional seconds must omit trailing zeroes.');
}
} else {
$frac = '0';
}
$time = $year . $month . $day . $hour . $minute . $second . '.' . $frac .
self::TZ_UTC;
$dt = DateTimeImmutable::createFromFormat('!YmdHis.uT', $time, new DateTimeZone('UTC'));
if ($dt === false) {
throw new DecodeException('Failed to decode GeneralizedTime');
}
$offset = $idx;
return self::create($dt);
}
/**
* Create `DateTimeZone` object from string.
*/
private static function createTimeZone(?string $tz): DateTimeZone
{
try {
return new DateTimeZone($tz ?? 'UTC');
} catch (Throwable $e) {
throw new UnexpectedValueException('Invalid timezone.', 0, $e);
}
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Primitive;
use SpomkyLabs\Pki\ASN1\Type\PrimitiveString;
use SpomkyLabs\Pki\ASN1\Type\UniversalClass;
/**
* Implements *GraphicString* type.
*/
final class GraphicString extends PrimitiveString
{
use UniversalClass;
private function __construct(string $string)
{
parent::__construct(self::TYPE_GRAPHIC_STRING, $string);
}
public static function create(string $string): self
{
return new self($string);
}
protected function validateString(string $string): bool
{
// allow everything
return true;
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Primitive;
use SpomkyLabs\Pki\ASN1\Type\PrimitiveString;
use SpomkyLabs\Pki\ASN1\Type\UniversalClass;
/**
* Implements *IA5String* type.
*/
final class IA5String extends PrimitiveString
{
use UniversalClass;
private function __construct(string $string)
{
parent::__construct(self::TYPE_IA5_STRING, $string);
}
public static function create(string $string): self
{
return new self($string);
}
protected function validateString(string $string): bool
{
return preg_match('/[^\x00-\x7f]/', $string) !== 1;
}
}

View File

@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Primitive;
use Brick\Math\BigInteger;
use function gettype;
use InvalidArgumentException;
use function is_int;
use function is_scalar;
use function is_string;
use SpomkyLabs\Pki\ASN1\Component\Identifier;
use SpomkyLabs\Pki\ASN1\Component\Length;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Feature\ElementBase;
use SpomkyLabs\Pki\ASN1\Type\PrimitiveType;
use SpomkyLabs\Pki\ASN1\Type\UniversalClass;
use SpomkyLabs\Pki\ASN1\Util\BigInt;
/**
* Implements *INTEGER* type.
*/
class Integer extends Element
{
use UniversalClass;
use PrimitiveType;
/**
* The number.
*/
private readonly BigInt $_number;
/**
* @param BigInteger|int|string $number Base 10 integer
*/
final protected function __construct(BigInteger|int|string $number, int $typeTag)
{
parent::__construct($typeTag);
if (! self::validateNumber($number)) {
$var = is_scalar($number) ? (string) $number : gettype($number);
throw new InvalidArgumentException("'{$var}' is not a valid number.");
}
$this->_number = BigInt::create($number);
}
public static function create(BigInteger|int|string $number): static
{
return new static($number, self::TYPE_INTEGER);
}
/**
* Get the number as a base 10.
*
* @return string Integer as a string
*/
public function number(): string
{
return $this->_number->base10();
}
public function getValue(): BigInteger
{
return $this->_number->getValue();
}
/**
* Get the number as an integer type.
*/
public function intNumber(): int
{
return $this->_number->toInt();
}
protected function encodedAsDER(): string
{
return $this->_number->signedOctets();
}
protected static function decodeFromDER(Identifier $identifier, string $data, int &$offset): ElementBase
{
$idx = $offset;
$length = Length::expectFromDER($data, $idx)->intLength();
$bytes = mb_substr($data, $idx, $length, '8bit');
$idx += $length;
$num = BigInt::fromSignedOctets($bytes)->getValue();
$offset = $idx;
// late static binding since enumerated extends integer type
return static::create($num);
}
/**
* Test that number is valid for this context.
*/
private static function validateNumber(mixed $num): bool
{
if (is_int($num)) {
return true;
}
if (is_string($num) && preg_match('/-?\d+/', $num) === 1) {
return true;
}
if ($num instanceof BigInteger) {
return true;
}
return false;
}
}

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Primitive;
use SpomkyLabs\Pki\ASN1\Component\Identifier;
use SpomkyLabs\Pki\ASN1\Component\Length;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Exception\DecodeException;
use SpomkyLabs\Pki\ASN1\Feature\ElementBase;
use SpomkyLabs\Pki\ASN1\Type\PrimitiveType;
use SpomkyLabs\Pki\ASN1\Type\UniversalClass;
/**
* Implements *NULL* type.
*/
final class NullType extends Element
{
use UniversalClass;
use PrimitiveType;
private function __construct()
{
parent::__construct(self::TYPE_NULL);
}
public static function create(): self
{
return new self();
}
protected function encodedAsDER(): string
{
return '';
}
protected static function decodeFromDER(Identifier $identifier, string $data, int &$offset): ElementBase
{
$idx = $offset;
if (! $identifier->isPrimitive()) {
throw new DecodeException('Null value must be primitive.');
}
// null type has always zero length
Length::expectFromDER($data, $idx, 0);
$offset = $idx;
return self::create();
}
}

View File

@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Primitive;
use Brick\Math\BigInteger;
use function gettype;
use InvalidArgumentException;
use function is_int;
use function is_scalar;
use function is_string;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Type\PrimitiveType;
use SpomkyLabs\Pki\ASN1\Type\UniversalClass;
use SpomkyLabs\Pki\ASN1\Util\BigInt;
use function strval;
abstract class Number extends Element
{
use UniversalClass;
use PrimitiveType;
/**
* The number.
*/
private readonly BigInt $number;
/**
* @param BigInteger|int|string $number Base 10 integer
*/
protected function __construct(int $tag, BigInteger|int|string $number)
{
parent::__construct($tag);
if (! self::validateNumber($number)) {
$var = is_scalar($number) ? strval($number) : gettype($number);
throw new InvalidArgumentException(sprintf('"%s" is not a valid number.', $var));
}
$this->number = BigInt::create($number);
}
abstract public static function create(BigInteger|int|string $number): self;
/**
* Get the number as a base 10.
*
* @return string Integer as a string
*/
public function number(): string
{
return $this->number->base10();
}
public function getValue(): BigInteger
{
return $this->number->getValue();
}
/**
* Get the number as an integer type.
*/
public function intNumber(): int
{
return $this->number->toInt();
}
protected function encodedAsDER(): string
{
return $this->number->signedOctets();
}
/**
* Test that number is valid for this context.
*/
private static function validateNumber(mixed $num): bool
{
if (is_int($num)) {
return true;
}
if (is_string($num) && preg_match('/-?\d+/', $num) === 1) {
return true;
}
if ($num instanceof BigInteger) {
return true;
}
return false;
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Primitive;
use SpomkyLabs\Pki\ASN1\Type\PrimitiveString;
use SpomkyLabs\Pki\ASN1\Type\UniversalClass;
/**
* Implements *NumericString* type.
*/
final class NumericString extends PrimitiveString
{
use UniversalClass;
private function __construct(string $string)
{
parent::__construct(self::TYPE_NUMERIC_STRING, $string);
}
public static function create(string $string): self
{
return new self($string);
}
protected function validateString(string $string): bool
{
return preg_match('/[^\d ]/', $string) !== 1;
}
}

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Primitive;
use SpomkyLabs\Pki\ASN1\Type\PrimitiveString;
use SpomkyLabs\Pki\ASN1\Type\UniversalClass;
/**
* Implements *ObjectDescriptor* type.
*/
final class ObjectDescriptor extends PrimitiveString
{
use UniversalClass;
private function __construct(string $descriptor)
{
parent::__construct(self::TYPE_OBJECT_DESCRIPTOR, $descriptor);
}
public static function create(string $descriptor): self
{
return new self($descriptor);
}
/**
* Get the object descriptor.
*/
public function descriptor(): string
{
return $this->string();
}
}

View File

@ -0,0 +1,198 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Primitive;
use Brick\Math\BigInteger;
use function chr;
use function count;
use function is_int;
use function mb_strlen;
use function ord;
use RuntimeException;
use SpomkyLabs\Pki\ASN1\Component\Identifier;
use SpomkyLabs\Pki\ASN1\Component\Length;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Exception\DecodeException;
use SpomkyLabs\Pki\ASN1\Feature\ElementBase;
use SpomkyLabs\Pki\ASN1\Type\PrimitiveType;
use SpomkyLabs\Pki\ASN1\Type\UniversalClass;
use Throwable;
use UnexpectedValueException;
/**
* Implements *OBJECT IDENTIFIER* type.
*/
final class ObjectIdentifier extends Element
{
use UniversalClass;
use PrimitiveType;
/**
* Object identifier split to sub ID's.
*
* @var BigInteger[]
*/
private readonly array $subids;
/**
* @param string $oid OID in dotted format
*/
private function __construct(
private readonly string $oid,
?int $typeTag
) {
$this->subids = self::explodeDottedOID($oid);
// if OID is non-empty
if (count($this->subids) > 0) {
// check that at least two nodes are set
if (count($this->subids) < 2) {
throw new UnexpectedValueException('OID must have at least two nodes.');
}
// check that root arc is in 0..2 range
if ($this->subids[0]->isGreaterThan(2)) {
throw new UnexpectedValueException('Root arc must be in range of 0..2.');
}
// if root arc is 0 or 1, second node must be in 0..39 range
if ($this->subids[0]->isLessThan(2) && $this->subids[1]->isGreaterThanOrEqualTo(40)) {
throw new UnexpectedValueException('Second node must be in 0..39 range for root arcs 0 and 1.');
}
}
parent::__construct($typeTag ?? self::TYPE_OBJECT_IDENTIFIER);
}
public static function create(string $oid, ?int $typeTag = null): self
{
return new self($oid, $typeTag);
}
/**
* Get OID in dotted format.
*/
public function oid(): string
{
return $this->oid;
}
protected function encodedAsDER(): string
{
$subids = $this->subids;
// encode first two subids to one according to spec section 8.19.4
if (count($subids) >= 2) {
$num = $subids[0]->multipliedBy(40)->plus($subids[1]);
array_splice($subids, 0, 2, [$num]);
}
return self::encodeSubIDs(...$subids);
}
protected static function decodeFromDER(Identifier $identifier, string $data, int &$offset): ElementBase
{
$idx = $offset;
$len = Length::expectFromDER($data, $idx)->intLength();
$subids = self::decodeSubIDs(mb_substr($data, $idx, $len, '8bit'));
$idx += $len;
// decode first subidentifier according to spec section 8.19.4
if (isset($subids[0])) {
if ($subids[0]->isLessThan(80)) {
[$x, $y] = $subids[0]->quotientAndRemainder(40);
} else {
$x = BigInteger::of(2);
$y = $subids[0]->minus(80);
}
array_splice($subids, 0, 1, [$x, $y]);
}
$offset = $idx;
return self::create(self::implodeSubIDs(...$subids));
}
/**
* Explode dotted OID to an array of sub ID's.
*
* @param string $oid OID in dotted format
*
* @return BigInteger[] Array of BigInteger numbers
*/
protected static function explodeDottedOID(string $oid): array
{
$subids = [];
if ($oid !== '') {
foreach (explode('.', $oid) as $subid) {
try {
$n = BigInteger::of($subid);
$subids[] = $n;
} catch (Throwable $e) {
throw new UnexpectedValueException(sprintf('"%s" is not a number.', $subid), 0, $e);
}
}
}
return $subids;
}
/**
* Implode an array of sub IDs to dotted OID format.
*/
protected static function implodeSubIDs(BigInteger ...$subids): string
{
return implode('.', array_map(static fn ($num) => $num->toBase(10), $subids));
}
/**
* Encode sub ID's to DER.
*/
protected static function encodeSubIDs(BigInteger ...$subids): string
{
$data = '';
foreach ($subids as $subid) {
// if number fits to one base 128 byte
if ($subid->isLessThan(128)) {
$data .= chr($subid->toInt());
} else { // encode to multiple bytes
$bytes = [];
do {
array_unshift($bytes, 0x7f & $subid->toInt());
$subid = $subid->shiftedRight(7);
} while ($subid->isGreaterThan(0));
// all bytes except last must have bit 8 set to one
foreach (array_splice($bytes, 0, -1) as $byte) {
$data .= chr(0x80 | $byte);
}
$byte = reset($bytes);
if (! is_int($byte)) {
throw new RuntimeException('Encoding failed');
}
$data .= chr($byte);
}
}
return $data;
}
/**
* Decode sub ID's from DER data.
*
* @return BigInteger[] Array of BigInteger numbers
*/
protected static function decodeSubIDs(string $data): array
{
$subids = [];
$idx = 0;
$end = mb_strlen($data, '8bit');
while ($idx < $end) {
$num = BigInteger::of(0);
while (true) {
if ($idx >= $end) {
throw new DecodeException('Unexpected end of data.');
}
$byte = ord($data[$idx++]);
$num = $num->or($byte & 0x7f);
// bit 8 of the last octet is zero
if (0 === ($byte & 0x80)) {
break;
}
$num = $num->shiftedLeft(7);
}
$subids[] = $num;
}
return $subids;
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Primitive;
use SpomkyLabs\Pki\ASN1\Type\PrimitiveString;
use SpomkyLabs\Pki\ASN1\Type\UniversalClass;
/**
* Implements *OCTET STRING* type.
*/
final class OctetString extends PrimitiveString
{
use UniversalClass;
private function __construct(string $string)
{
parent::__construct(self::TYPE_OCTET_STRING, $string);
}
public static function create(string $string): self
{
return new self($string);
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Primitive;
use SpomkyLabs\Pki\ASN1\Type\PrimitiveString;
use SpomkyLabs\Pki\ASN1\Type\UniversalClass;
/**
* Implements *PrintableString* type.
*/
final class PrintableString extends PrimitiveString
{
use UniversalClass;
private function __construct(string $string)
{
parent::__construct(self::TYPE_PRINTABLE_STRING, $string);
}
public static function create(string $string): self
{
return new self($string);
}
protected function validateString(string $string): bool
{
$chars = preg_quote(" '()+,-./:=?]", '/');
return preg_match('/[^A-Za-z0-9' . $chars . ']/', $string) !== 1;
}
}

View File

@ -0,0 +1,692 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Primitive;
use Brick\Math\BigInteger;
use function chr;
use function count;
use function in_array;
use const INF;
use LogicException;
use function mb_strlen;
use function ord;
use RangeException;
use SpomkyLabs\Pki\ASN1\Component\Identifier;
use SpomkyLabs\Pki\ASN1\Component\Length;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Exception\DecodeException;
use SpomkyLabs\Pki\ASN1\Feature\ElementBase;
use SpomkyLabs\Pki\ASN1\Type\PrimitiveType;
use SpomkyLabs\Pki\ASN1\Type\UniversalClass;
use SpomkyLabs\Pki\ASN1\Util\BigInt;
use Stringable;
use UnexpectedValueException;
/**
* Implements *REAL* type.
*/
final class Real extends Element implements Stringable
{
use UniversalClass;
use PrimitiveType;
/**
* Regex pattern to parse NR1 form number.
*
* @var string
*/
final public const NR1_REGEX = '/^\s*' .
'(?<s>[+\-])?' . // sign
'(?<i>\d+)' . // integer
'$/';
/**
* Regex pattern to parse NR2 form number.
*
* @var string
*/
final public const NR2_REGEX = '/^\s*' .
'(?<s>[+\-])?' . // sign
'(?<d>(?:\d+[\.,]\d*)|(?:\d*[\.,]\d+))' . // decimal number
'$/';
/**
* Regex pattern to parse NR3 form number.
*
* @var string
*/
final public const NR3_REGEX = '/^\s*' .
'(?<ms>[+\-])?' . // mantissa sign
'(?<m>(?:\d+[\.,]\d*)|(?:\d*[\.,]\d+))' . // mantissa
'[Ee](?<es>[+\-])?' . // exponent sign
'(?<e>\d+)' . // exponent
'$/';
/**
* Regex pattern to parse PHP exponent number format.
*
* @see http://php.net/manual/en/language.types.float.php
*
* @var string
*/
final public const PHP_EXPONENT_DNUM = '/^' .
'(?<ms>[+\-])?' . // sign
'(?<m>' .
'\d+' . // LNUM
'|' .
'(?:\d*\.\d+|\d+\.\d*)' . // DNUM
')[eE]' .
'(?<es>[+\-])?(?<e>\d+)' . // exponent
'$/';
/**
* Exponent when value is positive or negative infinite.
*
* @var int
*/
final public const INF_EXPONENT = 2047;
/**
* Exponent bias for IEEE 754 double precision float.
*
* @var int
*/
final public const EXP_BIAS = -1023;
/**
* Signed integer mantissa.
*/
private readonly BigInt $_mantissa;
/**
* Signed integer exponent.
*/
private readonly BigInt $_exponent;
/**
* Abstract value base.
*
* Must be 2 or 10.
*/
private readonly int $_base;
/**
* Whether to encode strictly in DER.
*/
private bool $_strictDer;
/**
* Number as a native float.
*
* @internal Lazily initialized
*/
private ?float $_float = null;
/**
* @param BigInteger|int|string $mantissa Integer mantissa
* @param BigInteger|int|string $exponent Integer exponent
* @param int $base Base, 2 or 10
*/
private function __construct(BigInteger|int|string $mantissa, BigInteger|int|string $exponent, int $base = 10)
{
if ($base !== 10 && $base !== 2) {
throw new UnexpectedValueException('Base must be 2 or 10.');
}
parent::__construct(self::TYPE_REAL);
$this->_strictDer = true;
$this->_mantissa = BigInt::create($mantissa);
$this->_exponent = BigInt::create($exponent);
$this->_base = $base;
}
public function __toString(): string
{
return sprintf('%g', $this->floatVal());
}
public static function create(
BigInteger|int|string $mantissa,
BigInteger|int|string $exponent,
int $base = 10
): self {
return new self($mantissa, $exponent, $base);
}
/**
* Create base 2 real number from float.
*/
public static function fromFloat(float $number): self
{
if (is_infinite($number)) {
return self::fromInfinite($number);
}
if (is_nan($number)) {
throw new UnexpectedValueException('NaN values not supported.');
}
[$m, $e] = self::parse754Double(pack('E', $number));
return self::create($m, $e, 2);
}
/**
* Create base 10 real number from string.
*
* @param string $number Real number in base-10 textual form
*/
public static function fromString(string $number): self
{
[$m, $e] = self::parseString($number);
return self::create($m, $e, 10);
}
/**
* Get self with strict DER flag set or unset.
*
* @param bool $strict whether to encode strictly in DER
*/
public function withStrictDER(bool $strict): self
{
$obj = clone $this;
$obj->_strictDer = $strict;
return $obj;
}
/**
* Get the mantissa.
*/
public function mantissa(): BigInt
{
return $this->_mantissa;
}
/**
* Get the exponent.
*/
public function exponent(): BigInt
{
return $this->_exponent;
}
/**
* Get the base.
*/
public function base(): int
{
return $this->_base;
}
/**
* Get number as a float.
*/
public function floatVal(): float
{
if (! isset($this->_float)) {
$m = $this->_mantissa->toInt();
$e = $this->_exponent->toInt();
$this->_float = (float) ($m * $this->_base ** $e);
}
return $this->_float;
}
/**
* Get number as a NR3 form string conforming to DER rules.
*/
public function nr3Val(): string
{
// convert to base 10
if ($this->_base === 2) {
[$m, $e] = self::parseString(sprintf('%15E', $this->floatVal()));
} else {
$m = $this->_mantissa->getValue();
$e = $this->_exponent->getValue();
}
$zero = BigInteger::of(0);
$ten = BigInteger::of(10);
// shift trailing zeroes from the mantissa to the exponent
// (X.690 07-2002, section 11.3.2.4)
while (! $m->isEqualTo($zero) && $m->mod($ten)->isEqualTo($zero)) {
$m = $m->dividedBy($ten);
$e = $e->plus(1);
}
// if exponent is zero, it must be prefixed with a "+" sign
// (X.690 07-2002, section 11.3.2.6)
if ($e->isEqualTo(0)) {
$es = '+';
} else {
$es = $e->isLessThan(0) ? '-' : '';
}
return sprintf('%s.E%s%s', $m->toBase(10), $es, $e->abs()->toBase(10));
}
protected function encodedAsDER(): string
{
$infExponent = BigInteger::of(self::INF_EXPONENT);
if ($this->_exponent->getValue()->isEqualTo($infExponent)) {
return $this->encodeSpecial();
}
// if the real value is the value zero, there shall be no contents
// octets in the encoding. (X.690 07-2002, section 8.5.2)
if ($this->_mantissa->getValue()->toBase(10) === '0') {
return '';
}
if ($this->_base === 10) {
return $this->encodeDecimal();
}
return $this->encodeBinary();
}
/**
* Encode in binary format.
*/
protected function encodeBinary(): string
{
/** @var BigInteger $m */
/** @var BigInteger $e */
/** @var int $sign */
[$base, $sign, $m, $e] = $this->prepareBinaryEncoding();
$zero = BigInteger::of(0);
$byte = 0x80;
if ($sign < 0) {
$byte |= 0x40;
}
// normalization: mantissa must be 0 or odd
if ($base === 2) {
// while last bit is zero
while ($m->isGreaterThan(0) && $m->and(0x01)->isEqualTo($zero)) {
$m = $m->shiftedRight(1);
$e = $e->plus(1);
}
} elseif ($base === 8) {
$byte |= 0x10;
// while last 3 bits are zero
while ($m->isGreaterThan(0) && $m->and(0x07)->isEqualTo($zero)) {
$m = $m->shiftedRight(3);
$e = $e->plus(1);
}
} else { // base === 16
$byte |= 0x20;
// while last 4 bits are zero
while ($m->isGreaterThan(0) && $m->and(0x0f)->isEqualTo($zero)) {
$m = $m->shiftedRight(4);
$e = $e->plus(1);
}
}
// scale factor
$scale = 0;
while ($m->isGreaterThan(0) && $m->and(0x01)->isEqualTo($zero)) {
$m = $m->shiftedRight(1);
++$scale;
}
$byte |= ($scale & 0x03) << 2;
// encode exponent
$exp_bytes = (BigInt::create($e))->signedOctets();
$exp_len = mb_strlen($exp_bytes, '8bit');
if ($exp_len > 0xff) {
throw new RangeException('Exponent encoding is too long.');
}
if ($exp_len <= 3) {
$byte |= ($exp_len - 1) & 0x03;
$bytes = chr($byte);
} else {
$byte |= 0x03;
$bytes = chr($byte) . chr($exp_len);
}
$bytes .= $exp_bytes;
// encode mantissa
$bytes .= (BigInt::create($m))->unsignedOctets();
return $bytes;
}
/**
* Encode in decimal format.
*/
protected function encodeDecimal(): string
{
// encode in NR3 decimal encoding
return chr(0x03) . $this->nr3Val();
}
/**
* Encode special value.
*/
protected function encodeSpecial(): string
{
return match ($this->_mantissa->toInt()) {
1 => chr(0x40),
-1 => chr(0x41),
default => throw new LogicException('Invalid special value.'),
};
}
protected static function decodeFromDER(Identifier $identifier, string $data, int &$offset): ElementBase
{
$idx = $offset;
$length = Length::expectFromDER($data, $idx)->intLength();
// if length is zero, value is zero (spec 8.5.2)
if ($length === 0) {
$obj = self::create(0, 0, 10);
} else {
$bytes = mb_substr($data, $idx, $length, '8bit');
$byte = ord($bytes[0]);
if ((0x80 & $byte) !== 0) { // bit 8 = 1
$obj = self::decodeBinaryEncoding($bytes);
} elseif ($byte >> 6 === 0x00) { // bit 8 = 0, bit 7 = 0
$obj = self::decodeDecimalEncoding($bytes);
} else { // bit 8 = 0, bit 7 = 1
$obj = self::decodeSpecialRealValue($bytes);
}
}
$offset = $idx + $length;
return $obj;
}
/**
* Decode binary encoding.
*/
protected static function decodeBinaryEncoding(string $data): self
{
$byte = ord($data[0]);
// bit 7 is set if mantissa is negative
$neg = (bool) (0x40 & $byte);
$base = match (($byte >> 4) & 0x03) {
0b00 => 2,
0b01 => 8,
0b10 => 16,
default => throw new DecodeException('Reserved REAL binary encoding base not supported.'),
};
// scaling factor in bits 4 and 3
$scale = ($byte >> 2) & 0x03;
$idx = 1;
// content length in bits 2 and 1
$len = ($byte & 0x03) + 1;
// if both bits are set, the next octet encodes the length
if ($len > 3) {
if (mb_strlen($data, '8bit') < 2) {
throw new DecodeException('Unexpected end of data while decoding REAL exponent length.');
}
$len = ord($data[1]);
$idx = 2;
}
if (mb_strlen($data, '8bit') < $idx + $len) {
throw new DecodeException('Unexpected end of data while decoding REAL exponent.');
}
// decode exponent
$octets = mb_substr($data, $idx, $len, '8bit');
$exp = BigInt::fromSignedOctets($octets)->getValue();
if ($base === 8) {
$exp = $exp->multipliedBy(3);
} elseif ($base === 16) {
$exp = $exp->multipliedBy(4);
}
if (mb_strlen($data, '8bit') <= $idx + $len) {
throw new DecodeException('Unexpected end of data while decoding REAL mantissa.');
}
// decode mantissa
$octets = mb_substr($data, $idx + $len, null, '8bit');
$n = BigInt::fromUnsignedOctets($octets)->getValue();
$n = $n->multipliedBy(2 ** $scale);
if ($neg) {
$n = $n->negated();
}
return self::create($n, $exp, 2);
}
/**
* Decode decimal encoding.
*/
protected static function decodeDecimalEncoding(string $data): self
{
$nr = ord($data[0]) & 0x3f;
if (! in_array($nr, [1, 2, 3], true)) {
throw new DecodeException('Unsupported decimal encoding form.');
}
$str = mb_substr($data, 1, null, '8bit');
return self::fromString($str);
}
/**
* Decode special encoding.
*/
protected static function decodeSpecialRealValue(string $data): self
{
if (mb_strlen($data, '8bit') !== 1) {
throw new DecodeException('SpecialRealValue must have one content octet.');
}
$byte = ord($data[0]);
if ($byte === 0x40) { // positive infinity
return self::fromInfinite(INF);
}
if ($byte === 0x41) { // negative infinity
return self::fromInfinite(-INF);
}
throw new DecodeException('Invalid SpecialRealValue encoding.');
}
/**
* Prepare value for binary encoding.
*
* @return array<int|BigInteger> (int) base, (int) sign, (BigInteger) mantissa and (BigInteger) exponent
*/
protected function prepareBinaryEncoding(): array
{
$base = 2;
$m = $this->_mantissa->getValue();
$ms = $m->getSign();
$m = BigInteger::of($m->abs());
$e = $this->_exponent->getValue();
$es = $e->getSign();
$e = BigInteger::of($e->abs());
$zero = BigInteger::of(0);
$three = BigInteger::of(3);
$four = BigInteger::of(4);
// DER uses only base 2 binary encoding
if (! $this->_strictDer) {
if ($e->mod($four)->isEqualTo($zero)) {
$base = 16;
$e = $e->dividedBy(4);
} elseif ($e->mod($three)->isEqualTo($zero)) {
$base = 8;
$e = $e->dividedBy(3);
}
}
return [$base, $ms, $m, $e->multipliedBy($es)];
}
/**
* Initialize from INF or -INF.
*/
private static function fromInfinite(float $inf): self
{
return self::create($inf === -INF ? -1 : 1, self::INF_EXPONENT, 2);
}
/**
* Parse IEEE 754 big endian formatted double precision float to base 2 mantissa and exponent.
*
* @param string $octets 64 bits
*
* @return BigInteger[] Tuple of mantissa and exponent
*/
private static function parse754Double(string $octets): array
{
$n = BigInteger::fromBytes($octets, false);
// sign bit
$neg = $n->testBit(63);
// 11 bits of biased exponent
$exponentMask = BigInteger::fromBase('7ff0000000000000', 16);
$exp = $n->and($exponentMask)
->shiftedRight(52)
->plus(self::EXP_BIAS);
// 52 bits of mantissa
$mantissaMask = BigInteger::fromBase('fffffffffffff', 16);
$man = $n->and($mantissaMask);
// zero, ASN.1 doesn't differentiate -0 from +0
$zero = BigInteger::of(0);
if ($exp->isEqualTo(self::EXP_BIAS) && $man->isEqualTo($zero)) {
return [BigInteger::of(0), BigInteger::of(0)];
}
// denormalized value, shift binary point
if ($exp->isEqualTo(self::EXP_BIAS)) {
$exp = $exp->plus(1);
} // normalized value, insert implicit leading one before the binary point
else {
$man = $man->or(BigInteger::of(1)->shiftedLeft(52));
}
// find the last fraction bit that is set
$last = 0;
while (! $man->testBit($last) && $last !== 52) {
$last++;
}
$bits_for_fraction = 52 - $last;
// adjust mantissa and exponent so that we have integer values
$man = $man->shiftedRight($last);
$exp = $exp->minus($bits_for_fraction);
// negate mantissa if number was negative
if ($neg) {
$man = $man->negated();
}
return [$man, $exp];
}
/**
* Parse textual REAL number to base 10 mantissa and exponent.
*
* @param string $str Number
*
* @return BigInteger[] Tuple of mantissa and exponent
*/
private static function parseString(string $str): array
{
// PHP exponent format
if (preg_match(self::PHP_EXPONENT_DNUM, $str, $match) === 1) {
[$m, $e] = self::parsePHPExponentMatch($match);
} // NR3 format
elseif (preg_match(self::NR3_REGEX, $str, $match) === 1) {
[$m, $e] = self::parseNR3Match($match);
} // NR2 format
elseif (preg_match(self::NR2_REGEX, $str, $match) === 1) {
[$m, $e] = self::parseNR2Match($match);
} // NR1 format
elseif (preg_match(self::NR1_REGEX, $str, $match) === 1) {
[$m, $e] = self::parseNR1Match($match);
} // invalid number
else {
throw new UnexpectedValueException("{$str} could not be parsed to REAL.");
}
// normalize so that mantissa has no trailing zeroes
$zero = BigInteger::of(0);
$ten = BigInteger::of(10);
while (! $m->isEqualTo($zero) && $m->mod($ten)->isEqualTo($zero)) {
$m = $m->dividedBy($ten);
$e = $e->plus(1);
}
return [$m, $e];
}
/**
* Parse PHP form float to base 10 mantissa and exponent.
*
* @param array<string> $match Regexp match
*
* @return BigInteger[] Tuple of mantissa and exponent
*/
private static function parsePHPExponentMatch(array $match): array
{
// mantissa sign
$ms = $match['ms'] === '-' ? -1 : 1;
$m_parts = explode('.', $match['m']);
// integer part of the mantissa
$int = ltrim($m_parts[0], '0');
// exponent sign
$es = $match['es'] === '-' ? -1 : 1;
// signed exponent
$e = BigInteger::of($match['e'])->multipliedBy($es);
// if mantissa had fractional part
if (count($m_parts) === 2) {
$frac = rtrim($m_parts[1], '0');
$e = $e->minus(mb_strlen($frac, '8bit'));
$int .= $frac;
}
$m = BigInteger::of($int)->multipliedBy($ms);
return [$m, $e];
}
/**
* Parse NR3 form number to base 10 mantissa and exponent.
*
* @param array<string> $match Regexp match
*
* @return BigInteger[] Tuple of mantissa and exponent
*/
private static function parseNR3Match(array $match): array
{
// mantissa sign
$ms = $match['ms'] === '-' ? -1 : 1;
// explode mantissa to integer and fraction parts
[$int, $frac] = explode('.', str_replace(',', '.', $match['m']));
$int = ltrim($int, '0');
$frac = rtrim($frac, '0');
// exponent sign
$es = $match['es'] === '-' ? -1 : 1;
// signed exponent
$e = BigInteger::of($match['e'])->multipliedBy($es);
// shift exponent by the number of base 10 fractions
$e = $e->minus(mb_strlen($frac, '8bit'));
// insert fractions to integer part and produce signed mantissa
$int .= $frac;
if ($int === '') {
$int = '0';
}
$m = BigInteger::of($int)->multipliedBy($ms);
return [$m, $e];
}
/**
* Parse NR2 form number to base 10 mantissa and exponent.
*
* @param array<string> $match Regexp match
*
* @return BigInteger[] Tuple of mantissa and exponent
*/
private static function parseNR2Match(array $match): array
{
$sign = $match['s'] === '-' ? -1 : 1;
// explode decimal number to integer and fraction parts
[$int, $frac] = explode('.', str_replace(',', '.', $match['d']));
$int = ltrim($int, '0');
$frac = rtrim($frac, '0');
// shift exponent by the number of base 10 fractions
$e = BigInteger::of(0);
$e = $e->minus(mb_strlen($frac, '8bit'));
// insert fractions to integer part and produce signed mantissa
$int .= $frac;
if ($int === '') {
$int = '0';
}
$m = BigInteger::of($int)->multipliedBy($sign);
return [$m, $e];
}
/**
* Parse NR1 form number to base 10 mantissa and exponent.
*
* @param array<string> $match Regexp match
*
* @return BigInteger[] Tuple of mantissa and exponent
*/
private static function parseNR1Match(array $match): array
{
$sign = $match['s'] === '-' ? -1 : 1;
$int = ltrim($match['i'], '0');
if ($int === '') {
$int = '0';
}
$m = BigInteger::of($int)->multipliedBy($sign);
return [$m, BigInteger::of(0)];
}
}

View File

@ -0,0 +1,163 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Primitive;
use Brick\Math\BigInteger;
use function chr;
use function is_int;
use function ord;
use RuntimeException;
use SpomkyLabs\Pki\ASN1\Component\Identifier;
use SpomkyLabs\Pki\ASN1\Component\Length;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Exception\DecodeException;
use SpomkyLabs\Pki\ASN1\Feature\ElementBase;
use SpomkyLabs\Pki\ASN1\Type\PrimitiveType;
use SpomkyLabs\Pki\ASN1\Type\UniversalClass;
use Throwable;
use UnexpectedValueException;
/**
* Implements *RELATIVE-OID* type.
*/
final class RelativeOID extends Element
{
use UniversalClass;
use PrimitiveType;
/**
* Object identifier split to sub ID's.
*
* @var BigInteger[]
*/
private readonly array $subids;
/**
* @param string $oid OID in dotted format
*/
private function __construct(
private readonly string $oid
) {
parent::__construct(self::TYPE_RELATIVE_OID);
$this->subids = self::explodeDottedOID($oid);
}
public static function create(string $oid): self
{
return new self($oid);
}
/**
* Get OID in dotted format.
*/
public function oid(): string
{
return $this->oid;
}
protected function encodedAsDER(): string
{
return self::encodeSubIDs(...$this->subids);
}
protected static function decodeFromDER(Identifier $identifier, string $data, int &$offset): ElementBase
{
$idx = $offset;
$len = Length::expectFromDER($data, $idx)->intLength();
$subids = self::decodeSubIDs(mb_substr($data, $idx, $len, '8bit'));
$offset = $idx + $len;
return self::create(self::implodeSubIDs(...$subids));
}
/**
* Explode dotted OID to an array of sub ID's.
*
* @param string $oid OID in dotted format
*
* @return BigInteger[] Array of BigInteger numbers
*/
protected static function explodeDottedOID(string $oid): array
{
$subids = [];
if ($oid !== '') {
foreach (explode('.', $oid) as $subid) {
try {
$n = BigInteger::of($subid);
$subids[] = $n;
} catch (Throwable $e) {
throw new UnexpectedValueException(sprintf('"%s" is not a number.', $subid), 0, $e);
}
}
}
return $subids;
}
/**
* Implode an array of sub IDs to dotted OID format.
*/
protected static function implodeSubIDs(BigInteger ...$subids): string
{
return implode('.', array_map(static fn ($num) => $num->toBase(10), $subids));
}
/**
* Encode sub ID's to DER.
*/
protected static function encodeSubIDs(BigInteger ...$subids): string
{
$data = '';
foreach ($subids as $subid) {
// if number fits to one base 128 byte
if ($subid->isLessThan(128)) {
$data .= chr($subid->toInt());
} else { // encode to multiple bytes
$bytes = [];
do {
array_unshift($bytes, 0x7f & $subid->toInt());
$subid = $subid->shiftedRight(7);
} while ($subid->isGreaterThan(0));
// all bytes except last must have bit 8 set to one
foreach (array_splice($bytes, 0, -1) as $byte) {
$data .= chr(0x80 | $byte);
}
$byte = reset($bytes);
if (! is_int($byte)) {
throw new RuntimeException('Encoding failed');
}
$data .= chr($byte);
}
}
return $data;
}
/**
* Decode sub ID's from DER data.
*
* @return BigInteger[] Array of BigInteger numbers
*/
protected static function decodeSubIDs(string $data): array
{
$subids = [];
$idx = 0;
$end = mb_strlen($data, '8bit');
while ($idx < $end) {
$num = BigInteger::of(0);
while (true) {
if ($idx >= $end) {
throw new DecodeException('Unexpected end of data.');
}
$byte = ord($data[$idx++]);
$num = $num->or($byte & 0x7f);
// bit 8 of the last octet is zero
if (0 === ($byte & 0x80)) {
break;
}
$num = $num->shiftedLeft(7);
}
$subids[] = $num;
}
return $subids;
}
}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Primitive;
use SpomkyLabs\Pki\ASN1\Type\PrimitiveString;
use SpomkyLabs\Pki\ASN1\Type\UniversalClass;
/**
* Implements *T61String* type.
*/
final class T61String extends PrimitiveString
{
use UniversalClass;
private function __construct(string $string)
{
parent::__construct(self::TYPE_T61_STRING, $string);
}
public static function create(string $string): self
{
return new self($string);
}
protected function validateString(string $string): bool
{
// allow everything since there's literally
// thousands of allowed characters (16 bit composed characters)
return true;
}
}

View File

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Primitive;
use DateTimeImmutable;
use DateTimeZone;
use SpomkyLabs\Pki\ASN1\Component\Identifier;
use SpomkyLabs\Pki\ASN1\Component\Length;
use SpomkyLabs\Pki\ASN1\Exception\DecodeException;
use SpomkyLabs\Pki\ASN1\Feature\ElementBase;
use SpomkyLabs\Pki\ASN1\Type\BaseTime;
use SpomkyLabs\Pki\ASN1\Type\PrimitiveType;
use SpomkyLabs\Pki\ASN1\Type\UniversalClass;
/**
* Implements *UTCTime* type.
*/
final class UTCTime extends BaseTime
{
use UniversalClass;
use PrimitiveType;
/**
* Regular expression to parse date.
*
* DER restricts format to UTC timezone (Z suffix).
*
* @var string
*/
final public const REGEX = '#^' .
'(\d\d)' . // YY
'(\d\d)' . // MM
'(\d\d)' . // DD
'(\d\d)' . // hh
'(\d\d)' . // mm
'(\d\d)' . // ss
'Z' . // TZ
'$#';
private function __construct(DateTimeImmutable $dt)
{
parent::__construct(self::TYPE_UTC_TIME, $dt);
}
public static function create(DateTimeImmutable $dt): self
{
return new self($dt);
}
public static function fromString(string $time): static
{
return new static(new DateTimeImmutable($time, new DateTimeZone('UTC')));
}
protected function encodedAsDER(): string
{
$dt = $this->dateTime->setTimezone(new DateTimeZone('UTC'));
return $dt->format('ymdHis\\Z');
}
protected static function decodeFromDER(Identifier $identifier, string $data, int &$offset): ElementBase
{
$idx = $offset;
$length = Length::expectFromDER($data, $idx)->intLength();
$str = mb_substr($data, $idx, $length, '8bit');
$idx += $length;
if (preg_match(self::REGEX, $str, $match) !== 1) {
throw new DecodeException('Invalid UTCTime format.');
}
[, $year, $month, $day, $hour, $minute, $second] = $match;
$time = $year . $month . $day . $hour . $minute . $second . self::TZ_UTC;
$dt = DateTimeImmutable::createFromFormat('!ymdHisT', $time, new DateTimeZone('UTC'));
if ($dt === false) {
throw new DecodeException('Failed to decode UTCTime');
}
$offset = $idx;
return self::create($dt);
}
}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Primitive;
use SpomkyLabs\Pki\ASN1\Type\PrimitiveString;
use SpomkyLabs\Pki\ASN1\Type\UniversalClass;
/**
* Implements *UTF8String* type.
*
* UTF8String* is an Unicode string with UTF-8 encoding.
*/
final class UTF8String extends PrimitiveString
{
use UniversalClass;
private function __construct(string $string)
{
parent::__construct(self::TYPE_UTF8_STRING, $string);
}
public static function create(string $string): self
{
return new self($string);
}
protected function validateString(string $string): bool
{
return mb_check_encoding($string, 'UTF-8');
}
}

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Primitive;
use function mb_strlen;
use SpomkyLabs\Pki\ASN1\Type\PrimitiveString;
use SpomkyLabs\Pki\ASN1\Type\UniversalClass;
/**
* Implements *UniversalString* type.
*
* Universal string is an Unicode string with UCS-4 encoding.
*/
final class UniversalString extends PrimitiveString
{
use UniversalClass;
private function __construct(string $string)
{
parent::__construct(self::TYPE_UNIVERSAL_STRING, $string);
}
public static function create(string $string): self
{
return new self($string);
}
protected function validateString(string $string): bool
{
// UCS-4 has fixed with of 4 octets (32 bits)
if (mb_strlen($string, '8bit') % 4 !== 0) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Primitive;
use SpomkyLabs\Pki\ASN1\Type\PrimitiveString;
use SpomkyLabs\Pki\ASN1\Type\UniversalClass;
/**
* Implements *VideotexString* type.
*/
final class VideotexString extends PrimitiveString
{
use UniversalClass;
private function __construct(string $string)
{
parent::__construct(self::TYPE_VIDEOTEX_STRING, $string);
}
public static function create(string $string): self
{
return new self($string);
}
protected function validateString(string $string): bool
{
// allow everything
return true;
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Primitive;
use SpomkyLabs\Pki\ASN1\Type\PrimitiveString;
use SpomkyLabs\Pki\ASN1\Type\UniversalClass;
/**
* Implements *VisibleString* type.
*/
final class VisibleString extends PrimitiveString
{
use UniversalClass;
private function __construct(string $string)
{
parent::__construct(self::TYPE_VISIBLE_STRING, $string);
}
public static function create(string $string): self
{
return new self($string);
}
protected function validateString(string $string): bool
{
return preg_match('/[^\x20-\x7e]/', $string) !== 1;
}
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type;
use InvalidArgumentException;
use SpomkyLabs\Pki\ASN1\Component\Identifier;
use SpomkyLabs\Pki\ASN1\Component\Length;
use SpomkyLabs\Pki\ASN1\Exception\DecodeException;
/**
* Base class for primitive strings.
*
* Used by types that don't require special processing of the encoded string data.
*
* @internal
*/
abstract class PrimitiveString extends BaseString
{
use PrimitiveType;
abstract public static function create(string $string): self;
protected static function decodeFromDER(Identifier $identifier, string $data, int &$offset): static
{
$idx = $offset;
if (! $identifier->isPrimitive()) {
throw new DecodeException('DER encoded string must be primitive.');
}
$length = Length::expectFromDER($data, $idx)->intLength();
$str = $length === 0 ? '' : mb_substr($data, $idx, $length, '8bit');
$offset = $idx + $length;
try {
return static::create($str);
} catch (InvalidArgumentException $e) {
throw new DecodeException($e->getMessage(), 0, $e);
}
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type;
/**
* Trait for primitive types.
*/
trait PrimitiveType
{
/**
* @see \Sop\ASN1\Feature\ElementBase::isConstructed()
*/
public function isConstructed(): bool
{
return false;
}
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type;
use SpomkyLabs\Pki\ASN1\Feature\ElementBase;
use SpomkyLabs\Pki\ASN1\Feature\Stringable;
/**
* Interface to mark types that correspond to ASN.1 specification's character strings. That being all simple strings and
* time types.
*/
interface StringType extends ElementBase, Stringable
{
}

View File

@ -0,0 +1,281 @@
<?php
/** @noinspection ALL */
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type;
use ArrayIterator;
use function count;
use Countable;
use IteratorAggregate;
use LogicException;
use OutOfBoundsException;
use SpomkyLabs\Pki\ASN1\Component\Identifier;
use SpomkyLabs\Pki\ASN1\Component\Length;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Exception\DecodeException;
use SpomkyLabs\Pki\ASN1\Feature\ElementBase;
/**
* Base class for the constructed types.
*/
abstract class Structure extends Element implements Countable, IteratorAggregate
{
use UniversalClass;
/**
* Array of elements in the structure.
*
* @var Element[]
*/
protected array $elements;
/**
* Lookup table for the tagged elements.
*
* @var null|Element[]
*/
private ?array $taggedMap = null;
/**
* Cache variable of elements wrapped into `UnspecifiedType` objects.
*
* @var null|UnspecifiedType[]
*/
private ?array $unspecifiedTypes = null;
/**
* @param ElementBase ...$elements Any number of elements
*/
protected function __construct(int $typeTag, ElementBase ...$elements)
{
parent::__construct($typeTag);
$this->elements = array_map(static fn (ElementBase $el) => $el->asElement(), $elements);
}
/**
* Clone magic method.
*/
public function __clone()
{
// clear cache-variables
$this->taggedMap = null;
$this->unspecifiedTypes = null;
}
public function isConstructed(): bool
{
return true;
}
/**
* Explode DER structure to DER encoded components that it contains.
*
* @return string[]
*/
public static function explodeDER(string $data): array
{
$offset = 0;
$identifier = Identifier::fromDER($data, $offset);
if (! $identifier->isConstructed()) {
throw new DecodeException('Element is not constructed.');
}
$length = Length::expectFromDER($data, $offset);
if ($length->isIndefinite()) {
throw new DecodeException('Explode not implemented for indefinite length encoding.');
}
$end = $offset + $length->intLength();
$parts = [];
while ($offset < $end) {
// start of the element
$idx = $offset;
// skip identifier
Identifier::fromDER($data, $offset);
// decode element length
$length = Length::expectFromDER($data, $offset)->intLength();
// extract der encoding of the element
$parts[] = mb_substr($data, $idx, $offset - $idx + $length, '8bit');
// update offset over content
$offset += $length;
}
return $parts;
}
/**
* Get self with an element at the given index replaced by another.
*
* @param int $idx Element index
* @param Element $el New element to insert into the structure
*/
public function withReplaced(int $idx, Element $el): self
{
if (! isset($this->elements[$idx])) {
throw new OutOfBoundsException("Structure doesn't have element at index {$idx}.");
}
$obj = clone $this;
$obj->elements[$idx] = $el;
return $obj;
}
/**
* Get self with an element inserted before the given index.
*
* @param int $idx Element index
* @param Element $el New element to insert into the structure
*/
public function withInserted(int $idx, Element $el): self
{
if (count($this->elements) < $idx || $idx < 0) {
throw new OutOfBoundsException("Index {$idx} is out of bounds.");
}
$obj = clone $this;
array_splice($obj->elements, $idx, 0, [$el]);
return $obj;
}
/**
* Get self with an element appended to the end.
*
* @param Element $el Element to insert into the structure
*/
public function withAppended(Element $el): self
{
$obj = clone $this;
array_push($obj->elements, $el);
return $obj;
}
/**
* Get self with an element prepended in the beginning.
*
* @param Element $el Element to insert into the structure
*/
public function withPrepended(Element $el): self
{
$obj = clone $this;
array_unshift($obj->elements, $el);
return $obj;
}
/**
* Get self with an element at the given index removed.
*
* @param int $idx Element index
*/
public function withoutElement(int $idx): self
{
if (! isset($this->elements[$idx])) {
throw new OutOfBoundsException("Structure doesn't have element at index {$idx}.");
}
$obj = clone $this;
array_splice($obj->elements, $idx, 1);
return $obj;
}
/**
* Get elements in the structure.
*
* @return UnspecifiedType[]
*/
public function elements(): array
{
if (! isset($this->unspecifiedTypes)) {
$this->unspecifiedTypes = array_map(
static fn (Element $el) => UnspecifiedType::create($el),
$this->elements
);
}
return $this->unspecifiedTypes;
}
/**
* Check whether the structure has an element at the given index, optionally satisfying given tag expectation.
*
* @param int $idx Index 0..n
* @param null|int $expectedTag Optional type tag expectation
*/
public function has(int $idx, ?int $expectedTag = null): bool
{
if (! isset($this->elements[$idx])) {
return false;
}
if (isset($expectedTag)) {
if (! $this->elements[$idx]->isType($expectedTag)) {
return false;
}
}
return true;
}
/**
* Get the element at the given index, optionally checking that the element has a given tag.
*
* @param int $idx Index 0..n
*/
public function at(int $idx): UnspecifiedType
{
if (! isset($this->elements[$idx])) {
throw new OutOfBoundsException("Structure doesn't have an element at index {$idx}.");
}
return UnspecifiedType::create($this->elements[$idx]);
}
/**
* Check whether the structure contains a context specific element with a given tag.
*
* @param int $tag Tag number
*/
public function hasTagged(int $tag): bool
{
// lazily build lookup map
if (! isset($this->taggedMap)) {
$this->taggedMap = [];
foreach ($this->elements as $element) {
if ($element->isTagged()) {
$this->taggedMap[$element->tag()] = $element;
}
}
}
return isset($this->taggedMap[$tag]);
}
/**
* Get a context specific element tagged with a given tag.
*/
public function getTagged(int $tag): TaggedType
{
if (! $this->hasTagged($tag)) {
throw new LogicException("No tagged element for tag {$tag}.");
}
return $this->taggedMap[$tag];
}
/**
* @see \Countable::count()
*/
public function count(): int
{
return count($this->elements);
}
/**
* Get an iterator for the `UnspecifiedElement` objects.
*
* @see \IteratorAggregate::getIterator()
*/
public function getIterator(): ArrayIterator
{
return new ArrayIterator($this->elements());
}
protected function encodedAsDER(): string
{
$data = '';
foreach ($this->elements as $element) {
$data .= $element->toDER();
}
return $data;
}
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Tagged;
/**
* Intermediate class to store DER data of an application specific type.
*/
final class ApplicationType extends DERTaggedType
{
}

Some files were not shown because too many files have changed in this diff Show More