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,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
{
}

View File

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

View File

@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Tagged;
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\TaggedType;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
/**
* Intermediate class to store tagged DER data.
*
* `implicit($tag)` or `explicit()` method is used to decode the actual element, which is only known by the abstract
* syntax of data structure.
*
* May be encoded back to complete DER encoding.
*/
class DERTaggedType extends TaggedType implements ExplicitTagging, ImplicitTagging
{
/**
* @param Identifier $_identifier Pre-parsed identifier
* @param string $_data DER data
* @param int $_offset Offset to next byte after identifier
* @param int $_valueOffset Offset to content
* @param int $_valueLength Content length
*/
final private function __construct(
private readonly Identifier $_identifier,
private readonly string $_data,
private readonly int $_offset,
private readonly int $_valueOffset,
private readonly int $_valueLength,
bool $indefinite_length
) {
parent::__construct($_identifier->intTag(), $indefinite_length);
}
public static function create(
Identifier $_identifier,
string $_data,
int $_offset,
int $_valueOffset,
int $_valueLength,
bool $indefinite_length
): static {
return new static($_identifier, $_data, $_offset, $_valueOffset, $_valueLength, $indefinite_length);
}
public function typeClass(): int
{
return $this->_identifier->typeClass();
}
public function isConstructed(): bool
{
return $this->_identifier->isConstructed();
}
public function implicit(int $tag, int $class = Identifier::CLASS_UNIVERSAL): UnspecifiedType
{
$identifier = $this->_identifier->withClass($class)
->withTag($tag);
$cls = self::determineImplClass($identifier);
$idx = $this->_offset;
/** @var ElementBase $element */
$element = $cls::decodeFromDER($identifier, $this->_data, $idx);
return $element->asUnspecified();
}
public function explicit(): UnspecifiedType
{
$idx = $this->_valueOffset;
return Element::fromDER($this->_data, $idx)->asUnspecified();
}
protected static function decodeFromDER(Identifier $identifier, string $data, int &$offset): ElementBase
{
$idx = $offset;
$length = Length::expectFromDER($data, $idx);
// offset to inner value
$value_offset = $idx;
if ($length->isIndefinite()) {
if ($identifier->isPrimitive()) {
throw new DecodeException('Primitive type with indefinite length is not supported.');
}
// EOC consists of two octets.
$value_length = $idx - $value_offset - 2;
} else {
$value_length = $length->intLength();
$idx += $value_length;
}
// late static binding since ApplicationType and PrivateType extend this class
$type = static::create($identifier, $data, $offset, $value_offset, $value_length, $length->isIndefinite());
$offset = $idx;
return $type;
}
protected function encodedAsDER(): string
{
return mb_substr($this->_data, $this->_valueOffset, $this->_valueLength, '8bit');
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Tagged;
use SpomkyLabs\Pki\ASN1\Feature\ElementBase;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
/**
* Interface for classes providing explicit tagging.
*/
interface ExplicitTagging extends ElementBase
{
/**
* Get explicitly tagged wrapped element.
*/
public function explicit(): UnspecifiedType;
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Tagged;
use BadMethodCallException;
use SpomkyLabs\Pki\ASN1\Component\Identifier;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Feature\ElementBase;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
/**
* Implements explicit tagging mode.
*
* Explicit tagging wraps a type by prepending a tag. Underlying DER encoding is not changed.
*/
final class ExplicitlyTaggedType extends TaggedTypeWrap implements ExplicitTagging
{
public static function create(int $tag, Element $element, int $class = Identifier::CLASS_CONTEXT_SPECIFIC): self
{
return new self($element, $class, $tag);
}
public function isConstructed(): bool
{
return true;
}
public function explicit(): UnspecifiedType
{
return $this->element->asUnspecified();
}
protected function encodedAsDER(): string
{
// get the full encoding of the wrapped element
return $this->element->toDER();
}
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,23 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Tagged;
use SpomkyLabs\Pki\ASN1\Component\Identifier;
use SpomkyLabs\Pki\ASN1\Feature\ElementBase;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
/**
* Interface for classes providing implicit tagging.
*/
interface ImplicitTagging extends ElementBase
{
/**
* Get implicitly tagged wrapped element.
*
* @param int $tag Tag of the element
* @param int $class Expected type class of the element
*/
public function implicit(int $tag, int $class = Identifier::CLASS_UNIVERSAL): UnspecifiedType;
}

View File

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Tagged;
use BadMethodCallException;
use SpomkyLabs\Pki\ASN1\Component\Identifier;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Feature\ElementBase;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
use UnexpectedValueException;
/**
* Implements implicit tagging mode.
*
* Implicit tagging changes the tag of the tagged type. This changes the DER encoding of the type, and hence the
* abstract syntax must be known when decoding the data.
*/
final class ImplicitlyTaggedType extends TaggedTypeWrap implements ImplicitTagging
{
public static function create(int $tag, Element $element, int $class = Identifier::CLASS_CONTEXT_SPECIFIC): self
{
return new self($element, $class, $tag);
}
public function isConstructed(): bool
{
// depends on the underlying type
return $this->element->isConstructed();
}
public function implicit(int $tag, int $class = Identifier::CLASS_UNIVERSAL): UnspecifiedType
{
$this->element->expectType($tag);
if ($this->element->typeClass() !== $class) {
throw new UnexpectedValueException(
sprintf(
'Type class %s expected, got %s.',
Identifier::classToName($class),
Identifier::classToName($this->element->typeClass())
)
);
}
return $this->element->asUnspecified();
}
protected function encodedAsDER(): string
{
// get only the content of the wrapped element.
return $this->element->encodedAsDER();
}
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,12 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Tagged;
/**
* Intermediate class to store DER data of a private tagging type.
*/
final class PrivateType extends DERTaggedType
{
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type\Tagged;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Type\TaggedType;
/**
* Base class to wrap inner element for tagging.
*/
abstract class TaggedTypeWrap extends TaggedType
{
protected function __construct(
protected readonly Element $element,
private readonly int $class,
int $typeTag
) {
parent::__construct($typeTag);
}
public function typeClass(): int
{
return $this->class;
}
}

View File

@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type;
use SpomkyLabs\Pki\ASN1\Component\Identifier;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Type\Tagged\ExplicitTagging;
use SpomkyLabs\Pki\ASN1\Type\Tagged\ImplicitTagging;
use UnexpectedValueException;
/**
* Base class for context-specific types.
*/
abstract class TaggedType extends Element
{
/**
* Check whether element supports explicit tagging.
*
* @param null|int $expectedTag Optional outer tag expectation
*/
public function expectExplicit(?int $expectedTag = null): ExplicitTagging
{
$el = $this;
if (! $el instanceof ExplicitTagging) {
throw new UnexpectedValueException("Element doesn't implement explicit tagging.");
}
if (isset($expectedTag)) {
$el->expectTagged($expectedTag);
}
return $el;
}
/**
* Get the wrapped inner element employing explicit tagging.
*
* @param null|int $expectedTag Optional outer tag expectation
*/
public function asExplicit(?int $expectedTag = null): UnspecifiedType
{
return $this->expectExplicit($expectedTag)
->explicit();
}
/**
* Check whether element supports implicit tagging.
*
* @param null|int $expectedTag Optional outer tag expectation
*/
public function expectImplicit(?int $expectedTag = null): ImplicitTagging
{
$el = $this;
if (! $el instanceof ImplicitTagging) {
throw new UnexpectedValueException("Element doesn't implement implicit tagging.");
}
if (isset($expectedTag)) {
$el->expectTagged($expectedTag);
}
return $el;
}
/**
* Get the wrapped inner element employing implicit tagging.
*
* @param int $tag Type tag of the inner element
* @param null|int $expectedTag Optional outer tag expectation
* @param int $expectedClass Optional inner type class expectation
*/
public function asImplicit(
int $tag,
?int $expectedTag = null,
int $expectedClass = Identifier::CLASS_UNIVERSAL
): UnspecifiedType {
return $this->expectImplicit($expectedTag)
->implicit($tag, $expectedClass);
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type;
use DateTimeImmutable;
/**
* Interface to mark types that encode a time as a string.
*/
interface TimeType extends StringType
{
/**
* Get the date and time.
*/
public function dateTime(): DateTimeImmutable;
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type;
use SpomkyLabs\Pki\ASN1\Component\Identifier;
/**
* Trait for types of universal class.
*/
trait UniversalClass
{
/**
* @see \Sop\ASN1\Feature\ElementBase::typeClass()
*/
public function typeClass(): int
{
return Identifier::CLASS_UNIVERSAL;
}
}

View File

@ -0,0 +1,518 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Type;
use SpomkyLabs\Pki\ASN1\Component\Identifier;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Feature\ElementBase;
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\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\Tagged\ApplicationType;
use SpomkyLabs\Pki\ASN1\Type\Tagged\PrivateType;
use UnexpectedValueException;
/**
* Decorator class to wrap an element without already knowing the specific underlying type.
*
* Provides accessor methods to test the underlying type and return a type hinted instance of the concrete element.
*/
final class UnspecifiedType implements ElementBase
{
private function __construct(
private readonly Element $element
) {
}
public static function create(Element $element): self
{
return new self($element);
}
/**
* Initialize from DER data.
*
* @param string $data DER encoded data
*/
public static function fromDER(string $data): self
{
return Element::fromDER($data)->asUnspecified();
}
/**
* Initialize from `ElementBase` interface.
*/
public static function fromElementBase(ElementBase $el): self
{
// if element is already wrapped
if ($el instanceof self) {
return $el;
}
return self::create($el->asElement());
}
/**
* Get the wrapped element as a context specific tagged type.
*/
public function asTagged(): TaggedType
{
if (! $this->element instanceof TaggedType) {
throw new UnexpectedValueException('Tagged element expected, got ' . $this->typeDescriptorString());
}
return $this->element;
}
/**
* Get the wrapped element as an application specific type.
*/
public function asApplication(): ApplicationType
{
if (! $this->element instanceof ApplicationType) {
throw new UnexpectedValueException('Application type expected, got ' . $this->typeDescriptorString());
}
return $this->element;
}
/**
* Get the wrapped element as a private tagged type.
*/
public function asPrivate(): PrivateType
{
if (! $this->element instanceof PrivateType) {
throw new UnexpectedValueException('Private type expected, got ' . $this->typeDescriptorString());
}
return $this->element;
}
/**
* Get the wrapped element as a boolean type.
*/
public function asBoolean(): Boolean
{
if (! $this->element instanceof Boolean) {
throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_BOOLEAN));
}
return $this->element;
}
/**
* Get the wrapped element as an integer type.
*/
public function asInteger(): Integer
{
if (! $this->element instanceof Integer) {
throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_INTEGER));
}
return $this->element;
}
/**
* Get the wrapped element as a bit string type.
*/
public function asBitString(): BitString
{
if (! $this->element instanceof BitString) {
throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_BIT_STRING));
}
return $this->element;
}
/**
* Get the wrapped element as an octet string type.
*/
public function asOctetString(): OctetString
{
if (! $this->element instanceof OctetString) {
throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_OCTET_STRING));
}
return $this->element;
}
/**
* Get the wrapped element as a null type.
*/
public function asNull(): NullType
{
if (! $this->element instanceof NullType) {
throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_NULL));
}
return $this->element;
}
/**
* Get the wrapped element as an object identifier type.
*/
public function asObjectIdentifier(): ObjectIdentifier
{
if (! $this->element instanceof ObjectIdentifier) {
throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_OBJECT_IDENTIFIER));
}
return $this->element;
}
/**
* Get the wrapped element as an object descriptor type.
*/
public function asObjectDescriptor(): ObjectDescriptor
{
if (! $this->element instanceof ObjectDescriptor) {
throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_OBJECT_DESCRIPTOR));
}
return $this->element;
}
/**
* Get the wrapped element as a real type.
*/
public function asReal(): Real
{
if (! $this->element instanceof Real) {
throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_REAL));
}
return $this->element;
}
/**
* Get the wrapped element as an enumerated type.
*/
public function asEnumerated(): Enumerated
{
if (! $this->element instanceof Enumerated) {
throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_ENUMERATED));
}
return $this->element;
}
/**
* Get the wrapped element as a UTF8 string type.
*/
public function asUTF8String(): UTF8String
{
if (! $this->element instanceof UTF8String) {
throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_UTF8_STRING));
}
return $this->element;
}
/**
* Get the wrapped element as a relative OID type.
*/
public function asRelativeOID(): RelativeOID
{
if (! $this->element instanceof RelativeOID) {
throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_RELATIVE_OID));
}
return $this->element;
}
/**
* Get the wrapped element as a sequence type.
*/
public function asSequence(): Sequence
{
if (! $this->element instanceof Sequence) {
throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_SEQUENCE));
}
return $this->element;
}
/**
* Get the wrapped element as a set type.
*/
public function asSet(): Set
{
if (! $this->element instanceof Set) {
throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_SET));
}
return $this->element;
}
/**
* Get the wrapped element as a numeric string type.
*/
public function asNumericString(): NumericString
{
if (! $this->element instanceof NumericString) {
throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_NUMERIC_STRING));
}
return $this->element;
}
/**
* Get the wrapped element as a printable string type.
*/
public function asPrintableString(): PrintableString
{
if (! $this->element instanceof PrintableString) {
throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_PRINTABLE_STRING));
}
return $this->element;
}
/**
* Get the wrapped element as a T61 string type.
*/
public function asT61String(): T61String
{
if (! $this->element instanceof T61String) {
throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_T61_STRING));
}
return $this->element;
}
/**
* Get the wrapped element as a videotex string type.
*/
public function asVideotexString(): VideotexString
{
if (! $this->element instanceof VideotexString) {
throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_VIDEOTEX_STRING));
}
return $this->element;
}
/**
* Get the wrapped element as a IA5 string type.
*/
public function asIA5String(): IA5String
{
if (! $this->element instanceof IA5String) {
throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_IA5_STRING));
}
return $this->element;
}
/**
* Get the wrapped element as an UTC time type.
*/
public function asUTCTime(): UTCTime
{
if (! $this->element instanceof UTCTime) {
throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_UTC_TIME));
}
return $this->element;
}
/**
* Get the wrapped element as a generalized time type.
*/
public function asGeneralizedTime(): GeneralizedTime
{
if (! $this->element instanceof GeneralizedTime) {
throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_GENERALIZED_TIME));
}
return $this->element;
}
/**
* Get the wrapped element as a graphic string type.
*/
public function asGraphicString(): GraphicString
{
if (! $this->element instanceof GraphicString) {
throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_GRAPHIC_STRING));
}
return $this->element;
}
/**
* Get the wrapped element as a visible string type.
*/
public function asVisibleString(): VisibleString
{
if (! $this->element instanceof VisibleString) {
throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_VISIBLE_STRING));
}
return $this->element;
}
/**
* Get the wrapped element as a general string type.
*/
public function asGeneralString(): GeneralString
{
if (! $this->element instanceof GeneralString) {
throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_GENERAL_STRING));
}
return $this->element;
}
/**
* Get the wrapped element as a universal string type.
*/
public function asUniversalString(): UniversalString
{
if (! $this->element instanceof UniversalString) {
throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_UNIVERSAL_STRING));
}
return $this->element;
}
/**
* Get the wrapped element as a character string type.
*/
public function asCharacterString(): CharacterString
{
if (! $this->element instanceof CharacterString) {
throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_CHARACTER_STRING));
}
return $this->element;
}
/**
* Get the wrapped element as a BMP string type.
*/
public function asBMPString(): BMPString
{
if (! $this->element instanceof BMPString) {
throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_BMP_STRING));
}
return $this->element;
}
/**
* Get the wrapped element as a constructed string type.
*/
public function asConstructedString(): ConstructedString
{
if (! $this->element instanceof ConstructedString) {
throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_CONSTRUCTED_STRING));
}
return $this->element;
}
/**
* Get the wrapped element as any string type.
*/
public function asString(): StringType
{
if (! $this->element instanceof StringType) {
throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_STRING));
}
return $this->element;
}
/**
* Get the wrapped element as any time type.
*/
public function asTime(): TimeType
{
if (! $this->element instanceof TimeType) {
throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_TIME));
}
return $this->element;
}
public function asElement(): Element
{
return $this->element;
}
public function asUnspecified(): self
{
return $this;
}
public function toDER(): string
{
return $this->element->toDER();
}
public function typeClass(): int
{
return $this->element->typeClass();
}
public function tag(): int
{
return $this->element->tag();
}
public function isConstructed(): bool
{
return $this->element->isConstructed();
}
public function isType(int $tag): bool
{
return $this->element->isType($tag);
}
public function isTagged(): bool
{
return $this->element->isTagged();
}
/**
* {@inheritdoc}
*
* Consider using any of the `as*` accessor methods instead.
*/
public function expectType(int $tag): ElementBase
{
return $this->element->expectType($tag);
}
/**
* {@inheritdoc}
*
* Consider using `asTagged()` method instead and chaining
* with `TaggedType::asExplicit()` or `TaggedType::asImplicit()`.
*/
public function expectTagged(?int $tag = null): TaggedType
{
return $this->element->expectTagged($tag);
}
/**
* Generate message for exceptions thrown by `as*` methods.
*
* @param int $tag Type tag of the expected element
*/
private function generateExceptionMessage(int $tag): string
{
return sprintf('%s expected, got %s.', Element::tagToName($tag), $this->typeDescriptorString());
}
/**
* Get textual description of the wrapped element for debugging purposes.
*/
private function typeDescriptorString(): string
{
$type_cls = $this->element->typeClass();
$tag = $this->element->tag();
$str = $this->element->isConstructed() ? 'constructed ' : 'primitive ';
if ($type_cls === Identifier::CLASS_UNIVERSAL) {
$str .= Element::tagToName($tag);
} else {
$str .= Identifier::classToName($type_cls) . " TAG {$tag}";
}
return $str;
}
}

View File

@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Util;
use Brick\Math\BigInteger;
use InvalidArgumentException;
use function mb_strlen;
use Stringable;
use Throwable;
/**
* Class to wrap an integer of arbirtary length.
*/
final class BigInt implements Stringable
{
/**
* Number as a BigInteger object.
*/
private readonly BigInteger $value;
/**
* Number as a base 10 integer string.
*
* @internal Lazily initialized
*/
private ?string $number = null;
/**
* Number as an integer type.
*
* @internal Lazily initialized
*/
private ?int $_intNum = null;
private function __construct(BigInteger|int|string $num)
{
// convert to BigInteger object
if (! $num instanceof BigInteger) {
try {
$num = BigInteger::of($num);
} catch (Throwable) {
throw new InvalidArgumentException('Unable to convert to integer.');
}
}
$this->value = $num;
}
public function __toString(): string
{
return $this->base10();
}
public static function create(BigInteger|int|string $num): self
{
return new self($num);
}
/**
* Initialize from an arbitrary length of octets as an unsigned integer.
*/
public static function fromUnsignedOctets(string $octets): self
{
if (mb_strlen($octets, '8bit') === 0) {
throw new InvalidArgumentException('Empty octets.');
}
return self::create(BigInteger::fromBytes($octets, false));
}
/**
* Initialize from an arbitrary length of octets as an signed integer having two's complement encoding.
*/
public static function fromSignedOctets(string $octets): self
{
if (mb_strlen($octets, '8bit') === 0) {
throw new InvalidArgumentException('Empty octets.');
}
return self::create(BigInteger::fromBytes($octets));
}
/**
* Get the number as a base 10 integer string.
*/
public function base10(): string
{
if ($this->number === null) {
$this->number = $this->value->toBase(10);
}
return $this->number;
}
/**
* Get the number as an integer.
*/
public function toInt(): int
{
if (! isset($this->_intNum)) {
$this->_intNum = $this->value->toInt();
}
return $this->_intNum;
}
public function getValue(): BigInteger
{
return $this->value;
}
/**
* Get the number as an unsigned integer encoded in binary.
*/
public function unsignedOctets(): string
{
return $this->value->toBytes(false);
}
/**
* Get the number as a signed integer encoded in two's complement binary.
*/
public function signedOctets(): string
{
return $this->value->toBytes();
}
}

View File

@ -0,0 +1,147 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Util;
use function assert;
use Brick\Math\BigInteger;
use function count;
use function is_array;
use function ord;
use OutOfBoundsException;
use RuntimeException;
use SpomkyLabs\Pki\ASN1\Type\Primitive\BitString;
/**
* Class to handle a bit string as a field of flags.
*/
final class Flags
{
/**
* Flag octets.
*/
private string $_flags;
/**
* @param BigInteger|int|string $flags Flags
* @param int $_width The number of flags. If width is larger than
* number of bits in $flags, zeroes are prepended
* to flag field.
*/
private function __construct(
BigInteger|int|string $flags,
private readonly int $_width
) {
if ($_width === 0) {
$this->_flags = '';
return;
}
// calculate number of unused bits in last octet
$last_octet_bits = $_width % 8;
$unused_bits = $last_octet_bits !== 0 ? 8 - $last_octet_bits : 0;
// mask bits outside bitfield width
$num = BigInteger::of($flags);
$mask = BigInteger::of(1)->shiftedLeft($_width)->minus(1);
$num = $num->and($mask);
// shift towards MSB if needed
$data = $num->shiftedLeft($unused_bits)
->toBytes(false);
$octets = unpack('C*', $data);
assert(is_array($octets), new RuntimeException('unpack() failed'));
$bits = count($octets) * 8;
// pad with zeroes
while ($bits < $_width) {
array_unshift($octets, 0);
$bits += 8;
}
$this->_flags = pack('C*', ...$octets);
}
public static function create(BigInteger|int|string $flags, int $_width): self
{
return new self($flags, $_width);
}
/**
* Initialize from `BitString`.
*/
public static function fromBitString(BitString $bs, int $width): self
{
$num_bits = $bs->numBits();
$data = $bs->string();
$num = $data === '' ? BigInteger::of(0) : BigInteger::fromBytes($bs->string(), false);
$num = $num->shiftedRight($bs->unusedBits());
if ($num_bits < $width) {
$num = $num->shiftedLeft($width - $num_bits);
}
return self::create($num, $width);
}
/**
* Check whether a bit at given index is set.
*
* Index 0 is the leftmost bit.
*/
public function test(int $idx): bool
{
if ($idx >= $this->_width) {
throw new OutOfBoundsException('Index is out of bounds.');
}
// octet index
$oi = (int) floor($idx / 8);
$byte = $this->_flags[$oi];
// bit index
$bi = $idx % 8;
// index 0 is the most significant bit in byte
$mask = 0x01 << (7 - $bi);
return (ord($byte) & $mask) > 0;
}
/**
* Get flags as an octet string.
*
* Zeroes are appended to the last octet if width is not divisible by 8.
*/
public function string(): string
{
return $this->_flags;
}
/**
* Get flags as a base 10 integer.
*
* @return string Integer as a string
*/
public function number(): string
{
$num = BigInteger::fromBytes($this->_flags, false);
$last_octet_bits = $this->_width % 8;
$unused_bits = $last_octet_bits !== 0 ? 8 - $last_octet_bits : 0;
$num = $num->shiftedRight($unused_bits);
return $num->toBase(10);
}
/**
* Get flags as an integer.
*/
public function intNumber(): int
{
$num = BigInt::create($this->number());
return $num->toInt();
}
/**
* Get flags as a `BitString` object.
*
* Unused bits are set accordingly. Trailing zeroes are not stripped.
*/
public function bitString(): BitString
{
$last_octet_bits = $this->_width % 8;
$unused_bits = $last_octet_bits !== 0 ? 8 - $last_octet_bits : 0;
return BitString::create($this->_flags, $unused_bits);
}
}

View File

@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoBridge;
use function defined;
use RuntimeException;
use SpomkyLabs\Pki\CryptoBridge\Crypto\OpenSSLCrypto;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Cipher\CipherAlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Feature\SignatureAlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\Asymmetric\PrivateKeyInfo;
use SpomkyLabs\Pki\CryptoTypes\Asymmetric\PublicKeyInfo;
use SpomkyLabs\Pki\CryptoTypes\Signature\Signature;
/**
* Base class for crypto engine implementations.
*/
abstract class Crypto
{
/**
* Sign data with given algorithm using given private key.
*
* @param string $data Data to sign
* @param PrivateKeyInfo $privkey_info Private key
* @param SignatureAlgorithmIdentifier $algo Signature algorithm
*/
abstract public function sign(
string $data,
PrivateKeyInfo $privkey_info,
SignatureAlgorithmIdentifier $algo
): Signature;
/**
* Verify signature with given algorithm using given public key.
*
* @param string $data Data to verify
* @param Signature $signature Signature
* @param PublicKeyInfo $pubkey_info Public key
* @param SignatureAlgorithmIdentifier $algo Signature algorithm
*
* @return bool True if signature matches
*/
abstract public function verify(
string $data,
Signature $signature,
PublicKeyInfo $pubkey_info,
SignatureAlgorithmIdentifier $algo
): bool;
/**
* Encrypt data with given algorithm using given key.
*
* Padding must be added by the caller. Initialization vector is taken from the algorithm identifier if available.
*
* @param string $data Plaintext
* @param string $key Encryption key
* @param CipherAlgorithmIdentifier $algo Encryption algorithm
*
* @return string Ciphertext
*/
abstract public function encrypt(string $data, string $key, CipherAlgorithmIdentifier $algo): string;
/**
* Decrypt data with given algorithm using given key.
*
* Possible padding is not removed and must be handled by the caller. Initialization vector is taken from the
* algorithm identifier if available.
*
* @param string $data Ciphertext
* @param string $key Encryption key
* @param CipherAlgorithmIdentifier $algo Encryption algorithm
*
* @return string Plaintext
*/
abstract public function decrypt(string $data, string $key, CipherAlgorithmIdentifier $algo): string;
/**
* Get default supported crypto implementation.
*/
public static function getDefault(): self
{
if (defined('OPENSSL_VERSION_NUMBER')) {
return new OpenSSLCrypto();
}
// @codeCoverageIgnoreStart
throw new RuntimeException('No crypto implementation available.');
// @codeCoverageIgnoreEnd
}
}

View File

@ -0,0 +1,234 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoBridge\Crypto;
use function array_key_exists;
use function mb_strlen;
use const OPENSSL_ALGO_MD4;
use const OPENSSL_ALGO_MD5;
use const OPENSSL_ALGO_SHA1;
use const OPENSSL_ALGO_SHA224;
use const OPENSSL_ALGO_SHA256;
use const OPENSSL_ALGO_SHA384;
use const OPENSSL_ALGO_SHA512;
use const OPENSSL_RAW_DATA;
use const OPENSSL_ZERO_PADDING;
use RuntimeException;
use SpomkyLabs\Pki\CryptoBridge\Crypto;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\AlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Cipher\BlockCipherAlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Cipher\CipherAlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Cipher\RC2CBCAlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Feature\SignatureAlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\Asymmetric\PrivateKeyInfo;
use SpomkyLabs\Pki\CryptoTypes\Asymmetric\PublicKeyInfo;
use SpomkyLabs\Pki\CryptoTypes\Signature\Signature;
use UnexpectedValueException;
/**
* Crypto engine using OpenSSL extension.
*/
final class OpenSSLCrypto extends Crypto
{
/**
* Mapping from algorithm OID to OpenSSL signature method identifier.
*
* @internal
*
* @var array<string, int>
*/
private const MAP_DIGEST_OID = [
AlgorithmIdentifier::OID_MD4_WITH_RSA_ENCRYPTION => OPENSSL_ALGO_MD4,
AlgorithmIdentifier::OID_MD5_WITH_RSA_ENCRYPTION => OPENSSL_ALGO_MD5,
AlgorithmIdentifier::OID_SHA1_WITH_RSA_ENCRYPTION => OPENSSL_ALGO_SHA1,
AlgorithmIdentifier::OID_SHA224_WITH_RSA_ENCRYPTION => OPENSSL_ALGO_SHA224,
AlgorithmIdentifier::OID_SHA256_WITH_RSA_ENCRYPTION => OPENSSL_ALGO_SHA256,
AlgorithmIdentifier::OID_SHA384_WITH_RSA_ENCRYPTION => OPENSSL_ALGO_SHA384,
AlgorithmIdentifier::OID_SHA512_WITH_RSA_ENCRYPTION => OPENSSL_ALGO_SHA512,
AlgorithmIdentifier::OID_ECDSA_WITH_SHA1 => OPENSSL_ALGO_SHA1,
AlgorithmIdentifier::OID_ECDSA_WITH_SHA224 => OPENSSL_ALGO_SHA224,
AlgorithmIdentifier::OID_ECDSA_WITH_SHA256 => OPENSSL_ALGO_SHA256,
AlgorithmIdentifier::OID_ECDSA_WITH_SHA384 => OPENSSL_ALGO_SHA384,
AlgorithmIdentifier::OID_ECDSA_WITH_SHA512 => OPENSSL_ALGO_SHA512,
];
/**
* Mapping from algorithm OID to OpenSSL cipher method name.
*
* @internal
*
* @var array<string, string>
*/
private const MAP_CIPHER_OID = [
AlgorithmIdentifier::OID_DES_CBC => 'des-cbc',
AlgorithmIdentifier::OID_DES_EDE3_CBC => 'des-ede3-cbc',
AlgorithmIdentifier::OID_AES_128_CBC => 'aes-128-cbc',
AlgorithmIdentifier::OID_AES_192_CBC => 'aes-192-cbc',
AlgorithmIdentifier::OID_AES_256_CBC => 'aes-256-cbc',
];
public function sign(
string $data,
PrivateKeyInfo $privkey_info,
SignatureAlgorithmIdentifier $algo
): Signature {
$this->_checkSignatureAlgoAndKey($algo, $privkey_info->algorithmIdentifier());
$result = openssl_sign($data, $signature, (string) $privkey_info->toPEM(), $this->_algoToDigest($algo));
if ($result === false) {
throw new RuntimeException('openssl_sign() failed: ' . $this->_getLastError());
}
return Signature::fromSignatureData($signature, $algo);
}
public function verify(
string $data,
Signature $signature,
PublicKeyInfo $pubkey_info,
SignatureAlgorithmIdentifier $algo
): bool {
$this->_checkSignatureAlgoAndKey($algo, $pubkey_info->algorithmIdentifier());
$result = openssl_verify(
$data,
$signature->bitString()
->string(),
(string) $pubkey_info->toPEM(),
$this->_algoToDigest($algo)
);
if ($result === -1) {
throw new RuntimeException('openssl_verify() failed: ' . $this->_getLastError());
}
return $result === 1;
}
public function encrypt(string $data, string $key, CipherAlgorithmIdentifier $algo): string
{
$this->_checkCipherKeySize($algo, $key);
$iv = $algo->initializationVector();
$result = openssl_encrypt(
$data,
$this->_algoToCipher($algo),
$key,
OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING,
$iv
);
if ($result === false) {
throw new RuntimeException('openssl_encrypt() failed: ' . $this->_getLastError());
}
return $result;
}
public function decrypt(string $data, string $key, CipherAlgorithmIdentifier $algo): string
{
$this->_checkCipherKeySize($algo, $key);
$iv = $algo->initializationVector();
$result = openssl_decrypt(
$data,
$this->_algoToCipher($algo),
$key,
OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING,
$iv
);
if ($result === false) {
throw new RuntimeException('openssl_decrypt() failed: ' . $this->_getLastError());
}
return $result;
}
/**
* Validate cipher algorithm key size.
*/
protected function _checkCipherKeySize(CipherAlgorithmIdentifier $algo, string $key): void
{
if ($algo instanceof BlockCipherAlgorithmIdentifier) {
if (mb_strlen($key, '8bit') !== $algo->keySize()) {
throw new UnexpectedValueException(
sprintf(
'Key length for %s must be %d, %d given.',
$algo->name(),
$algo->keySize(),
mb_strlen($key, '8bit')
)
);
}
}
}
/**
* Get last OpenSSL error message.
*/
protected function _getLastError(): ?string
{
// pump error message queue
$msg = null;
while (false !== ($err = openssl_error_string())) {
$msg = $err;
}
return $msg;
}
/**
* Check that given signature algorithm supports key of given type.
*
* @param SignatureAlgorithmIdentifier $sig_algo Signature algorithm
* @param AlgorithmIdentifier $key_algo Key algorithm
*/
protected function _checkSignatureAlgoAndKey(
SignatureAlgorithmIdentifier $sig_algo,
AlgorithmIdentifier $key_algo
): void {
if (! $sig_algo->supportsKeyAlgorithm($key_algo)) {
throw new UnexpectedValueException(
sprintf(
'Signature algorithm %s does not support key algorithm %s.',
$sig_algo->name(),
$key_algo->name()
)
);
}
}
/**
* Get OpenSSL digest method for given signature algorithm identifier.
*/
protected function _algoToDigest(SignatureAlgorithmIdentifier $algo): int
{
$oid = $algo->oid();
if (! array_key_exists($oid, self::MAP_DIGEST_OID)) {
throw new UnexpectedValueException(sprintf('Digest method %s not supported.', $algo->name()));
}
return self::MAP_DIGEST_OID[$oid];
}
/**
* Get OpenSSL cipher method for given cipher algorithm identifier.
*/
protected function _algoToCipher(CipherAlgorithmIdentifier $algo): string
{
$oid = $algo->oid();
if (array_key_exists($oid, self::MAP_CIPHER_OID)) {
return self::MAP_CIPHER_OID[$oid];
}
if ($oid === AlgorithmIdentifier::OID_RC2_CBC) {
if (! $algo instanceof RC2CBCAlgorithmIdentifier) {
throw new UnexpectedValueException('Not an RC2-CBC algorithm.');
}
return $this->_rc2AlgoToCipher($algo);
}
throw new UnexpectedValueException(sprintf('Cipher method %s not supported.', $algo->name()));
}
/**
* Get OpenSSL cipher method for given RC2 algorithm identifier.
*/
protected function _rc2AlgoToCipher(RC2CBCAlgorithmIdentifier $algo): string
{
return match ($algo->effectiveKeyBits()) {
128 => 'rc2-cbc',
64 => 'rc2-64-cbc',
40 => 'rc2-40-cbc',
default => throw new UnexpectedValueException($algo->effectiveKeyBits() . ' bit RC2 not supported.'),
};
}
}

View File

@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoEncoding;
use function is_string;
use RuntimeException;
use Stringable;
use UnexpectedValueException;
/**
* Implements PEM file encoding and decoding.
*
* @see https://tools.ietf.org/html/rfc7468
*/
final class PEM implements Stringable
{
// well-known PEM types
final public const TYPE_CERTIFICATE = 'CERTIFICATE';
final public const TYPE_CRL = 'X509 CRL';
final public const TYPE_CERTIFICATE_REQUEST = 'CERTIFICATE REQUEST';
final public const TYPE_ATTRIBUTE_CERTIFICATE = 'ATTRIBUTE CERTIFICATE';
final public const TYPE_PRIVATE_KEY = 'PRIVATE KEY';
final public const TYPE_PUBLIC_KEY = 'PUBLIC KEY';
final public const TYPE_ENCRYPTED_PRIVATE_KEY = 'ENCRYPTED PRIVATE KEY';
final public const TYPE_RSA_PRIVATE_KEY = 'RSA PRIVATE KEY';
final public const TYPE_RSA_PUBLIC_KEY = 'RSA PUBLIC KEY';
final public const TYPE_EC_PRIVATE_KEY = 'EC PRIVATE KEY';
final public const TYPE_PKCS7 = 'PKCS7';
final public const TYPE_CMS = 'CMS';
/**
* Regular expression to match PEM block.
*
* @var string
*/
final public const PEM_REGEX = '/' .
/* line start */
'(?:^|[\r\n])' .
/* header */
'-----BEGIN (.+?)-----[\r\n]+' .
/* payload */
'(.+?)' .
/* trailer */
'[\r\n]+-----END \\1-----' .
'/ms';
/**
* @param string $type Content type
* @param string $data Payload
*/
private function __construct(
private readonly string $type,
private readonly string $data
) {
}
public function __toString(): string
{
return $this->string();
}
public static function create(string $_type, string $_data): self
{
return new self($_type, $_data);
}
/**
* Initialize from a PEM-formatted string.
*/
public static function fromString(string $str): self
{
if (preg_match(self::PEM_REGEX, $str, $match) !== 1) {
throw new UnexpectedValueException('Not a PEM formatted string.');
}
$payload = preg_replace('/\s+/', '', $match[2]);
if (! is_string($payload)) {
throw new UnexpectedValueException('Failed to decode PEM data.');
}
$data = base64_decode($payload, true);
if ($data === false) {
throw new UnexpectedValueException('Failed to decode PEM data.');
}
return self::create($match[1], $data);
}
/**
* Initialize from a file.
*
* @param string $filename Path to file
*/
public static function fromFile(string $filename): self
{
if (! is_readable($filename)) {
throw new RuntimeException("Failed to read {$filename}.");
}
$str = file_get_contents($filename);
if ($str === false) {
throw new RuntimeException("Failed to read {$filename}.");
}
return self::fromString($str);
}
/**
* Get content type.
*/
public function type(): string
{
return $this->type;
}
public function data(): string
{
return $this->data;
}
/**
* Encode to PEM string.
*/
public function string(): string
{
return "-----BEGIN {$this->type}-----\n" .
trim(chunk_split(base64_encode($this->data), 64, "\n")) . "\n" .
"-----END {$this->type}-----";
}
}

View File

@ -0,0 +1,155 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoEncoding;
use ArrayIterator;
use function count;
use Countable;
use function is_string;
use IteratorAggregate;
use LogicException;
use const PREG_SET_ORDER;
use RuntimeException;
use Stringable;
use UnexpectedValueException;
/**
* Container for multiple PEM objects.
*
* The order of PEMs shall be retained, eg. when read from a file.
*/
final class PEMBundle implements Countable, IteratorAggregate, Stringable
{
/**
* Array of PEM objects.
*
* @var PEM[]
*/
private array $pems;
private function __construct(PEM ...$pems)
{
$this->pems = $pems;
}
public function __toString(): string
{
return $this->string();
}
public static function create(PEM ...$pems): self
{
return new self(...$pems);
}
/**
* Initialize from a string.
*/
public static function fromString(string $str): self
{
$hasMatches = preg_match_all(PEM::PEM_REGEX, $str, $matches, PREG_SET_ORDER);
if ($hasMatches === false || $hasMatches === 0) {
throw new UnexpectedValueException('No PEM blocks.');
}
$pems = array_map(
static function ($match) {
$payload = preg_replace('/\s+/', '', $match[2]);
if (! is_string($payload)) {
throw new UnexpectedValueException('Failed to decode PEM data.');
}
$data = base64_decode($payload, true);
if ($data === false) {
throw new UnexpectedValueException('Failed to decode PEM data.');
}
return PEM::create($match[1], $data);
},
$matches
);
return self::create(...$pems);
}
/**
* Initialize from a file.
*/
public static function fromFile(string $filename): self
{
if (! is_readable($filename)) {
throw new RuntimeException("Failed to read {$filename}.");
}
$str = file_get_contents($filename);
if ($str === false) {
throw new RuntimeException("Failed to read {$filename}.");
}
return self::fromString($str);
}
/**
* Get self with PEM objects appended.
*/
public function withPEMs(PEM ...$pems): self
{
$obj = clone $this;
$obj->pems = array_merge($obj->pems, $pems);
return $obj;
}
/**
* Get all PEMs in a bundle.
*
* @return PEM[]
*/
public function all(): array
{
return $this->pems;
}
/**
* Get the first PEM in a bundle.
*/
public function first(): PEM
{
if (count($this->pems) === 0) {
throw new LogicException('No PEMs.');
}
return $this->pems[0];
}
/**
* Get the last PEM in a bundle.
*/
public function last(): PEM
{
if (count($this->pems) === 0) {
throw new LogicException('No PEMs.');
}
return $this->pems[count($this->pems) - 1];
}
/**
* @see \Countable::count()
*/
public function count(): int
{
return count($this->pems);
}
/**
* Get iterator for PEMs.
*
* @see \IteratorAggregate::getIterator()
*/
public function getIterator(): ArrayIterator
{
return new ArrayIterator($this->pems);
}
/**
* Encode bundle to a string of contiguous PEM blocks.
*/
public function string(): string
{
return implode("\n", array_map(static fn (PEM $pem) => $pem->string(), $this->pems));
}
}

View File

@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Type\Constructed\Sequence;
use SpomkyLabs\Pki\ASN1\Type\Primitive\ObjectIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Feature\AlgorithmIdentifierType;
/**
* Implements AlgorithmIdentifier ASN.1 type.
*
* @see https://tools.ietf.org/html/rfc2898#appendix-C
* @see https://tools.ietf.org/html/rfc3447#appendix-C
*/
abstract class AlgorithmIdentifier implements AlgorithmIdentifierType
{
// RSA encryption
final public const OID_RSA_ENCRYPTION = '1.2.840.113549.1.1.1';
// RSA signature algorithms
final public const OID_MD2_WITH_RSA_ENCRYPTION = '1.2.840.113549.1.1.2';
final public const OID_MD4_WITH_RSA_ENCRYPTION = '1.2.840.113549.1.1.3';
final public const OID_MD5_WITH_RSA_ENCRYPTION = '1.2.840.113549.1.1.4';
final public const OID_SHA1_WITH_RSA_ENCRYPTION = '1.2.840.113549.1.1.5';
final public const OID_SHA256_WITH_RSA_ENCRYPTION = '1.2.840.113549.1.1.11';
final public const OID_SHA384_WITH_RSA_ENCRYPTION = '1.2.840.113549.1.1.12';
final public const OID_SHA512_WITH_RSA_ENCRYPTION = '1.2.840.113549.1.1.13';
final public const OID_SHA224_WITH_RSA_ENCRYPTION = '1.2.840.113549.1.1.14';
// Elliptic Curve signature algorithms
final public const OID_ECDSA_WITH_SHA1 = '1.2.840.10045.4.1';
final public const OID_ECDSA_WITH_SHA224 = '1.2.840.10045.4.3.1';
final public const OID_ECDSA_WITH_SHA256 = '1.2.840.10045.4.3.2';
final public const OID_ECDSA_WITH_SHA384 = '1.2.840.10045.4.3.3';
final public const OID_ECDSA_WITH_SHA512 = '1.2.840.10045.4.3.4';
// Elliptic Curve public key
final public const OID_EC_PUBLIC_KEY = '1.2.840.10045.2.1';
// Elliptic curve / algorithm pairs from RFC 8410
final public const OID_X25519 = '1.3.101.110';
final public const OID_X448 = '1.3.101.111';
final public const OID_ED25519 = '1.3.101.112';
final public const OID_ED448 = '1.3.101.113';
// Cipher algorithms
final public const OID_DES_CBC = '1.3.14.3.2.7';
final public const OID_RC2_CBC = '1.2.840.113549.3.2';
final public const OID_DES_EDE3_CBC = '1.2.840.113549.3.7';
final public const OID_AES_128_CBC = '2.16.840.1.101.3.4.1.2';
final public const OID_AES_192_CBC = '2.16.840.1.101.3.4.1.22';
final public const OID_AES_256_CBC = '2.16.840.1.101.3.4.1.42';
// HMAC-SHA-1 from RFC 8018
final public const OID_HMAC_WITH_SHA1 = '1.2.840.113549.2.7';
// HMAC algorithms from RFC 4231
final public const OID_HMAC_WITH_SHA224 = '1.2.840.113549.2.8';
final public const OID_HMAC_WITH_SHA256 = '1.2.840.113549.2.9';
final public const OID_HMAC_WITH_SHA384 = '1.2.840.113549.2.10';
final public const OID_HMAC_WITH_SHA512 = '1.2.840.113549.2.11';
// Message digest algorithms
final public const OID_MD5 = '1.2.840.113549.2.5';
final public const OID_SHA1 = '1.3.14.3.2.26';
final public const OID_SHA224 = '2.16.840.1.101.3.4.2.4';
final public const OID_SHA256 = '2.16.840.1.101.3.4.2.1';
final public const OID_SHA384 = '2.16.840.1.101.3.4.2.2';
final public const OID_SHA512 = '2.16.840.1.101.3.4.2.3';
protected function __construct(
protected readonly string $oid
) {
}
/**
* Initialize from ASN.1.
*/
public static function fromASN1(Sequence $seq): self
{
return AlgorithmIdentifierFactory::create()->parse($seq);
}
public function oid(): string
{
return $this->oid;
}
public function toASN1(): Sequence
{
$elements = [ObjectIdentifier::create($this->oid)];
$params = $this->paramsASN1();
if (isset($params)) {
$elements[] = $params;
}
return Sequence::create(...$elements);
}
/**
* Get algorithm identifier parameters as ASN.1.
*
* If type allows parameters to be omitted, return null.
*/
abstract protected function paramsASN1(): ?Element;
}

View File

@ -0,0 +1,158 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier;
use function array_key_exists;
use SpomkyLabs\Pki\ASN1\Type\Constructed\Sequence;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Asymmetric\ECPublicKeyAlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Asymmetric\Ed25519AlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Asymmetric\Ed448AlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Asymmetric\RSAEncryptionAlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Asymmetric\X25519AlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Asymmetric\X448AlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Cipher\AES128CBCAlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Cipher\AES192CBCAlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Cipher\AES256CBCAlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Cipher\DESCBCAlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Cipher\DESEDE3CBCAlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Cipher\RC2CBCAlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Hash\HMACWithSHA1AlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Hash\HMACWithSHA224AlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Hash\HMACWithSHA256AlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Hash\HMACWithSHA384AlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Hash\HMACWithSHA512AlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Hash\MD5AlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Hash\SHA1AlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Hash\SHA224AlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Hash\SHA256AlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Hash\SHA384AlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Hash\SHA512AlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Signature\ECDSAWithSHA1AlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Signature\ECDSAWithSHA224AlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Signature\ECDSAWithSHA256AlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Signature\ECDSAWithSHA384AlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Signature\ECDSAWithSHA512AlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Signature\MD2WithRSAEncryptionAlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Signature\MD4WithRSAEncryptionAlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Signature\MD5WithRSAEncryptionAlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Signature\SHA1WithRSAEncryptionAlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Signature\SHA224WithRSAEncryptionAlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Signature\SHA256WithRSAEncryptionAlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Signature\SHA384WithRSAEncryptionAlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Signature\SHA512WithRSAEncryptionAlgorithmIdentifier;
/**
* Factory class to parse AlgorithmIdentifier ASN.1 types to specific algorithm identifier objects.
*
* Additional providers may be added to the process to support algorithm identifiers that are implemented in external
* libraries.
*/
final class AlgorithmIdentifierFactory
{
/**
* Mapping for algorithm identifiers provided by this library.
*
* @internal
*
* @var array<string, string>
*/
private const MAP_OID_TO_CLASS = [
AlgorithmIdentifier::OID_RSA_ENCRYPTION => RSAEncryptionAlgorithmIdentifier::class,
AlgorithmIdentifier::OID_EC_PUBLIC_KEY => ECPublicKeyAlgorithmIdentifier::class,
AlgorithmIdentifier::OID_X25519 => X25519AlgorithmIdentifier::class,
AlgorithmIdentifier::OID_X448 => X448AlgorithmIdentifier::class,
AlgorithmIdentifier::OID_ED25519 => Ed25519AlgorithmIdentifier::class,
AlgorithmIdentifier::OID_ED448 => Ed448AlgorithmIdentifier::class,
AlgorithmIdentifier::OID_DES_CBC => DESCBCAlgorithmIdentifier::class,
AlgorithmIdentifier::OID_DES_EDE3_CBC => DESEDE3CBCAlgorithmIdentifier::class,
AlgorithmIdentifier::OID_RC2_CBC => RC2CBCAlgorithmIdentifier::class,
AlgorithmIdentifier::OID_AES_128_CBC => AES128CBCAlgorithmIdentifier::class,
AlgorithmIdentifier::OID_AES_192_CBC => AES192CBCAlgorithmIdentifier::class,
AlgorithmIdentifier::OID_AES_256_CBC => AES256CBCAlgorithmIdentifier::class,
AlgorithmIdentifier::OID_HMAC_WITH_SHA1 => HMACWithSHA1AlgorithmIdentifier::class,
AlgorithmIdentifier::OID_HMAC_WITH_SHA224 => HMACWithSHA224AlgorithmIdentifier::class,
AlgorithmIdentifier::OID_HMAC_WITH_SHA256 => HMACWithSHA256AlgorithmIdentifier::class,
AlgorithmIdentifier::OID_HMAC_WITH_SHA384 => HMACWithSHA384AlgorithmIdentifier::class,
AlgorithmIdentifier::OID_HMAC_WITH_SHA512 => HMACWithSHA512AlgorithmIdentifier::class,
AlgorithmIdentifier::OID_MD5 => MD5AlgorithmIdentifier::class,
AlgorithmIdentifier::OID_SHA1 => SHA1AlgorithmIdentifier::class,
AlgorithmIdentifier::OID_SHA224 => SHA224AlgorithmIdentifier::class,
AlgorithmIdentifier::OID_SHA256 => SHA256AlgorithmIdentifier::class,
AlgorithmIdentifier::OID_SHA384 => SHA384AlgorithmIdentifier::class,
AlgorithmIdentifier::OID_SHA512 => SHA512AlgorithmIdentifier::class,
AlgorithmIdentifier::OID_MD2_WITH_RSA_ENCRYPTION => MD2WithRSAEncryptionAlgorithmIdentifier::class,
AlgorithmIdentifier::OID_MD4_WITH_RSA_ENCRYPTION => MD4WithRSAEncryptionAlgorithmIdentifier::class,
AlgorithmIdentifier::OID_MD5_WITH_RSA_ENCRYPTION => MD5WithRSAEncryptionAlgorithmIdentifier::class,
AlgorithmIdentifier::OID_SHA1_WITH_RSA_ENCRYPTION => SHA1WithRSAEncryptionAlgorithmIdentifier::class,
AlgorithmIdentifier::OID_SHA224_WITH_RSA_ENCRYPTION => SHA224WithRSAEncryptionAlgorithmIdentifier::class,
AlgorithmIdentifier::OID_SHA256_WITH_RSA_ENCRYPTION => SHA256WithRSAEncryptionAlgorithmIdentifier::class,
AlgorithmIdentifier::OID_SHA384_WITH_RSA_ENCRYPTION => SHA384WithRSAEncryptionAlgorithmIdentifier::class,
AlgorithmIdentifier::OID_SHA512_WITH_RSA_ENCRYPTION => SHA512WithRSAEncryptionAlgorithmIdentifier::class,
AlgorithmIdentifier::OID_ECDSA_WITH_SHA1 => ECDSAWithSHA1AlgorithmIdentifier::class,
AlgorithmIdentifier::OID_ECDSA_WITH_SHA224 => ECDSAWithSHA224AlgorithmIdentifier::class,
AlgorithmIdentifier::OID_ECDSA_WITH_SHA256 => ECDSAWithSHA256AlgorithmIdentifier::class,
AlgorithmIdentifier::OID_ECDSA_WITH_SHA384 => ECDSAWithSHA384AlgorithmIdentifier::class,
AlgorithmIdentifier::OID_ECDSA_WITH_SHA512 => ECDSAWithSHA512AlgorithmIdentifier::class,
];
/**
* Additional algorithm identifier providers.
*
* @var AlgorithmIdentifierProvider[]
*/
private readonly array $_additionalProviders;
/**
* @param AlgorithmIdentifierProvider ...$providers Additional providers
*/
private function __construct(AlgorithmIdentifierProvider ...$providers)
{
$this->_additionalProviders = $providers;
}
public static function create(AlgorithmIdentifierProvider ...$providers): self
{
return new self(...$providers);
}
/**
* Get the name of a class that implements algorithm identifier for given OID.
*
* @param string $oid Object identifier in dotted format
*
* @return null|string Fully qualified class name or null if not supported
*/
public function getClass(string $oid): ?string
{
// if OID is provided by this factory
if (array_key_exists($oid, self::MAP_OID_TO_CLASS)) {
return self::MAP_OID_TO_CLASS[$oid];
}
// try additional providers
foreach ($this->_additionalProviders as $provider) {
if ($provider->supportsOID($oid)) {
return $provider->getClassByOID($oid);
}
}
return null;
}
/**
* Parse AlgorithmIdentifier from an ASN.1 sequence.
*/
public function parse(Sequence $seq): AlgorithmIdentifier
{
$oid = $seq->at(0)
->asObjectIdentifier()
->oid();
$params = $seq->has(1) ? $seq->at(1) : null;
$cls = $this->getClass($oid);
if ($cls !== null) {
return $cls::fromASN1Params($params);
}
return GenericAlgorithmIdentifier::create($oid, $params);
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier;
/**
* Interface to provide lookup from OID to class name of specific algorithm identifier type implementations.
*
* This allows AlgorithmIdentifier types to be implemented in external libraries and to use AlgorithmIdentifierFactory
* to resolve them.
*/
interface AlgorithmIdentifierProvider
{
/**
* Check whether this provider supports algorithm identifier of given OID.
*
* @param string $oid Object identifier in dotted format
*/
public function supportsOID(string $oid): bool;
/**
* Get the name of a class that implements algorithm identifier for given OID.
*
* @param string $oid Object identifier in dotted format
*
* @return string Fully qualified name of a class that extends
* SpecificAlgorithmIdentifier
*/
public function getClassByOID(string $oid): string;
}

View File

@ -0,0 +1,291 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Asymmetric;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Type\Primitive\ObjectIdentifier;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Feature\AsymmetricCryptoAlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\SpecificAlgorithmIdentifier;
use UnexpectedValueException;
/*
From RFC 5480 - 2.1.1. Unrestricted Algorithm Identifier and Parameters:
The parameter for id-ecPublicKey is as follows and MUST always be
present:
ECParameters ::= CHOICE {
namedCurve OBJECT IDENTIFIER
-- implicitCurve NULL
-- specifiedCurve SpecifiedECDomain
}
*/
/**
* Algorithm identifier for the elliptic curve public key.
*
* @see https://tools.ietf.org/html/rfc5480#section-2.1.1
*/
final class ECPublicKeyAlgorithmIdentifier extends SpecificAlgorithmIdentifier implements AsymmetricCryptoAlgorithmIdentifier
{
/**
* prime192v1/secp192r1 curve OID.
*
* @see http://oid-info.com/get/1.2.840.10045.3.1.1
*
* @var string
*/
final public const CURVE_PRIME192V1 = '1.2.840.10045.3.1.1';
/**
* prime192v2 curve OID.
*
* @see http://oid-info.com/get/1.2.840.10045.3.1.2
*
* @var string
*/
final public const CURVE_PRIME192V2 = '1.2.840.10045.3.1.2';
/**
* prime192v3 curve OID.
*
* @see http://oid-info.com/get/1.2.840.10045.3.1.3
*
* @var string
*/
final public const CURVE_PRIME192V3 = '1.2.840.10045.3.1.3';
/**
* prime239v1 curve OID.
*
* @see http://oid-info.com/get/1.2.840.10045.3.1.4
*
* @var string
*/
final public const CURVE_PRIME239V1 = '1.2.840.10045.3.1.4';
/**
* prime239v2 curve OID.
*
* @see http://oid-info.com/get/1.2.840.10045.3.1.5
*
* @var string
*/
final public const CURVE_PRIME239V2 = '1.2.840.10045.3.1.5';
/**
* prime239v3 curve OID.
*
* @see http://oid-info.com/get/1.2.840.10045.3.1.6
*
* @var string
*/
final public const CURVE_PRIME239V3 = '1.2.840.10045.3.1.6';
/**
* prime256v1/secp256r1 curve OID.
*
* @see http://oid-info.com/get/1.2.840.10045.3.1.7
*
* @var string
*/
final public const CURVE_PRIME256V1 = '1.2.840.10045.3.1.7';
/**
* "SEC 2" recommended elliptic curve domain - secp112r1.
*
* @see http://www.oid-info.com/get/1.3.132.0.6
*
* @var string
*/
final public const CURVE_SECP112R1 = '1.3.132.0.6';
/**
* "SEC 2" recommended elliptic curve domain - secp112r2.
*
* @see http://oid-info.com/get/1.3.132.0.7
*
* @var string
*/
final public const CURVE_SECP112R2 = '1.3.132.0.7';
/**
* "SEC 2" recommended elliptic curve domain - secp128r1.
*
* @see http://oid-info.com/get/1.3.132.0.28
*
* @var string
*/
final public const CURVE_SECP128R1 = '1.3.132.0.28';
/**
* "SEC 2" recommended elliptic curve domain - secp128r2.
*
* @see http://oid-info.com/get/1.3.132.0.29
*
* @var string
*/
final public const CURVE_SECP128R2 = '1.3.132.0.29';
/**
* "SEC 2" recommended elliptic curve domain - secp160k1.
*
* @see http://oid-info.com/get/1.3.132.0.9
*
* @var string
*/
final public const CURVE_SECP160K1 = '1.3.132.0.9';
/**
* "SEC 2" recommended elliptic curve domain - secp160r1.
*
* @see http://oid-info.com/get/1.3.132.0.8
*
* @var string
*/
final public const CURVE_SECP160R1 = '1.3.132.0.8';
/**
* "SEC 2" recommended elliptic curve domain - secp160r2.
*
* @see http://oid-info.com/get/1.3.132.0.30
*
* @var string
*/
final public const CURVE_SECP160R2 = '1.3.132.0.30';
/**
* "SEC 2" recommended elliptic curve domain - secp192k1.
*
* @see http://oid-info.com/get/1.3.132.0.31
*
* @var string
*/
final public const CURVE_SECP192K1 = '1.3.132.0.31';
/**
* "SEC 2" recommended elliptic curve domain - secp224k1.
*
* @see http://oid-info.com/get/1.3.132.0.32
*
* @var string
*/
final public const CURVE_SECP224K1 = '1.3.132.0.32';
/**
* "SEC 2" recommended elliptic curve domain - secp224r1.
*
* @see http://oid-info.com/get/1.3.132.0.33
*
* @var string
*/
final public const CURVE_SECP224R1 = '1.3.132.0.33';
/**
* "SEC 2" recommended elliptic curve domain - secp256k1.
*
* @see http://oid-info.com/get/1.3.132.0.10
*
* @var string
*/
final public const CURVE_SECP256K1 = '1.3.132.0.10';
/**
* National Institute of Standards and Technology (NIST) 384-bit elliptic curve.
*
* @see http://oid-info.com/get/1.3.132.0.34
*
* @var string
*/
final public const CURVE_SECP384R1 = '1.3.132.0.34';
/**
* National Institute of Standards and Technology (NIST) 512-bit elliptic curve.
*
* @see http://oid-info.com/get/1.3.132.0.35
*
* @var string
*/
final public const CURVE_SECP521R1 = '1.3.132.0.35';
/**
* Mapping from curve OID to field bit size.
*
* @internal
*
* @var array<string, int>
*/
final public const MAP_CURVE_TO_SIZE = [
self::CURVE_PRIME192V1 => 192,
self::CURVE_PRIME192V2 => 192,
self::CURVE_PRIME192V3 => 192,
self::CURVE_PRIME239V1 => 239,
self::CURVE_PRIME239V2 => 239,
self::CURVE_PRIME239V3 => 239,
self::CURVE_PRIME256V1 => 256,
self::CURVE_SECP112R1 => 112,
self::CURVE_SECP112R2 => 112,
self::CURVE_SECP128R1 => 128,
self::CURVE_SECP128R2 => 128,
self::CURVE_SECP160K1 => 160,
self::CURVE_SECP160R1 => 160,
self::CURVE_SECP160R2 => 160,
self::CURVE_SECP192K1 => 192,
self::CURVE_SECP224K1 => 224,
self::CURVE_SECP224R1 => 224,
self::CURVE_SECP256K1 => 256,
self::CURVE_SECP384R1 => 384,
self::CURVE_SECP521R1 => 521,
];
/**
* @param string $namedCurve Curve identifier
*/
private function __construct(
private readonly string $namedCurve
) {
parent::__construct(self::OID_EC_PUBLIC_KEY);
}
public static function create(string $namedCurve): self
{
return new self($namedCurve);
}
public function name(): string
{
return 'ecPublicKey';
}
/**
* @return self
*/
public static function fromASN1Params(?UnspecifiedType $params = null): SpecificAlgorithmIdentifier
{
if (! isset($params)) {
throw new UnexpectedValueException('No parameters.');
}
$named_curve = $params->asObjectIdentifier()
->oid();
return self::create($named_curve);
}
/**
* Get OID of the named curve.
*/
public function namedCurve(): string
{
return $this->namedCurve;
}
/**
* @return ObjectIdentifier
*/
protected function paramsASN1(): ?Element
{
return ObjectIdentifier::create($this->namedCurve);
}
}

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Asymmetric;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\AlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\SpecificAlgorithmIdentifier;
use UnexpectedValueException;
/**
* Algorithm identifier for the Edwards-curve Digital Signature Algorithm (EdDSA) with curve25519.
*
* Same algorithm identifier is used for public and private keys as well as for signatures.
*
* @see http://oid-info.com/get/1.3.101.112
* @see https://tools.ietf.org/html/rfc8420#appendix-A.1
*/
final class Ed25519AlgorithmIdentifier extends RFC8410EdAlgorithmIdentifier
{
protected function __construct()
{
parent::__construct(self::OID_ED25519);
}
public static function create(): self
{
return new self();
}
/**
* @return self
*/
public static function fromASN1Params(?UnspecifiedType $params = null): SpecificAlgorithmIdentifier
{
if ($params !== null) {
throw new UnexpectedValueException('Parameters must be absent.');
}
return self::create();
}
public function name(): string
{
return 'id-Ed25519';
}
public function supportsKeyAlgorithm(AlgorithmIdentifier $algo): bool
{
return $algo->oid() === self::OID_ED25519;
}
}

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Asymmetric;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\AlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\SpecificAlgorithmIdentifier;
use UnexpectedValueException;
/**
* Algorithm identifier for the Edwards-curve Digital Signature Algorithm (EdDSA) with curve448.
*
* Same algorithm identifier is used for public and private keys as well as for signatures.
*
* @see http://oid-info.com/get/1.3.101.113
* @see https://tools.ietf.org/html/rfc8420#appendix-A.2
*/
final class Ed448AlgorithmIdentifier extends RFC8410EdAlgorithmIdentifier
{
protected function __construct()
{
parent::__construct(self::OID_ED448);
}
public static function create(): self
{
return new self();
}
/**
* @return self
*/
public static function fromASN1Params(?UnspecifiedType $params = null): SpecificAlgorithmIdentifier
{
if ($params !== null) {
throw new UnexpectedValueException('Parameters must be absent.');
}
return self::create();
}
public function name(): string
{
return 'id-Ed448';
}
public function supportsKeyAlgorithm(AlgorithmIdentifier $algo): bool
{
return $algo->oid() === self::OID_ED448;
}
}

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Asymmetric;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Feature\AsymmetricCryptoAlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Feature\SignatureAlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\SpecificAlgorithmIdentifier;
/*
From RFC 8410:
For all of the OIDs, the parameters MUST be absent.
It is possible to find systems that require the parameters to be
present. This can be due to either a defect in the original 1997
syntax or a programming error where developers never got input where
this was not true. The optimal solution is to fix these systems;
where this is not possible, the problem needs to be restricted to
that subsystem and not propagated to the Internet.
*/
/**
* Algorithm identifier for the Edwards-curve Digital Signature Algorithm (EdDSA) identifiers specified by RFC 8410.
*
* Same algorithm identifier is used for public and private keys as well as for signatures.
*
* @see https://tools.ietf.org/html/rfc8410#section-3
* @see https://tools.ietf.org/html/rfc8410#section-6
*/
abstract class RFC8410EdAlgorithmIdentifier extends SpecificAlgorithmIdentifier implements AsymmetricCryptoAlgorithmIdentifier, SignatureAlgorithmIdentifier
{
protected function paramsASN1(): ?Element
{
return null;
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Asymmetric;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Feature\AsymmetricCryptoAlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\SpecificAlgorithmIdentifier;
/*
From RFC 8410:
For all of the OIDs, the parameters MUST be absent.
It is possible to find systems that require the parameters to be
present. This can be due to either a defect in the original 1997
syntax or a programming error where developers never got input where
this was not true. The optimal solution is to fix these systems;
where this is not possible, the problem needs to be restricted to
that subsystem and not propagated to the Internet.
*/
/**
* Algorithm identifier for the Diffie-Hellman operations specified by RFC 8410.
*
* @see https://tools.ietf.org/html/rfc8410#section-3
*/
abstract class RFC8410XAlgorithmIdentifier extends SpecificAlgorithmIdentifier implements AsymmetricCryptoAlgorithmIdentifier
{
protected function paramsASN1(): ?Element
{
return null;
}
}

View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Asymmetric;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Type\Primitive\NullType;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Feature\AsymmetricCryptoAlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\SpecificAlgorithmIdentifier;
use UnexpectedValueException;
/*
From RFC 3447:
When rsaEncryption is used in an AlgorithmIdentifier the
parameters MUST be present and MUST be NULL.
*/
/**
* Algorithm identifier for RSA encryption.
*
* @see http://www.oid-info.com/get/1.2.840.113549.1.1.1
* @see https://tools.ietf.org/html/rfc3447#appendix-C
*/
final class RSAEncryptionAlgorithmIdentifier extends SpecificAlgorithmIdentifier implements AsymmetricCryptoAlgorithmIdentifier
{
private function __construct()
{
parent::__construct(self::OID_RSA_ENCRYPTION);
}
public static function create(): self
{
return new self();
}
public function name(): string
{
return 'rsaEncryption';
}
/**
* @return self
*/
public static function fromASN1Params(?UnspecifiedType $params = null): SpecificAlgorithmIdentifier
{
if (! isset($params)) {
throw new UnexpectedValueException('No parameters.');
}
$params->asNull();
return self::create();
}
/**
* @return NullType
*/
protected function paramsASN1(): ?Element
{
return NullType::create();
}
}

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Asymmetric;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\SpecificAlgorithmIdentifier;
use UnexpectedValueException;
/**
* Algorithm identifier for the Diffie-Hellman operation with curve25519.
*
* @see http://oid-info.com/get/1.3.101.110
*/
final class X25519AlgorithmIdentifier extends RFC8410XAlgorithmIdentifier
{
private function __construct()
{
parent::__construct(self::OID_X25519);
}
public static function create(): self
{
return new self();
}
/**
* @return self
*/
public static function fromASN1Params(?UnspecifiedType $params = null): SpecificAlgorithmIdentifier
{
if ($params !== null) {
throw new UnexpectedValueException('Parameters must be absent.');
}
return self::create();
}
public function name(): string
{
return 'id-X25519';
}
}

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Asymmetric;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\SpecificAlgorithmIdentifier;
use UnexpectedValueException;
/**
* Algorithm identifier for the Diffie-Hellman operation with curve448.
*
* @see http://oid-info.com/get/1.3.101.111
*/
final class X448AlgorithmIdentifier extends RFC8410XAlgorithmIdentifier
{
private function __construct()
{
parent::__construct(self::OID_X448);
}
public static function create(): self
{
return new self();
}
/**
* @return self
*/
public static function fromASN1Params(?UnspecifiedType $params = null): SpecificAlgorithmIdentifier
{
if ($params !== null) {
throw new UnexpectedValueException('Parameters must be absent.');
}
return self::create();
}
public function name(): string
{
return 'id-X448';
}
}

View File

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Cipher;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\SpecificAlgorithmIdentifier;
use UnexpectedValueException;
/**
* Algorithm identifier for AES with 128-bit key in CBC mode.
*
* @see https://tools.ietf.org/html/rfc3565.html#section-4.1
* @see http://www.alvestrand.no/objectid/2.16.840.1.101.3.4.1.2.html
* @see http://www.oid-info.com/get/2.16.840.1.101.3.4.1.2
*/
final class AES128CBCAlgorithmIdentifier extends AESCBCAlgorithmIdentifier
{
/**
* @param string $iv Initialization vector
*/
protected function __construct(string $iv)
{
parent::__construct(self::OID_AES_128_CBC, $iv);
}
public static function create(string $iv): self
{
return new self($iv);
}
/**
* @return self
*/
public static function fromASN1Params(?UnspecifiedType $params = null): SpecificAlgorithmIdentifier
{
if (! isset($params)) {
throw new UnexpectedValueException('No parameters.');
}
$iv = $params->asOctetString()
->string();
return self::create($iv);
}
public function name(): string
{
return 'aes128-CBC';
}
public function keySize(): int
{
return 16;
}
}

View File

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Cipher;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\SpecificAlgorithmIdentifier;
use UnexpectedValueException;
/**
* Algorithm identifier for AES with 192-bit key in CBC mode.
*
* @see https://tools.ietf.org/html/rfc3565.html#section-4.1
* @see http://www.alvestrand.no/objectid/2.16.840.1.101.3.4.1.22.html
* @see http://www.oid-info.com/get/2.16.840.1.101.3.4.1.22
*/
final class AES192CBCAlgorithmIdentifier extends AESCBCAlgorithmIdentifier
{
/**
* @param null|string $iv Initialization vector
*/
protected function __construct(?string $iv = null)
{
parent::__construct(self::OID_AES_192_CBC, $iv);
}
public static function create(?string $iv = null): self
{
return new self($iv);
}
/**
* @return self
*/
public static function fromASN1Params(?UnspecifiedType $params = null): SpecificAlgorithmIdentifier
{
if (! isset($params)) {
throw new UnexpectedValueException('No parameters.');
}
$iv = $params->asOctetString()
->string();
return self::create($iv);
}
public function name(): string
{
return 'aes192-CBC';
}
public function keySize(): int
{
return 24;
}
}

View File

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Cipher;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\SpecificAlgorithmIdentifier;
use UnexpectedValueException;
/**
* Algorithm identifier for AES with 256-bit key in CBC mode.
*
* @see https://tools.ietf.org/html/rfc3565.html#section-4.1
* @see http://www.alvestrand.no/objectid/2.16.840.1.101.3.4.1.42.html
* @see http://www.oid-info.com/get/2.16.840.1.101.3.4.1.42
*/
final class AES256CBCAlgorithmIdentifier extends AESCBCAlgorithmIdentifier
{
/**
* @param null|string $iv Initialization vector
*/
protected function __construct(?string $iv = null)
{
parent::__construct(self::OID_AES_256_CBC, $iv);
}
public static function create(?string $iv = null): self
{
return new self($iv);
}
/**
* @return self
*/
public static function fromASN1Params(?UnspecifiedType $params = null): SpecificAlgorithmIdentifier
{
if (! isset($params)) {
throw new UnexpectedValueException('No parameters.');
}
$iv = $params->asOctetString()
->string();
return new static($iv);
}
public function name(): string
{
return 'aes256-CBC';
}
public function keySize(): int
{
return 32;
}
}

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Cipher;
use LogicException;
use SpomkyLabs\Pki\ASN1\Type\Primitive\OctetString;
/*
From RFC 3565 - 4.1. AES Algorithm Identifiers and Parameters:
The AlgorithmIdentifier parameters field MUST be present, and the parameter field MUST contain a AES-IV:
AES-IV ::= OCTET STRING (SIZE(16))
*/
/**
* Base class for AES-CBC algorithm identifiers.
*
* @see https://tools.ietf.org/html/rfc3565.html#section-4.1
*/
abstract class AESCBCAlgorithmIdentifier extends BlockCipherAlgorithmIdentifier
{
public function blockSize(): int
{
return 16;
}
public function ivSize(): int
{
return 16;
}
protected function paramsASN1(): OctetString
{
if (! isset($this->initializationVector)) {
throw new LogicException('IV not set.');
}
return OctetString::create($this->initializationVector);
}
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Cipher;
/**
* Base class for block cipher algorithm identifiers.
*/
abstract class BlockCipherAlgorithmIdentifier extends CipherAlgorithmIdentifier
{
/**
* Get block size in bytes.
*/
abstract public function blockSize(): int;
}

View File

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Cipher;
use function mb_strlen;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\SpecificAlgorithmIdentifier;
use UnexpectedValueException;
/**
* Base class for cipher algorithm identifiers.
*/
abstract class CipherAlgorithmIdentifier extends SpecificAlgorithmIdentifier
{
protected function __construct(
string $oid,
protected string $initializationVector
) {
$this->_checkIVSize($initializationVector);
parent::__construct($oid);
}
/**
* Get key size in bytes.
*/
abstract public function keySize(): int;
/**
* Get the initialization vector size in bytes.
*/
abstract public function ivSize(): int;
/**
* Get initialization vector.
*/
public function initializationVector(): string
{
return $this->initializationVector;
}
/**
* Get copy of the object with given initialization vector.
*
* @param string $iv Initialization vector or null to remove
*/
public function withInitializationVector(string $iv): self
{
$this->_checkIVSize($iv);
$obj = clone $this;
$obj->initializationVector = $iv;
return $obj;
}
/**
* Check that initialization vector size is valid for the cipher.
*/
protected function _checkIVSize(string $iv): void
{
if (mb_strlen($iv, '8bit') !== $this->ivSize()) {
throw new UnexpectedValueException('Invalid IV size.');
}
}
}

View File

@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Cipher;
use LogicException;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Type\Primitive\OctetString;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\SpecificAlgorithmIdentifier;
use UnexpectedValueException;
/*
RFC 2898 defines parameters as follows:
{OCTET STRING (SIZE(8)) IDENTIFIED BY desCBC}
*/
/**
* Algorithm identifier for DES cipher in CBC mode.
*
* @see http://www.alvestrand.no/objectid/1.3.14.3.2.7.html
* @see http://www.oid-info.com/get/1.3.14.3.2.7
* @see https://tools.ietf.org/html/rfc2898#appendix-C
*/
final class DESCBCAlgorithmIdentifier extends BlockCipherAlgorithmIdentifier
{
/**
* @param null|string $iv Initialization vector
*/
private function __construct(?string $iv)
{
$this->_checkIVSize($iv);
parent::__construct(self::OID_DES_CBC, $iv);
}
public static function create(?string $iv = null): self
{
return new self($iv);
}
public function name(): string
{
return 'desCBC';
}
/**
* @return self
*/
public static function fromASN1Params(?UnspecifiedType $params = null): SpecificAlgorithmIdentifier
{
if (! isset($params)) {
throw new UnexpectedValueException('No parameters.');
}
$iv = $params->asOctetString()
->string();
return self::create($iv);
}
public function blockSize(): int
{
return 8;
}
public function keySize(): int
{
return 8;
}
public function ivSize(): int
{
return 8;
}
/**
* @return OctetString
*/
protected function paramsASN1(): ?Element
{
if (! isset($this->initializationVector)) {
throw new LogicException('IV not set.');
}
return OctetString::create($this->initializationVector);
}
}

View File

@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Cipher;
use LogicException;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Type\Primitive\OctetString;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\SpecificAlgorithmIdentifier;
use UnexpectedValueException;
/*
RFC 2898 defines parameters as follows:
{OCTET STRING (SIZE(8)) IDENTIFIED BY des-EDE3-CBC}
*/
/**
* Algorithm identifier for Triple-DES cipher in CBC mode.
*
* @see http://www.alvestrand.no/objectid/1.2.840.113549.3.7.html
* @see http://oid-info.com/get/1.2.840.113549.3.7
* @see https://tools.ietf.org/html/rfc2898#appendix-C
* @see https://tools.ietf.org/html/rfc2630#section-12.4.1
*/
final class DESEDE3CBCAlgorithmIdentifier extends BlockCipherAlgorithmIdentifier
{
/**
* @param null|string $iv Initialization vector
*/
private function __construct(?string $iv)
{
parent::__construct(self::OID_DES_EDE3_CBC, $iv);
$this->_checkIVSize($iv);
}
public static function create(?string $iv = null): self
{
return new self($iv);
}
public function name(): string
{
return 'des-EDE3-CBC';
}
/**
* @return self
*/
public static function fromASN1Params(?UnspecifiedType $params = null): SpecificAlgorithmIdentifier
{
if (! isset($params)) {
throw new UnexpectedValueException('No parameters.');
}
$iv = $params->asOctetString()
->string();
return self::create($iv);
}
public function blockSize(): int
{
return 8;
}
public function keySize(): int
{
return 24;
}
public function ivSize(): int
{
return 8;
}
/**
* @return OctetString
*/
protected function paramsASN1(): ?Element
{
if (! isset($this->initializationVector)) {
throw new LogicException('IV not set.');
}
return OctetString::create($this->initializationVector);
}
}

View File

@ -0,0 +1,200 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Cipher;
use LogicException;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Type\Constructed\Sequence;
use SpomkyLabs\Pki\ASN1\Type\Primitive\Integer;
use SpomkyLabs\Pki\ASN1\Type\Primitive\OctetString;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\SpecificAlgorithmIdentifier;
use UnexpectedValueException;
/*
Parameters may be seen in various forms. This implementation attemts
to take them all into consideration.
# RFC 2268 - A Description of the RC2(r) Encryption Algorithm
RC2-CBCParameter ::= CHOICE {
iv IV,
params SEQUENCE {
version RC2Version,
iv IV
}
}
# RFC 2898 - PKCS #5: Password-Based Cryptography Specification Version 2.0
RC2-CBC-Parameter ::= SEQUENCE {
rc2ParameterVersion INTEGER OPTIONAL,
iv OCTET STRING (SIZE(8))
}
# RFC 3370 - Cryptographic Message Syntax (CMS) Algorithms
RC2CBCParameter ::= SEQUENCE {
rc2ParameterVersion INTEGER,
iv OCTET STRING } -- exactly 8 octets
*/
/**
* Algorithm identifier for RC2 cipher in CBC mode.
*
* @see http://www.alvestrand.no/objectid/1.2.840.113549.3.2.html
* @see http://www.oid-info.com/get/1.2.840.113549.3.2
* @see https://tools.ietf.org/html/rfc2268#section-6
* @see https://tools.ietf.org/html/rfc3370#section-5.2
* @see https://tools.ietf.org/html/rfc2898#appendix-C
*/
final class RC2CBCAlgorithmIdentifier extends BlockCipherAlgorithmIdentifier
{
/**
* RFC 2268 translation table for effective key bits.
*
* This table maps effective key bytes from 0..255 to version number.
*
* @var array<int> ekb => version
*/
private const EKB_TABLE = [
0xbd, 0x56, 0xea, 0xf2, 0xa2, 0xf1, 0xac, 0x2a,
0xb0, 0x93, 0xd1, 0x9c, 0x1b, 0x33, 0xfd, 0xd0,
0x30, 0x04, 0xb6, 0xdc, 0x7d, 0xdf, 0x32, 0x4b,
0xf7, 0xcb, 0x45, 0x9b, 0x31, 0xbb, 0x21, 0x5a,
0x41, 0x9f, 0xe1, 0xd9, 0x4a, 0x4d, 0x9e, 0xda,
0xa0, 0x68, 0x2c, 0xc3, 0x27, 0x5f, 0x80, 0x36,
0x3e, 0xee, 0xfb, 0x95, 0x1a, 0xfe, 0xce, 0xa8,
0x34, 0xa9, 0x13, 0xf0, 0xa6, 0x3f, 0xd8, 0x0c,
0x78, 0x24, 0xaf, 0x23, 0x52, 0xc1, 0x67, 0x17,
0xf5, 0x66, 0x90, 0xe7, 0xe8, 0x07, 0xb8, 0x60,
0x48, 0xe6, 0x1e, 0x53, 0xf3, 0x92, 0xa4, 0x72,
0x8c, 0x08, 0x15, 0x6e, 0x86, 0x00, 0x84, 0xfa,
0xf4, 0x7f, 0x8a, 0x42, 0x19, 0xf6, 0xdb, 0xcd,
0x14, 0x8d, 0x50, 0x12, 0xba, 0x3c, 0x06, 0x4e,
0xec, 0xb3, 0x35, 0x11, 0xa1, 0x88, 0x8e, 0x2b,
0x94, 0x99, 0xb7, 0x71, 0x74, 0xd3, 0xe4, 0xbf,
0x3a, 0xde, 0x96, 0x0e, 0xbc, 0x0a, 0xed, 0x77,
0xfc, 0x37, 0x6b, 0x03, 0x79, 0x89, 0x62, 0xc6,
0xd7, 0xc0, 0xd2, 0x7c, 0x6a, 0x8b, 0x22, 0xa3,
0x5b, 0x05, 0x5d, 0x02, 0x75, 0xd5, 0x61, 0xe3,
0x18, 0x8f, 0x55, 0x51, 0xad, 0x1f, 0x0b, 0x5e,
0x85, 0xe5, 0xc2, 0x57, 0x63, 0xca, 0x3d, 0x6c,
0xb4, 0xc5, 0xcc, 0x70, 0xb2, 0x91, 0x59, 0x0d,
0x47, 0x20, 0xc8, 0x4f, 0x58, 0xe0, 0x01, 0xe2,
0x16, 0x38, 0xc4, 0x6f, 0x3b, 0x0f, 0x65, 0x46,
0xbe, 0x7e, 0x2d, 0x7b, 0x82, 0xf9, 0x40, 0xb5,
0x1d, 0x73, 0xf8, 0xeb, 0x26, 0xc7, 0x87, 0x97,
0x25, 0x54, 0xb1, 0x28, 0xaa, 0x98, 0x9d, 0xa5,
0x64, 0x6d, 0x7a, 0xd4, 0x10, 0x81, 0x44, 0xef,
0x49, 0xd6, 0xae, 0x2e, 0xdd, 0x76, 0x5c, 0x2f,
0xa7, 0x1c, 0xc9, 0x09, 0x69, 0x9a, 0x83, 0xcf,
0x29, 0x39, 0xb9, 0xe9, 0x4c, 0xff, 0x43, 0xab,
];
/**
* @param int $effectiveKeyBits Number of effective key bits
* @param null|string $iv Initialization vector
*/
private function __construct(
private readonly int $effectiveKeyBits,
?string $iv
) {
parent::__construct(self::OID_RC2_CBC, $iv);
$this->_checkIVSize($iv);
}
public static function create(int $_effectiveKeyBits = 64, ?string $iv = null): self
{
return new self($_effectiveKeyBits, $iv);
}
public function name(): string
{
return 'rc2-cbc';
}
/**
* @return self
*/
public static function fromASN1Params(?UnspecifiedType $params = null): SpecificAlgorithmIdentifier
{
if (! isset($params)) {
throw new UnexpectedValueException('No parameters.');
}
$key_bits = 32;
// rfc2268 a choice containing only IV
if ($params->isType(Element::TYPE_OCTET_STRING)) {
$iv = $params->asOctetString()
->string();
} else {
$seq = $params->asSequence();
$idx = 0;
// version is optional in rfc2898
if ($seq->has($idx, Element::TYPE_INTEGER)) {
$version = $seq->at($idx++)
->asInteger()
->intNumber();
$key_bits = self::_versionToEKB($version);
}
// IV is present in all variants
$iv = $seq->at($idx)
->asOctetString()
->string();
}
return self::create($key_bits, $iv);
}
/**
* Get number of effective key bits.
*/
public function effectiveKeyBits(): int
{
return $this->effectiveKeyBits;
}
public function blockSize(): int
{
return 8;
}
public function keySize(): int
{
return (int) round($this->effectiveKeyBits / 8);
}
public function ivSize(): int
{
return 8;
}
/**
* @return Sequence
*/
protected function paramsASN1(): ?Element
{
if ($this->effectiveKeyBits >= 256) {
$version = $this->effectiveKeyBits;
} else {
$version = self::EKB_TABLE[$this->effectiveKeyBits];
}
if (! isset($this->initializationVector)) {
throw new LogicException('IV not set.');
}
return Sequence::create(Integer::create($version), OctetString::create($this->initializationVector));
}
/**
* Translate version number to number of effective key bits.
*/
private static function _versionToEKB(int $version): int
{
static $lut;
if ($version > 255) {
return $version;
}
if (! isset($lut)) {
$lut = array_flip(self::EKB_TABLE);
}
return $lut[$version];
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Feature;
use SpomkyLabs\Pki\ASN1\Type\Constructed\Sequence;
/**
* Base interface for algorithm identifiers.
*/
interface AlgorithmIdentifierType
{
/**
* Get the object identifier of the algorithm.
*
* @return string Object identifier in dotted format
*/
public function oid(): string;
/**
* Get a human-readable name of the algorithm.
*/
public function name(): string;
/**
* Generate ASN.1 structure.
*/
public function toASN1(): Sequence;
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Feature;
/**
* Algorithm identifier for asymmetric cryptography algorithms.
*/
interface AsymmetricCryptoAlgorithmIdentifier extends AlgorithmIdentifierType
{
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Feature;
/**
* Algorithm identifier for encryption algorithms.
*/
interface EncryptionAlgorithmIdentifier extends AlgorithmIdentifierType
{
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Feature;
/**
* Algorithm identifier for hash functions.
*/
interface HashAlgorithmIdentifier extends AlgorithmIdentifierType
{
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Feature;
/**
* Algorithm identifier for pseudorandom functions.
*/
interface PRFAlgorithmIdentifier extends AlgorithmIdentifierType
{
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Feature;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\AlgorithmIdentifier;
/**
* Algorithm identifier for signature algorithms.
*/
interface SignatureAlgorithmIdentifier extends AlgorithmIdentifierType
{
/**
* Check whether signature algorithm supports given key algorithm.
*/
public function supportsKeyAlgorithm(AlgorithmIdentifier $algo): bool;
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
/**
* Generic algorithm identifier to hold parameters as ASN.1 objects.
*/
final class GenericAlgorithmIdentifier extends AlgorithmIdentifier
{
/**
* @param string $oid Algorithm OID
* @param null|UnspecifiedType $params Parameters
*/
private function __construct(
string $oid,
private readonly ?UnspecifiedType $params
) {
parent::__construct($oid);
}
public static function create(string $oid, ?UnspecifiedType $params = null): self
{
return new self($oid, $params);
}
public function name(): string
{
return $this->oid;
}
public function parameters(): ?UnspecifiedType
{
return $this->params;
}
protected function paramsASN1(): ?Element
{
return $this->params?->asElement();
}
}

View File

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Hash;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Feature\HashAlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Feature\PRFAlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\SpecificAlgorithmIdentifier;
use UnexpectedValueException;
/*
Per RFC 2898 this algorithm identifier has no parameters:
algid-hmacWithSHA1 AlgorithmIdentifier {{PBKDF2-PRFs}} ::=
{algorithm id-hmacWithSHA1, parameters NULL : NULL}
*/
/**
* HMAC-SHA-1 algorithm identifier.
*
* @see http://www.alvestrand.no/objectid/1.2.840.113549.2.7.html
* @see http://www.oid-info.com/get/1.2.840.113549.2.7
* @see https://tools.ietf.org/html/rfc2898#appendix-C
*/
final class HMACWithSHA1AlgorithmIdentifier extends SpecificAlgorithmIdentifier implements HashAlgorithmIdentifier, PRFAlgorithmIdentifier
{
private function __construct()
{
parent::__construct(self::OID_HMAC_WITH_SHA1);
}
public static function create(): self
{
return new self();
}
public function name(): string
{
return 'hmacWithSHA1';
}
/**
* @return self
*/
public static function fromASN1Params(?UnspecifiedType $params = null): SpecificAlgorithmIdentifier
{
if (isset($params)) {
throw new UnexpectedValueException('Parameters must be omitted.');
}
return self::create();
}
protected function paramsASN1(): ?Element
{
return null;
}
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Hash;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
/**
* HMAC with SHA-224 algorithm identifier.
*
* @see https://tools.ietf.org/html/rfc4231#section-3.1
*/
final class HMACWithSHA224AlgorithmIdentifier extends RFC4231HMACAlgorithmIdentifier
{
private function __construct(?Element $params)
{
parent::__construct(self::OID_HMAC_WITH_SHA224, $params);
}
public static function create(?Element $params = null): self
{
return new self($params);
}
public static function fromASN1Params(?UnspecifiedType $params = null): self
{
/*
* RFC 4231 states that the "parameter" component SHOULD be present
* but have type NULL.
*/
return self::create($params?->asNull());
}
public function name(): string
{
return 'hmacWithSHA224';
}
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Hash;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
/**
* HMAC with SHA-256 algorithm identifier.
*
* @see https://tools.ietf.org/html/rfc4231#section-3.1
*/
final class HMACWithSHA256AlgorithmIdentifier extends RFC4231HMACAlgorithmIdentifier
{
private function __construct(?Element $params)
{
parent::__construct(self::OID_HMAC_WITH_SHA256, $params);
}
public static function create(?Element $params = null): self
{
return new self($params);
}
public static function fromASN1Params(?UnspecifiedType $params = null): self
{
/*
* RFC 4231 states that the "parameter" component SHOULD be present
* but have type NULL.
*/
return self::create($params?->asNull());
}
public function name(): string
{
return 'hmacWithSHA256';
}
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Hash;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
/**
* HMAC with SHA-384 algorithm identifier.
*
* @see https://tools.ietf.org/html/rfc4231#section-3.1
*/
final class HMACWithSHA384AlgorithmIdentifier extends RFC4231HMACAlgorithmIdentifier
{
private function __construct(?Element $params)
{
parent::__construct(self::OID_HMAC_WITH_SHA384, $params);
}
public static function create(?Element $params = null): self
{
return new self($params);
}
public static function fromASN1Params(?UnspecifiedType $params = null): self
{
/*
* RFC 4231 states that the "parameter" component SHOULD be present
* but have type NULL.
*/
return new self($params?->asNull());
}
public function name(): string
{
return 'hmacWithSHA384';
}
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Hash;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
/**
* HMAC with SHA-512 algorithm identifier.
*
* @see https://tools.ietf.org/html/rfc4231#section-3.1
*/
final class HMACWithSHA512AlgorithmIdentifier extends RFC4231HMACAlgorithmIdentifier
{
private function __construct(?Element $params)
{
parent::__construct(self::OID_HMAC_WITH_SHA512, $params);
}
public static function create(?Element $params = null): self
{
return new self($params);
}
public static function fromASN1Params(?UnspecifiedType $params = null): self
{
/*
* RFC 4231 states that the "parameter" component SHOULD be present
* but have type NULL.
*/
return self::create($params?->asNull());
}
public function name(): string
{
return 'hmacWithSHA512';
}
}

View File

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Hash;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Type\Primitive\NullType;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Feature\HashAlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\SpecificAlgorithmIdentifier;
/*
From RFC 1321 - 1. Executive Summary
In the X.509 type AlgorithmIdentifier, the parameters for MD5
should have type NULL.
From RFC 3370 - 2.2 MD5
The AlgorithmIdentifier parameters field MUST be present, and the
parameters field MUST contain NULL. Implementations MAY accept the
MD5 AlgorithmIdentifiers with absent parameters as well as NULL
parameters.
*/
/**
* MD5 algorithm identifier.
*
* @see http://oid-info.com/get/1.2.840.113549.2.5
* @see https://tools.ietf.org/html/rfc1321#section-1
* @see https://tools.ietf.org/html/rfc3370#section-2.2
*/
final class MD5AlgorithmIdentifier extends SpecificAlgorithmIdentifier implements HashAlgorithmIdentifier
{
/**
* Parameters.
*/
private ?NullType $params;
private function __construct()
{
parent::__construct(self::OID_MD5);
$this->params = NullType::create();
}
public static function create(): self
{
return new self();
}
public function name(): string
{
return 'md5';
}
public static function fromASN1Params(?UnspecifiedType $params = null): static
{
$obj = static::create();
// if parameters field is present, it must be null type
if (isset($params)) {
$obj->params = $params->asNull();
}
return $obj;
}
/**
* @return null|NullType
*/
protected function paramsASN1(): ?Element
{
return $this->params;
}
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Hash;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Type\Primitive\NullType;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Feature\HashAlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Feature\PRFAlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\SpecificAlgorithmIdentifier;
/**
* Base class for HMAC algorithm identifiers specified in RFC 4231.
*
* @see https://tools.ietf.org/html/rfc4231#section-3.1
*/
abstract class RFC4231HMACAlgorithmIdentifier extends SpecificAlgorithmIdentifier implements HashAlgorithmIdentifier, PRFAlgorithmIdentifier
{
/**
* @param Element|null $params Parameters stored for re-encoding.
*/
protected function __construct(
string $oid,
protected ?Element $params
) {
parent::__construct($oid);
}
/**
* @return null|NullType
*/
protected function paramsASN1(): ?Element
{
return $this->params;
}
}

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Hash;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Type\Primitive\NullType;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Feature\HashAlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\SpecificAlgorithmIdentifier;
/*
From RFC 3370 - 2.1 SHA-1
The AlgorithmIdentifier parameters field is OPTIONAL. If present,
the parameters field MUST contain a NULL. Implementations MUST
accept SHA-1 AlgorithmIdentifiers with absent parameters.
Implementations MUST accept SHA-1 AlgorithmIdentifiers with NULL
parameters. Implementations SHOULD generate SHA-1
AlgorithmIdentifiers with absent parameters.
*/
/**
* SHA-1 algorithm identifier.
*
* @see http://oid-info.com/get/1.3.14.3.2.26
* @see https://tools.ietf.org/html/rfc3370#section-2.1
*/
final class SHA1AlgorithmIdentifier extends SpecificAlgorithmIdentifier implements HashAlgorithmIdentifier
{
/**
* Parameters.
*/
private ?NullType $params;
private function __construct()
{
parent::__construct(self::OID_SHA1);
$this->params = null;
}
public static function create(): self
{
return new self();
}
public function name(): string
{
return 'sha1';
}
public static function fromASN1Params(?UnspecifiedType $params = null): static
{
$obj = static::create();
// if parameters field is present, it must be null type
if (isset($params)) {
$obj->params = $params->asNull();
}
return $obj;
}
/**
* @return null|NullType
*/
protected function paramsASN1(): ?Element
{
return $this->params;
}
}

View File

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Hash;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\SpecificAlgorithmIdentifier;
/**
* SHA-224 algorithm identifier.
*
* @see http://oid-info.com/get/2.16.840.1.101.3.4.2.4
* @see https://tools.ietf.org/html/rfc3874#section-4
* @see https://tools.ietf.org/html/rfc4055#section-2.1
* @see https://tools.ietf.org/html/rfc5754#section-2.1
*/
final class SHA224AlgorithmIdentifier extends SHA2AlgorithmIdentifier
{
private function __construct()
{
parent::__construct(self::OID_SHA224);
}
public static function create(): self
{
return new self();
}
/**
* @return self
*/
public static function fromASN1Params(?UnspecifiedType $params = null): SpecificAlgorithmIdentifier
{
$obj = new static();
// if parameters field is present, it must be null type
if (isset($params)) {
$obj->_params = $params->asNull();
}
return $obj;
}
public function name(): string
{
return 'sha224';
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Hash;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\SpecificAlgorithmIdentifier;
/**
* SHA-256 algorithm identifier.
*
* @see http://oid-info.com/get/2.16.840.1.101.3.4.2.1
* @see https://tools.ietf.org/html/rfc4055#section-2.1
* @see https://tools.ietf.org/html/rfc5754#section-2.2
*/
final class SHA256AlgorithmIdentifier extends SHA2AlgorithmIdentifier
{
private function __construct()
{
parent::__construct(self::OID_SHA256);
}
public static function create(): self
{
return new self();
}
/**
* @return self
*/
public static function fromASN1Params(?UnspecifiedType $params = null): SpecificAlgorithmIdentifier
{
$obj = new static();
// if parameters field is present, it must be null type
if (isset($params)) {
$obj->_params = $params->asNull();
}
return $obj;
}
public function name(): string
{
return 'sha256';
}
}

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