primo commit

This commit is contained in:
2024-12-17 17:34:10 +01:00
commit e650f8df99
16435 changed files with 2451012 additions and 0 deletions

View File

@ -0,0 +1,278 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Component;
use Brick\Math\BigInteger;
use SpomkyLabs\Pki\ASN1\Exception\DecodeException;
use SpomkyLabs\Pki\ASN1\Feature\Encodable;
use SpomkyLabs\Pki\ASN1\Util\BigInt;
use function array_key_exists;
use function mb_strlen;
use function ord;
/**
* Class to represent BER/DER identifier octets.
* @see \SpomkyLabs\Pki\Test\ASN1\Component\IdentifierTest
*/
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 DomainException;
use LogicException;
use SpomkyLabs\Pki\ASN1\Exception\DecodeException;
use SpomkyLabs\Pki\ASN1\Feature\Encodable;
use SpomkyLabs\Pki\ASN1\Util\BigInt;
use function count;
use function mb_strlen;
use function ord;
/**
* 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,81 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1;
use BadMethodCallException;
use SpomkyLabs\Pki\ASN1\Component\Identifier;
use SpomkyLabs\Pki\ASN1\Component\Length;
use SpomkyLabs\Pki\ASN1\Feature\ElementBase;
use function mb_strlen;
/**
* Container for raw DER encoded data.
*
* May be inserted into structure without decoding first.
* @see \SpomkyLabs\Pki\Test\ASN1\DERDataTest
*/
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,475 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1;
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;
use function array_key_exists;
use function mb_strlen;
/**
* Base class for all ASN.1 type elements.
* @see \SpomkyLabs\Pki\Test\ASN1\ElementTest
*/
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 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;
use function count;
/**
* 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 SpomkyLabs\Pki\ASN1\Type\PrimitiveString;
use SpomkyLabs\Pki\ASN1\Type\UniversalClass;
use function mb_strlen;
/**
* 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 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;
use function chr;
use function mb_strlen;
use function ord;
/**
* 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 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 function chr;
use function ord;
/**
* 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 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;
use function intval;
use function mb_strlen;
/**
* 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 InvalidArgumentException;
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;
use function gettype;
use function is_int;
use function is_scalar;
use function is_string;
/**
* 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 InvalidArgumentException;
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 gettype;
use function is_int;
use function is_scalar;
use function is_string;
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 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;
use function chr;
use function count;
use function is_int;
use function mb_strlen;
use function ord;
/**
* 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 LogicException;
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;
use function chr;
use function count;
use function in_array;
use function mb_strlen;
use function ord;
use const INF;
/**
* 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 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;
use function chr;
use function is_int;
use function ord;
/**
* 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 SpomkyLabs\Pki\ASN1\Type\PrimitiveString;
use SpomkyLabs\Pki\ASN1\Type\UniversalClass;
use function mb_strlen;
/**
* 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 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;
use function count;
/**
* 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,519 @@
<?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.
* @see \SpomkyLabs\Pki\Test\ASN1\Type\UnspecifiedTypeTest
*/
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,126 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Util;
use Brick\Math\BigInteger;
use InvalidArgumentException;
use Stringable;
use Throwable;
use function mb_strlen;
/**
* Class to wrap an integer of arbirtary length.
* @see \SpomkyLabs\Pki\Test\ASN1\Util\BigIntTest
*/
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,148 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\ASN1\Util;
use Brick\Math\BigInteger;
use OutOfBoundsException;
use RuntimeException;
use SpomkyLabs\Pki\ASN1\Type\Primitive\BitString;
use function assert;
use function count;
use function is_array;
use function ord;
/**
* Class to handle a bit string as a field of flags.
* @see \SpomkyLabs\Pki\Test\ASN1\Util\FlagsTest
*/
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 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;
use function defined;
/**
* 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 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;
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;
/**
* 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 RuntimeException;
use Stringable;
use UnexpectedValueException;
use function is_string;
/**
* 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 Countable;
use IteratorAggregate;
use LogicException;
use RuntimeException;
use Stringable;
use UnexpectedValueException;
use function count;
use function is_string;
use const PREG_SET_ORDER;
/**
* 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,137 @@
<?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_RSASSA_PSS_ENCRYPTION = '1.2.840.113549.1.1.10';
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 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;
use function array_key_exists;
/**
* 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,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.10
* @see https://datatracker.ietf.org/doc/html/rfc8017#section-8.1
*/
final class RSAPSSSSAEncryptionAlgorithmIdentifier extends SpecificAlgorithmIdentifier implements AsymmetricCryptoAlgorithmIdentifier
{
private function __construct()
{
parent::__construct(self::OID_RSASSA_PSS_ENCRYPTION);
}
public static function create(): self
{
return new self();
}
public function name(): string
{
return 'rsassa-pss';
}
/**
* @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 SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\SpecificAlgorithmIdentifier;
use UnexpectedValueException;
use function mb_strlen;
/**
* 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