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,180 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\ASN1;
use ArrayIterator;
use Countable;
use IteratorAggregate;
use LogicException;
use SpomkyLabs\Pki\ASN1\Type\Constructed\Sequence;
use SpomkyLabs\Pki\ASN1\Type\Constructed\Set;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
use SpomkyLabs\Pki\X501\ASN1\AttributeValue\AttributeValue;
use function count;
/**
* Implements *Attribute* ASN.1 type.
*
* @see https://www.itu.int/ITU-T/formal-language/itu-t/x/x501/2012/InformationFramework.html#InformationFramework.Attribute
*/
final class Attribute implements Countable, IteratorAggregate
{
/**
* Attribute type.
*/
private readonly AttributeType $type;
/**
* Attribute values.
*
* @var AttributeValue[]
*/
private readonly array $values;
/**
* @param AttributeType $type Attribute type
* @param AttributeValue ...$values Attribute values
*/
private function __construct(AttributeType $type, AttributeValue ...$values)
{
// check that attribute values have correct oid
foreach ($values as $value) {
if ($value->oid() !== $type->oid()) {
throw new LogicException('Attribute OID mismatch.');
}
}
$this->type = $type;
$this->values = $values;
}
public static function create(AttributeType $type, AttributeValue ...$values): self
{
return new self($type, ...$values);
}
/**
* Initialize from ASN.1.
*/
public static function fromASN1(Sequence $seq): self
{
$type = AttributeType::fromASN1($seq->at(0)->asObjectIdentifier());
$values = array_map(
static fn (UnspecifiedType $el) => AttributeValue::fromASN1ByOID($type->oid(), $el),
$seq->at(1)
->asSet()
->elements()
);
return self::create($type, ...$values);
}
/**
* Convenience method to initialize from attribute values.
*
* @param AttributeValue ...$values One or more values
*/
public static function fromAttributeValues(AttributeValue ...$values): self
{
// we need at least one value to determine OID
if (count($values) === 0) {
throw new LogicException('No values.');
}
$oid = reset($values)
->oid();
return self::create(AttributeType::create($oid), ...$values);
}
/**
* Get first value of the attribute.
*/
public function first(): AttributeValue
{
if (count($this->values) === 0) {
throw new LogicException('Attribute contains no values.');
}
return $this->values[0];
}
/**
* Get all values.
*
* @return AttributeValue[]
*/
public function values(): array
{
return $this->values;
}
/**
* Generate ASN.1 structure.
*/
public function toASN1(): Sequence
{
$values = array_map(static fn (AttributeValue $value) => $value->toASN1(), $this->values);
$valueset = Set::create(...$values);
return Sequence::create($this->type->toASN1(), $valueset->sortedSetOf());
}
/**
* Cast attribute values to another AttributeValue class.
*
* This method is generally used to cast UnknownAttributeValue values to specific objects when class is declared
* outside this package.
*
* The new class must be derived from AttributeValue and have the same OID as current attribute values.
*
* @param string $cls AttributeValue class name
*/
public function castValues(string $cls): self
{
// check that target class derives from AttributeValue
if (! is_subclass_of($cls, AttributeValue::class)) {
throw new LogicException(sprintf('%s must be derived from %s.', $cls, AttributeValue::class));
}
$values = array_map(
function (AttributeValue $value) use ($cls) {
/** @var AttributeValue $cls Class name as a string */
$value = $cls::fromSelf($value);
if ($value->oid() !== $this->oid()) {
throw new LogicException('Attribute OID mismatch.');
}
return $value;
},
$this->values
);
return self::fromAttributeValues(...$values);
}
/**
* @see \Countable::count()
*/
public function count(): int
{
return count($this->values);
}
/**
* @see \IteratorAggregate::getIterator()
*/
public function getIterator(): ArrayIterator
{
return new ArrayIterator($this->values);
}
/**
* Get attribute type.
*/
public function type(): AttributeType
{
return $this->type;
}
/**
* Get OID of the attribute.
*/
public function oid(): string
{
return $this->type->oid();
}
}

View File

@ -0,0 +1,525 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\ASN1;
use OutOfBoundsException;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Type\Primitive\ObjectIdentifier;
use SpomkyLabs\Pki\ASN1\Type\Primitive\PrintableString;
use SpomkyLabs\Pki\ASN1\Type\Primitive\UTF8String;
use SpomkyLabs\Pki\ASN1\Type\StringType;
use function array_key_exists;
/**
* Implements *AttributeType* ASN.1 type.
*
* @see https://www.itu.int/ITU-T/formal-language/itu-t/x/x501/2012/InformationFramework.html#InformationFramework.AttributeType
*/
final class AttributeType
{
// OID's from 2.5.4 arc
final public const OID_OBJECT_CLASS = '2.5.4.0';
final public const OID_ALIASED_ENTRY_NAME = '2.5.4.1';
final public const OID_KNOWLEDGE_INFORMATION = '2.5.4.2';
final public const OID_COMMON_NAME = '2.5.4.3';
final public const OID_SURNAME = '2.5.4.4';
final public const OID_SERIAL_NUMBER = '2.5.4.5';
final public const OID_COUNTRY_NAME = '2.5.4.6';
final public const OID_LOCALITY_NAME = '2.5.4.7';
final public const OID_STATE_OR_PROVINCE_NAME = '2.5.4.8';
final public const OID_STREET_ADDRESS = '2.5.4.9';
final public const OID_ORGANIZATION_NAME = '2.5.4.10';
final public const OID_ORGANIZATIONAL_UNIT_NAME = '2.5.4.11';
final public const OID_TITLE = '2.5.4.12';
final public const OID_DESCRIPTION = '2.5.4.13';
final public const OID_SEARCH_GUIDE = '2.5.4.14';
final public const OID_BUSINESS_CATEGORY = '2.5.4.15';
final public const OID_POSTAL_ADDRESS = '2.5.4.16';
final public const OID_POSTAL_CODE = '2.5.4.17';
final public const OID_POST_OFFICE_BOX = '2.5.4.18';
final public const OID_PHYSICAL_DELIVERY_OFFICE_NAME = '2.5.4.19';
final public const OID_TELEPHONE_NUMBER = '2.5.4.20';
final public const OID_TELEX_NUMBER = '2.5.4.21';
final public const OID_TELETEX_TERMINAL_IDENTIFIER = '2.5.4.22';
final public const OID_FACSIMILE_TELEPHONE_NUMBER = '2.5.4.23';
final public const OID_X121_ADDRESS = '2.5.4.24';
final public const OID_INTERNATIONAL_ISDN_NUMBER = '2.5.4.25';
final public const OID_REGISTERED_ADDRESS = '2.5.4.26';
final public const OID_DESTINATION_INDICATOR = '2.5.4.27';
final public const OID_PREFERRED_DELIVERY_METHOD = '2.5.4.28';
final public const OID_PRESENTATION_ADDRESS = '2.5.4.29';
final public const OID_SUPPORTED_APPLICATION_CONTEXT = '2.5.4.30';
final public const OID_MEMBER = '2.5.4.31';
final public const OID_OWNER = '2.5.4.32';
final public const OID_ROLE_OCCUPANT = '2.5.4.33';
final public const OID_SEE_ALSO = '2.5.4.34';
final public const OID_USER_PASSWORD = '2.5.4.35';
final public const OID_USER_CERTIFICATE = '2.5.4.36';
final public const OID_CA_CERTIFICATE = '2.5.4.37';
final public const OID_AUTHORITY_REVOCATION_LIST = '2.5.4.38';
final public const OID_CERTIFICATE_REVOCATION_LIST = '2.5.4.39';
final public const OID_CROSS_CERTIFICATE_PAIR = '2.5.4.40';
final public const OID_NAME = '2.5.4.41';
final public const OID_GIVEN_NAME = '2.5.4.42';
final public const OID_INITIALS = '2.5.4.43';
final public const OID_GENERATION_QUALIFIER = '2.5.4.44';
final public const OID_UNIQUE_IDENTIFIER = '2.5.4.45';
final public const OID_DN_QUALIFIER = '2.5.4.46';
final public const OID_ENHANCED_SEARCH_GUIDE = '2.5.4.47';
final public const OID_PROTOCOL_INFORMATION = '2.5.4.48';
final public const OID_DISTINGUISHED_NAME = '2.5.4.49';
final public const OID_UNIQUE_MEMBER = '2.5.4.50';
final public const OID_HOUSE_IDENTIFIER = '2.5.4.51';
final public const OID_SUPPORTED_ALGORITHMS = '2.5.4.52';
final public const OID_DELTA_REVOCATION_LIST = '2.5.4.53';
final public const OID_DMD_NAME = '2.5.4.54';
final public const OID_CLEARANCE = '2.5.4.55';
final public const OID_DEFAULT_DIR_QOP = '2.5.4.56';
final public const OID_ATTRIBUTE_INTEGRITY_INFO = '2.5.4.57';
final public const OID_ATTRIBUTE_CERTIFICATE = '2.5.4.58';
final public const OID_ATTRIBUTE_CERTIFICATE_REVOCATION_LIST = '2.5.4.59';
final public const OID_CONF_KEY_INFO = '2.5.4.60';
final public const OID_AA_CERTIFICATE = '2.5.4.61';
final public const OID_ATTRIBUTE_DESCRIPTOR_CERTIFICATE = '2.5.4.62';
final public const OID_ATTRIBUTE_AUTHORITY_REVOCATION_LIST = '2.5.4.63';
final public const OID_FAMILY_INFORMATION = '2.5.4.64';
final public const OID_PSEUDONYM = '2.5.4.65';
final public const OID_COMMUNICATIONS_SERVICE = '2.5.4.66';
final public const OID_COMMUNICATIONS_NETWORK = '2.5.4.67';
final public const OID_CERTIFICATION_PRACTICE_STMT = '2.5.4.68';
final public const OID_CERTIFICATE_POLICY = '2.5.4.69';
final public const OID_PKI_PATH = '2.5.4.70';
final public const OID_PRIV_POLICY = '2.5.4.71';
final public const OID_ROLE = '2.5.4.72';
final public const OID_DELEGATION_PATH = '2.5.4.73';
final public const OID_PROT_PRIV_POLICY = '2.5.4.74';
final public const OID_XML_PRIVILEGE_INFO = '2.5.4.75';
final public const OID_XML_PRIV_POLICY = '2.5.4.76';
final public const OID_UUID_PAIR = '2.5.4.77';
final public const OID_TAG_OID = '2.5.4.78';
final public const OID_UII_FORMAT = '2.5.4.79';
final public const OID_UII_IN_URH = '2.5.4.80';
final public const OID_CONTENT_URL = '2.5.4.81';
final public const OID_PERMISSION = '2.5.4.82';
final public const OID_URI = '2.5.4.83';
final public const OID_PWD_ATTRIBUTE = '2.5.4.84';
final public const OID_USER_PWD = '2.5.4.85';
final public const OID_URN = '2.5.4.86';
final public const OID_URL = '2.5.4.87';
final public const OID_UTM_COORDINATES = '2.5.4.88';
final public const OID_URNC = '2.5.4.89';
final public const OID_UII = '2.5.4.90';
final public const OID_EPC = '2.5.4.91';
final public const OID_TAG_AFI = '2.5.4.92';
final public const OID_EPC_FORMAT = '2.5.4.93';
final public const OID_EPC_IN_URN = '2.5.4.94';
final public const OID_LDAP_URL = '2.5.4.95';
final public const OID_TAG_LOCATION = '2.5.4.96';
final public const OID_ORGANIZATION_IDENTIFIER = '2.5.4.97';
// Miscellany attribute OID's
final public const OID_CLEARANCE_X501 = '2.5.1.5.55';
/**
* Default ASN.1 string types for attributes.
*
* Attributes not mapped here shall use UTF8String as a default type.
*
* @internal
*
* @var array<string, int>
*/
private const MAP_ATTR_TO_STR_TYPE = [
self::OID_DN_QUALIFIER => Element::TYPE_PRINTABLE_STRING,
self::OID_COUNTRY_NAME => Element::TYPE_PRINTABLE_STRING,
self::OID_SERIAL_NUMBER => Element::TYPE_PRINTABLE_STRING,
];
/**
* OID to attribute names mapping.
*
* First name is the primary name. If there's more than one name, others may be used as an alias.
*
* Generated using ldap-attribs.py.
*
* @internal
*
* @var array<string, array<string>>
*/
private const MAP_OID_TO_NAME = [
'0.9.2342.19200300.100.1.1' => ['uid', 'userid'],
'0.9.2342.19200300.100.1.2' => ['textEncodedORAddress'],
'0.9.2342.19200300.100.1.3' => ['mail', 'rfc822Mailbox'],
'0.9.2342.19200300.100.1.4' => ['info'],
'0.9.2342.19200300.100.1.5' => ['drink', 'favouriteDrink'],
'0.9.2342.19200300.100.1.6' => ['roomNumber'],
'0.9.2342.19200300.100.1.7' => ['photo'],
'0.9.2342.19200300.100.1.8' => ['userClass'],
'0.9.2342.19200300.100.1.9' => ['host'],
'0.9.2342.19200300.100.1.10' => ['manager'],
'0.9.2342.19200300.100.1.11' => ['documentIdentifier'],
'0.9.2342.19200300.100.1.12' => ['documentTitle'],
'0.9.2342.19200300.100.1.13' => ['documentVersion'],
'0.9.2342.19200300.100.1.14' => ['documentAuthor'],
'0.9.2342.19200300.100.1.15' => ['documentLocation'],
'0.9.2342.19200300.100.1.20' => ['homePhone', 'homeTelephoneNumber'],
'0.9.2342.19200300.100.1.21' => ['secretary'],
'0.9.2342.19200300.100.1.22' => ['otherMailbox'],
'0.9.2342.19200300.100.1.25' => ['dc', 'domainComponent'],
'0.9.2342.19200300.100.1.26' => ['aRecord'],
'0.9.2342.19200300.100.1.27' => ['mDRecord'],
'0.9.2342.19200300.100.1.28' => ['mXRecord'],
'0.9.2342.19200300.100.1.29' => ['nSRecord'],
'0.9.2342.19200300.100.1.30' => ['sOARecord'],
'0.9.2342.19200300.100.1.31' => ['cNAMERecord'],
'0.9.2342.19200300.100.1.37' => ['associatedDomain'],
'0.9.2342.19200300.100.1.38' => ['associatedName'],
'0.9.2342.19200300.100.1.39' => ['homePostalAddress'],
'0.9.2342.19200300.100.1.40' => ['personalTitle'],
'0.9.2342.19200300.100.1.41' => ['mobile', 'mobileTelephoneNumber'],
'0.9.2342.19200300.100.1.42' => ['pager', 'pagerTelephoneNumber'],
'0.9.2342.19200300.100.1.43' => ['co', 'friendlyCountryName'],
'0.9.2342.19200300.100.1.44' => ['uniqueIdentifier'],
'0.9.2342.19200300.100.1.45' => ['organizationalStatus'],
'0.9.2342.19200300.100.1.46' => ['janetMailbox'],
'0.9.2342.19200300.100.1.47' => ['mailPreferenceOption'],
'0.9.2342.19200300.100.1.48' => ['buildingName'],
'0.9.2342.19200300.100.1.49' => ['dSAQuality'],
'0.9.2342.19200300.100.1.50' => ['singleLevelQuality'],
'0.9.2342.19200300.100.1.51' => ['subtreeMinimumQuality'],
'0.9.2342.19200300.100.1.52' => ['subtreeMaximumQuality'],
'0.9.2342.19200300.100.1.53' => ['personalSignature'],
'0.9.2342.19200300.100.1.54' => ['dITRedirect'],
'0.9.2342.19200300.100.1.55' => ['audio'],
'0.9.2342.19200300.100.1.56' => ['documentPublisher'],
'0.9.2342.19200300.100.1.60' => ['jpegPhoto'],
'1.2.840.113549.1.9.1' => ['email', 'emailAddress', 'pkcs9email'],
'1.2.840.113556.1.2.102' => ['memberOf'],
'1.3.6.1.1.1.1.0' => ['uidNumber'],
'1.3.6.1.1.1.1.1' => ['gidNumber'],
'1.3.6.1.1.1.1.2' => ['gecos'],
'1.3.6.1.1.1.1.3' => ['homeDirectory'],
'1.3.6.1.1.1.1.4' => ['loginShell'],
'1.3.6.1.1.1.1.5' => ['shadowLastChange'],
'1.3.6.1.1.1.1.6' => ['shadowMin'],
'1.3.6.1.1.1.1.7' => ['shadowMax'],
'1.3.6.1.1.1.1.8' => ['shadowWarning'],
'1.3.6.1.1.1.1.9' => ['shadowInactive'],
'1.3.6.1.1.1.1.10' => ['shadowExpire'],
'1.3.6.1.1.1.1.11' => ['shadowFlag'],
'1.3.6.1.1.1.1.12' => ['memberUid'],
'1.3.6.1.1.1.1.13' => ['memberNisNetgroup'],
'1.3.6.1.1.1.1.14' => ['nisNetgroupTriple'],
'1.3.6.1.1.1.1.15' => ['ipServicePort'],
'1.3.6.1.1.1.1.16' => ['ipServiceProtocol'],
'1.3.6.1.1.1.1.17' => ['ipProtocolNumber'],
'1.3.6.1.1.1.1.18' => ['oncRpcNumber'],
'1.3.6.1.1.1.1.19' => ['ipHostNumber'],
'1.3.6.1.1.1.1.20' => ['ipNetworkNumber'],
'1.3.6.1.1.1.1.21' => ['ipNetmaskNumber'],
'1.3.6.1.1.1.1.22' => ['macAddress'],
'1.3.6.1.1.1.1.23' => ['bootParameter'],
'1.3.6.1.1.1.1.24' => ['bootFile'],
'1.3.6.1.1.1.1.26' => ['nisMapName'],
'1.3.6.1.1.1.1.27' => ['nisMapEntry'],
'1.3.6.1.1.4' => ['vendorName'],
'1.3.6.1.1.5' => ['vendorVersion'],
'1.3.6.1.1.16.4' => ['entryUUID'],
'1.3.6.1.1.20' => ['entryDN'],
'2.5.4.0' => ['objectClass'],
'2.5.4.1' => ['aliasedObjectName', 'aliasedEntryName'],
'2.5.4.2' => ['knowledgeInformation'],
'2.5.4.3' => ['cn', 'commonName'],
'2.5.4.4' => ['sn', 'surname'],
'2.5.4.5' => ['serialNumber'],
'2.5.4.6' => ['c', 'countryName'],
'2.5.4.7' => ['l', 'localityName'],
'2.5.4.8' => ['st', 'stateOrProvinceName'],
'2.5.4.9' => ['street', 'streetAddress'],
'2.5.4.10' => ['o', 'organizationName'],
'2.5.4.11' => ['ou', 'organizationalUnitName'],
'2.5.4.12' => ['title'],
'2.5.4.13' => ['description'],
'2.5.4.14' => ['searchGuide'],
'2.5.4.15' => ['businessCategory'],
'2.5.4.16' => ['postalAddress'],
'2.5.4.17' => ['postalCode'],
'2.5.4.18' => ['postOfficeBox'],
'2.5.4.19' => ['physicalDeliveryOfficeName'],
'2.5.4.20' => ['telephoneNumber'],
'2.5.4.21' => ['telexNumber'],
'2.5.4.22' => ['teletexTerminalIdentifier'],
'2.5.4.23' => ['facsimileTelephoneNumber', 'fax'],
'2.5.4.24' => ['x121Address'],
'2.5.4.25' => ['internationaliSDNNumber'],
'2.5.4.26' => ['registeredAddress'],
'2.5.4.27' => ['destinationIndicator'],
'2.5.4.28' => ['preferredDeliveryMethod'],
'2.5.4.29' => ['presentationAddress'],
'2.5.4.30' => ['supportedApplicationContext'],
'2.5.4.31' => ['member'],
'2.5.4.32' => ['owner'],
'2.5.4.33' => ['roleOccupant'],
'2.5.4.34' => ['seeAlso'],
'2.5.4.35' => ['userPassword'],
'2.5.4.36' => ['userCertificate'],
'2.5.4.37' => ['cACertificate'],
'2.5.4.38' => ['authorityRevocationList'],
'2.5.4.39' => ['certificateRevocationList'],
'2.5.4.40' => ['crossCertificatePair'],
'2.5.4.41' => ['name'],
'2.5.4.42' => ['givenName', 'gn'],
'2.5.4.43' => ['initials'],
'2.5.4.44' => ['generationQualifier'],
'2.5.4.45' => ['x500UniqueIdentifier'],
'2.5.4.46' => ['dnQualifier'],
'2.5.4.47' => ['enhancedSearchGuide'],
'2.5.4.48' => ['protocolInformation'],
'2.5.4.49' => ['distinguishedName'],
'2.5.4.50' => ['uniqueMember'],
'2.5.4.51' => ['houseIdentifier'],
'2.5.4.52' => ['supportedAlgorithms'],
'2.5.4.53' => ['deltaRevocationList'],
'2.5.4.54' => ['dmdName'],
'2.5.4.65' => ['pseudonym'],
'2.5.18.1' => ['createTimestamp'],
'2.5.18.2' => ['modifyTimestamp'],
'2.5.18.3' => ['creatorsName'],
'2.5.18.4' => ['modifiersName'],
'2.5.18.5' => ['administrativeRole'],
'2.5.18.6' => ['subtreeSpecification'],
'2.5.18.9' => ['hasSubordinates'],
'2.5.18.10' => ['subschemaSubentry'],
'2.5.21.1' => ['dITStructureRules'],
'2.5.21.2' => ['dITContentRules'],
'2.5.21.4' => ['matchingRules'],
'2.5.21.5' => ['attributeTypes'],
'2.5.21.6' => ['objectClasses'],
'2.5.21.7' => ['nameForms'],
'2.5.21.8' => ['matchingRuleUse'],
'2.5.21.9' => ['structuralObjectClass'],
'2.16.840.1.113730.3.1.1' => ['carLicense'],
'2.16.840.1.113730.3.1.2' => ['departmentNumber'],
'2.16.840.1.113730.3.1.3' => ['employeeNumber'],
'2.16.840.1.113730.3.1.4' => ['employeeType'],
'2.16.840.1.113730.3.1.34' => ['ref'],
'2.16.840.1.113730.3.1.39' => ['preferredLanguage'],
'2.16.840.1.113730.3.1.40' => ['userSMIMECertificate'],
'2.16.840.1.113730.3.1.216' => ['userPKCS12'],
'2.16.840.1.113730.3.1.241' => ['displayName'],
];
/**
* @param string $_oid OID in dotted format
*/
private function __construct(
protected string $_oid
) {
}
public static function create(string $oid): self
{
return new self($oid);
}
/**
* Initialize from ASN.1.
*/
public static function fromASN1(ObjectIdentifier $oi): self
{
return self::create($oi->oid());
}
/**
* Initialize from attribute name.
*/
public static function fromName(string $name): self
{
$oid = self::attrNameToOID($name);
return self::create($oid);
}
/**
* Get OID of the attribute.
*
* @return string OID in dotted format
*/
public function oid(): string
{
return $this->_oid;
}
/**
* Get name of the attribute.
*/
public function typeName(): string
{
if (array_key_exists($this->_oid, self::MAP_OID_TO_NAME)) {
return self::MAP_OID_TO_NAME[$this->_oid][0];
}
return $this->_oid;
}
/**
* Generate ASN.1 element.
*/
public function toASN1(): ObjectIdentifier
{
return ObjectIdentifier::create($this->_oid);
}
/**
* Convert attribute name to OID.
*
* @param string $name Primary attribute name or an alias
*
* @return string OID in dotted format
*/
public static function attrNameToOID(string $name): string
{
// if already in OID form
if (preg_match('/^[0-9]+(?:\.[0-9]+)*$/', $name) === 1) {
return $name;
}
$map = self::_oidReverseMap();
$k = mb_strtolower($name, '8bit');
if (! isset($map[$k])) {
throw new OutOfBoundsException("No OID for {$name}.");
}
return $map[$k];
}
/**
* Get ASN.1 string for given attribute type.
*
* @param string $oid Attribute OID
* @param string $str String
*/
public static function asn1StringForType(string $oid, string $str): StringType
{
if (! array_key_exists($oid, self::MAP_ATTR_TO_STR_TYPE)) {
return UTF8String::create($str);
}
return PrintableString::create($str);
}
/**
* Get name to OID lookup map.
*
* @return array<string>
*/
private static function _oidReverseMap(): array
{
static $map;
if (! isset($map)) {
$map = [];
// for each attribute type
foreach (self::MAP_OID_TO_NAME as $oid => $names) {
// for primary name and aliases
foreach ($names as $name) {
$map[mb_strtolower($name, '8bit')] = $oid;
}
}
}
return $map;
}
}

View File

@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\ASN1;
use SpomkyLabs\Pki\ASN1\Type\Constructed\Sequence;
use SpomkyLabs\Pki\X501\ASN1\AttributeValue\AttributeValue;
use Stringable;
/**
* Implements *AttributeTypeAndValue* ASN.1 type.
*
* @see https://www.itu.int/ITU-T/formal-language/itu-t/x/x501/2012/InformationFramework.html#InformationFramework.AttributeTypeAndValue
*/
final class AttributeTypeAndValue implements Stringable
{
/**
* @param AttributeType $type Attribute type
* @param AttributeValue $value Attribute value
*/
private function __construct(
private readonly AttributeType $type,
private readonly AttributeValue $value
) {
}
public function __toString(): string
{
return $this->toString();
}
public static function create(AttributeType $type, AttributeValue $value): self
{
return new self($type, $value);
}
/**
* Initialize from ASN.1.
*/
public static function fromASN1(Sequence $seq): self
{
$type = AttributeType::fromASN1($seq->at(0)->asObjectIdentifier());
$value = AttributeValue::fromASN1ByOID($type->oid(), $seq->at(1));
return self::create($type, $value);
}
/**
* Convenience method to initialize from attribute value.
*
* @param AttributeValue $value Attribute value
*/
public static function fromAttributeValue(AttributeValue $value): self
{
return self::create(AttributeType::create($value->oid()), $value);
}
/**
* Get attribute value.
*/
public function value(): AttributeValue
{
return $this->value;
}
/**
* Generate ASN.1 structure.
*/
public function toASN1(): Sequence
{
return Sequence::create($this->type->toASN1(), $this->value->toASN1());
}
/**
* Get attributeTypeAndValue string conforming to RFC 2253.
*
* @see https://tools.ietf.org/html/rfc2253#section-2.3
*/
public function toString(): string
{
return $this->type->typeName() . '=' . $this->value->rfc2253String();
}
/**
* Check whether attribute is semantically equal to other.
*
* @param AttributeTypeAndValue $other Object to compare to
*/
public function equals(self $other): bool
{
// check that attribute types match
if ($this->oid() !== $other->oid()) {
return false;
}
$matcher = $this->value->equalityMatchingRule();
return $matcher->compare($this->value->stringValue(), $other->value->stringValue()) === true;
}
/**
* Get attribute type.
*/
public function type(): AttributeType
{
return $this->type;
}
/**
* Get OID of the attribute.
*/
public function oid(): string
{
return $this->type->oid();
}
}

View File

@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\ASN1\AttributeValue;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
use SpomkyLabs\Pki\X501\ASN1\Attribute;
use SpomkyLabs\Pki\X501\ASN1\AttributeType;
use SpomkyLabs\Pki\X501\ASN1\AttributeTypeAndValue;
use SpomkyLabs\Pki\X501\MatchingRule\MatchingRule;
use Stringable;
use function array_key_exists;
/**
* Base class for attribute values.
*
* @see https://www.itu.int/ITU-T/formal-language/itu-t/x/x501/2012/InformationFramework.html#InformationFramework.AttributeValue
*/
abstract class AttributeValue implements Stringable
{
/**
* Mapping from attribute type OID to attribute value class name.
*
* @internal
*
* @var array<string, string>
*/
private const MAP_OID_TO_CLASS = [
AttributeType::OID_COMMON_NAME => CommonNameValue::class,
AttributeType::OID_SURNAME => SurnameValue::class,
AttributeType::OID_SERIAL_NUMBER => SerialNumberValue::class,
AttributeType::OID_COUNTRY_NAME => CountryNameValue::class,
AttributeType::OID_LOCALITY_NAME => LocalityNameValue::class,
AttributeType::OID_STATE_OR_PROVINCE_NAME => StateOrProvinceNameValue::class,
AttributeType::OID_ORGANIZATION_NAME => OrganizationNameValue::class,
AttributeType::OID_ORGANIZATIONAL_UNIT_NAME => OrganizationalUnitNameValue::class,
AttributeType::OID_TITLE => TitleValue::class,
AttributeType::OID_DESCRIPTION => DescriptionValue::class,
AttributeType::OID_NAME => NameValue::class,
AttributeType::OID_GIVEN_NAME => GivenNameValue::class,
AttributeType::OID_PSEUDONYM => PseudonymValue::class,
];
/**
* @param string $oid OID of the attribute type.
*/
protected function __construct(
protected string $oid
) {
}
/**
* Get attribute value as an UTF-8 encoded string.
*/
public function __toString(): string
{
return $this->_transcodedString();
}
/**
* Generate ASN.1 element.
*/
abstract public function toASN1(): Element;
/**
* Get attribute value as a string.
*/
abstract public function stringValue(): string;
/**
* Get matching rule for equality comparison.
*/
abstract public function equalityMatchingRule(): MatchingRule;
/**
* Get attribute value as a string conforming to RFC 2253.
*
* @see https://tools.ietf.org/html/rfc2253#section-2.4
*/
abstract public function rfc2253String(): string;
/**
* Initialize from ASN.1.
*/
abstract public static function fromASN1(UnspecifiedType $el): self;
/**
* Initialize from ASN.1 with given OID hint.
*
* @param string $oid Attribute's OID
*/
public static function fromASN1ByOID(string $oid, UnspecifiedType $el): self
{
if (! array_key_exists($oid, self::MAP_OID_TO_CLASS)) {
return new UnknownAttributeValue($oid, $el->asElement());
}
$cls = self::MAP_OID_TO_CLASS[$oid];
return $cls::fromASN1($el);
}
/**
* Initialize from another AttributeValue.
*
* This method is generally used to cast UnknownAttributeValue to specific object when class is declared outside
* this package.
*
* @param self $obj Instance of AttributeValue
*/
public static function fromSelf(self $obj): self
{
return static::fromASN1($obj->toASN1()->asUnspecified());
}
/**
* Get attribute type's OID.
*/
public function oid(): string
{
return $this->oid;
}
/**
* Get Attribute object with this as a single value.
*/
public function toAttribute(): Attribute
{
return Attribute::fromAttributeValues($this);
}
/**
* Get AttributeTypeAndValue object with this as a value.
*/
public function toAttributeTypeAndValue(): AttributeTypeAndValue
{
return AttributeTypeAndValue::fromAttributeValue($this);
}
/**
* Get attribute value as an UTF-8 string conforming to RFC 4518.
*
* @see https://tools.ietf.org/html/rfc4518#section-2.1
*/
abstract protected function _transcodedString(): string;
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\ASN1\AttributeValue;
use SpomkyLabs\Pki\X501\ASN1\AttributeType;
use SpomkyLabs\Pki\X501\ASN1\AttributeValue\Feature\DirectoryString;
/**
* 'commonName' attribute value.
*
* @see https://www.itu.int/ITU-T/formal-language/itu-t/x/x520/2012/SelectedAttributeTypes.html#SelectedAttributeTypes.commonName
*/
final class CommonNameValue extends DirectoryString
{
public static function create(string $value, int $string_tag = DirectoryString::UTF8): static
{
return new static(AttributeType::OID_COMMON_NAME, $value, $string_tag);
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\ASN1\AttributeValue;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
use SpomkyLabs\Pki\X501\ASN1\AttributeType;
use SpomkyLabs\Pki\X501\ASN1\AttributeValue\Feature\PrintableStringValue;
/**
* 'countryName' attribute value.
*
* @see https://www.itu.int/ITU-T/formal-language/itu-t/x/x520/2012/SelectedAttributeTypes.html#SelectedAttributeTypes.countryName
*/
final class CountryNameValue extends PrintableStringValue
{
/**
* @param string $value String value
*/
protected function __construct(string $value)
{
parent::__construct(AttributeType::OID_COUNTRY_NAME, $value);
}
public static function create(string $value): self
{
return new self($value);
}
public static function fromASN1(UnspecifiedType $el): self
{
return self::create($el->asPrintableString()->string());
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\ASN1\AttributeValue;
use SpomkyLabs\Pki\X501\ASN1\AttributeType;
use SpomkyLabs\Pki\X501\ASN1\AttributeValue\Feature\DirectoryString;
/**
* 'description' attribute value.
*
* @see https://www.itu.int/ITU-T/formal-language/itu-t/x/x520/2012/SelectedAttributeTypes.html#SelectedAttributeTypes.description
*/
final class DescriptionValue extends DirectoryString
{
public static function create(string $value, int $string_tag = DirectoryString::UTF8): static
{
return new static(AttributeType::OID_DESCRIPTION, $value, $string_tag);
}
}

View File

@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\ASN1\AttributeValue\Feature;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Type\Primitive\BMPString;
use SpomkyLabs\Pki\ASN1\Type\Primitive\PrintableString;
use SpomkyLabs\Pki\ASN1\Type\Primitive\T61String;
use SpomkyLabs\Pki\ASN1\Type\Primitive\UniversalString;
use SpomkyLabs\Pki\ASN1\Type\Primitive\UTF8String;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
use SpomkyLabs\Pki\X501\ASN1\AttributeValue\AttributeValue;
use SpomkyLabs\Pki\X501\DN\DNParser;
use SpomkyLabs\Pki\X501\MatchingRule\CaseIgnoreMatch;
use SpomkyLabs\Pki\X501\MatchingRule\MatchingRule;
use SpomkyLabs\Pki\X501\StringPrep\TranscodeStep;
use UnexpectedValueException;
use function array_key_exists;
/**
* Base class for attribute values having *(Unbounded)DirectoryString* as a syntax.
*
* @see https://www.itu.int/ITU-T/formal-language/itu-t/x/x520/2012/SelectedAttributeTypes.html#SelectedAttributeTypes.UnboundedDirectoryString
*/
abstract class DirectoryString extends AttributeValue
{
/**
* Teletex string syntax.
*
* @var int
*/
final public const TELETEX = Element::TYPE_T61_STRING;
/**
* Printable string syntax.
*
* @var int
*/
final public const PRINTABLE = Element::TYPE_PRINTABLE_STRING;
/**
* BMP string syntax.
*
* @var int
*/
final public const BMP = Element::TYPE_BMP_STRING;
/**
* Universal string syntax.
*
* @var int
*/
final public const UNIVERSAL = Element::TYPE_UNIVERSAL_STRING;
/**
* UTF-8 string syntax.
*
* @var int
*/
final public const UTF8 = Element::TYPE_UTF8_STRING;
/**
* Mapping from syntax enumeration to ASN.1 class name.
*
* @internal
*
* @var array<int, string>
*/
private const MAP_TAG_TO_CLASS = [
self::TELETEX => T61String::class,
self::PRINTABLE => PrintableString::class,
self::UNIVERSAL => UniversalString::class,
self::UTF8 => UTF8String::class,
self::BMP => BMPString::class,
];
/**
* @param string $_string String value
* @param int $_stringTag Syntax choice
*/
final protected function __construct(
string $oid,
protected string $_string,
protected int $_stringTag
) {
parent::__construct($oid);
}
abstract public static function create(string $value, int $string_tag = self::UTF8): static;
/**
* @return self
*/
public static function fromASN1(UnspecifiedType $el): AttributeValue
{
$tag = $el->tag();
// validate tag
self::_tagToASN1Class($tag);
return static::create($el->asString()->string(), $tag);
}
public function toASN1(): Element
{
$cls = self::_tagToASN1Class($this->_stringTag);
return $cls::create($this->_string);
}
public function stringValue(): string
{
return $this->_string;
}
public function equalityMatchingRule(): MatchingRule
{
return CaseIgnoreMatch::create($this->_stringTag);
}
public function rfc2253String(): string
{
// TeletexString is encoded as binary
if ($this->_stringTag === self::TELETEX) {
return $this->_transcodedString();
}
return DNParser::escapeString($this->_transcodedString());
}
protected function _transcodedString(): string
{
return TranscodeStep::create($this->_stringTag)
->apply($this->_string)
;
}
/**
* Get ASN.1 class name for given DirectoryString type tag.
*/
private static function _tagToASN1Class(int $tag): string
{
if (! array_key_exists($tag, self::MAP_TAG_TO_CLASS)) {
throw new UnexpectedValueException(
sprintf('Type %s is not valid DirectoryString.', Element::tagToName($tag))
);
}
return self::MAP_TAG_TO_CLASS[$tag];
}
}

View File

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\ASN1\AttributeValue\Feature;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Type\Primitive\PrintableString;
use SpomkyLabs\Pki\X501\ASN1\AttributeValue\AttributeValue;
use SpomkyLabs\Pki\X501\DN\DNParser;
use SpomkyLabs\Pki\X501\MatchingRule\CaseIgnoreMatch;
use SpomkyLabs\Pki\X501\MatchingRule\MatchingRule;
/**
* Base class for attribute values having *PrintableString* syntax.
*/
abstract class PrintableStringValue extends AttributeValue
{
/**
* @param string $_string String value
*/
protected function __construct(
string $oid,
protected string $_string
) {
parent::__construct($oid);
}
public function toASN1(): Element
{
return PrintableString::create($this->_string);
}
public function stringValue(): string
{
return $this->_string;
}
public function equalityMatchingRule(): MatchingRule
{
// default to caseIgnoreMatch
return CaseIgnoreMatch::create(Element::TYPE_PRINTABLE_STRING);
}
public function rfc2253String(): string
{
return DNParser::escapeString($this->_transcodedString());
}
protected function _transcodedString(): string
{
// PrintableString maps directly to UTF-8
return $this->_string;
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\ASN1\AttributeValue;
use SpomkyLabs\Pki\X501\ASN1\AttributeType;
use SpomkyLabs\Pki\X501\ASN1\AttributeValue\Feature\DirectoryString;
/**
* 'givenName' attribute value.
*
* @see https://www.itu.int/ITU-T/formal-language/itu-t/x/x520/2012/SelectedAttributeTypes.html#SelectedAttributeTypes.givenName
*/
final class GivenNameValue extends DirectoryString
{
public static function create(string $value, int $string_tag = DirectoryString::UTF8): static
{
return new static(AttributeType::OID_GIVEN_NAME, $value, $string_tag);
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\ASN1\AttributeValue;
use SpomkyLabs\Pki\X501\ASN1\AttributeType;
use SpomkyLabs\Pki\X501\ASN1\AttributeValue\Feature\DirectoryString;
/**
* 'localityName' attribute value.
*
* @see https://www.itu.int/ITU-T/formal-language/itu-t/x/x520/2012/SelectedAttributeTypes.html#SelectedAttributeTypes.localityName
*/
final class LocalityNameValue extends DirectoryString
{
public static function create(string $value, int $string_tag = DirectoryString::UTF8): static
{
return new static(AttributeType::OID_LOCALITY_NAME, $value, $string_tag);
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\ASN1\AttributeValue;
use SpomkyLabs\Pki\X501\ASN1\AttributeType;
use SpomkyLabs\Pki\X501\ASN1\AttributeValue\Feature\DirectoryString;
/**
* 'name' attribute value.
*
* @see https://www.itu.int/ITU-T/formal-language/itu-t/x/x520/2012/SelectedAttributeTypes.html#SelectedAttributeTypes.name
*/
final class NameValue extends DirectoryString
{
public static function create(string $value, int $string_tag = DirectoryString::UTF8): static
{
return new static(AttributeType::OID_NAME, $value, $string_tag);
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\ASN1\AttributeValue;
use SpomkyLabs\Pki\X501\ASN1\AttributeType;
use SpomkyLabs\Pki\X501\ASN1\AttributeValue\Feature\DirectoryString;
/**
* 'organizationName' attribute value.
*
* @see https://www.itu.int/ITU-T/formal-language/itu-t/x/x520/2012/SelectedAttributeTypes.html#SelectedAttributeTypes.organizationName
*/
final class OrganizationNameValue extends DirectoryString
{
public static function create(string $value, int $string_tag = DirectoryString::UTF8): static
{
return new static(AttributeType::OID_ORGANIZATION_NAME, $value, $string_tag);
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\ASN1\AttributeValue;
use SpomkyLabs\Pki\X501\ASN1\AttributeType;
use SpomkyLabs\Pki\X501\ASN1\AttributeValue\Feature\DirectoryString;
/**
* 'organizationalUnitName' attribute value.
*
* @see https://www.itu.int/ITU-T/formal-language/itu-t/x/x520/2012/SelectedAttributeTypes.html#SelectedAttributeTypes.organizationalUnitName
*/
final class OrganizationalUnitNameValue extends DirectoryString
{
public static function create(string $value, int $string_tag = DirectoryString::UTF8): static
{
return new static(AttributeType::OID_ORGANIZATIONAL_UNIT_NAME, $value, $string_tag);
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\ASN1\AttributeValue;
use SpomkyLabs\Pki\X501\ASN1\AttributeType;
use SpomkyLabs\Pki\X501\ASN1\AttributeValue\Feature\DirectoryString;
/**
* 'pseudonym' attribute value.
*
* @see https://www.itu.int/ITU-T/formal-language/itu-t/x/x520/2012/SelectedAttributeTypes.html#SelectedAttributeTypes.pseudonym
*/
final class PseudonymValue extends DirectoryString
{
public static function create(string $value, int $string_tag = DirectoryString::UTF8): static
{
return new static(AttributeType::OID_PSEUDONYM, $value, $string_tag);
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\ASN1\AttributeValue;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
use SpomkyLabs\Pki\X501\ASN1\AttributeType;
use SpomkyLabs\Pki\X501\ASN1\AttributeValue\Feature\PrintableStringValue;
/**
* 'serialNumber' attribute value.
*
* @see https://www.itu.int/ITU-T/formal-language/itu-t/x/x520/2012/SelectedAttributeTypes.html#SelectedAttributeTypes.serialNumber
*/
final class SerialNumberValue extends PrintableStringValue
{
/**
* @param string $value String value
*/
protected function __construct(string $value)
{
parent::__construct(AttributeType::OID_SERIAL_NUMBER, $value);
}
public static function create(string $value): self
{
return new self($value);
}
public static function fromASN1(UnspecifiedType $el): self
{
return self::create($el->asPrintableString()->string());
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\ASN1\AttributeValue;
use SpomkyLabs\Pki\X501\ASN1\AttributeType;
use SpomkyLabs\Pki\X501\ASN1\AttributeValue\Feature\DirectoryString;
/**
* 'stateOrProvinceName' attribute value.
*
* @see https://www.itu.int/ITU-T/formal-language/itu-t/x/x520/2012/SelectedAttributeTypes.html#SelectedAttributeTypes.stateOrProvinceName
*/
final class StateOrProvinceNameValue extends DirectoryString
{
public static function create(string $value, int $string_tag = DirectoryString::UTF8): static
{
return new static(AttributeType::OID_STATE_OR_PROVINCE_NAME, $value, $string_tag);
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\ASN1\AttributeValue;
use SpomkyLabs\Pki\X501\ASN1\AttributeType;
use SpomkyLabs\Pki\X501\ASN1\AttributeValue\Feature\DirectoryString;
/**
* 'surname' attribute value.
*
* @see https://www.itu.int/ITU-T/formal-language/itu-t/x/x520/2012/SelectedAttributeTypes.html#SelectedAttributeTypes.surname
*/
final class SurnameValue extends DirectoryString
{
public static function create(string $value, int $string_tag = DirectoryString::UTF8): static
{
return new static(AttributeType::OID_SURNAME, $value, $string_tag);
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\ASN1\AttributeValue;
use SpomkyLabs\Pki\X501\ASN1\AttributeType;
use SpomkyLabs\Pki\X501\ASN1\AttributeValue\Feature\DirectoryString;
/**
* 'title' attribute value.
*
* @see https://www.itu.int/ITU-T/formal-language/itu-t/x/x520/2012/SelectedAttributeTypes.html#SelectedAttributeTypes.title
*/
final class TitleValue extends DirectoryString
{
public static function create(string $value, int $string_tag = DirectoryString::UTF8): static
{
return new static(AttributeType::OID_TITLE, $value, $string_tag);
}
}

View File

@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\ASN1\AttributeValue;
use BadMethodCallException;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
use SpomkyLabs\Pki\X501\DN\DNParser;
use SpomkyLabs\Pki\X501\MatchingRule\BinaryMatch;
use SpomkyLabs\Pki\X501\MatchingRule\MatchingRule;
use SpomkyLabs\Pki\X501\StringPrep\TranscodeStep;
/**
* Class to hold ASN.1 structure of an unimplemented attribute value.
*/
final class UnknownAttributeValue extends AttributeValue
{
/**
* @param Element $_element ASN.1 element.
*/
protected function __construct(
string $oid,
protected Element $_element
) {
parent::__construct($oid);
$this->oid = $oid;
}
public static function create(string $oid, Element $_element): self
{
return new self($oid, $_element);
}
public function toASN1(): Element
{
return $this->_element;
}
public function stringValue(): string
{
// if value is encoded as a string type
if ($this->_element->isType(Element::TYPE_STRING)) {
return $this->_element->asUnspecified()
->asString()
->string();
}
// return DER encoding as a hexstring (see RFC2253 section 2.4)
return '#' . bin2hex($this->_element->toDER());
}
public function equalityMatchingRule(): MatchingRule
{
return new BinaryMatch();
}
public function rfc2253String(): string
{
$str = $this->_transcodedString();
// if value has a string representation
if ($this->_element->isType(Element::TYPE_STRING)) {
$str = DNParser::escapeString($str);
}
return $str;
}
public static function fromASN1(UnspecifiedType $el): AttributeValue
{
throw new BadMethodCallException('ASN.1 parsing must be implemented in a concrete class.');
}
protected function _transcodedString(): string
{
// if transcoding is defined for the value type
if (TranscodeStep::isTypeSupported($this->_element->tag())) {
$step = TranscodeStep::create($this->_element->tag());
return $step->apply($this->stringValue());
}
return $this->stringValue();
}
}

View File

@ -0,0 +1,187 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\ASN1\Collection;
use ArrayIterator;
use Countable;
use IteratorAggregate;
use SpomkyLabs\Pki\ASN1\Type\Structure;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
use SpomkyLabs\Pki\X501\ASN1\Attribute;
use SpomkyLabs\Pki\X501\ASN1\AttributeType;
use UnexpectedValueException;
use function count;
/**
* Base class for X.501 attribute containers.
*
* Implements methods for Countable and IteratorAggregate interfaces.
*/
abstract class AttributeCollection implements Countable, IteratorAggregate
{
/**
* Array of attributes.
*
* Always with consecutive indices.
*
* @var Attribute[]
*/
protected array $_attributes;
/**
* @param Attribute ...$attribs List of attributes
*/
final private function __construct(Attribute ...$attribs)
{
$this->_attributes = $attribs;
}
public static function create(Attribute ...$attribs): static
{
return new static(...$attribs);
}
/**
* Check whether attribute is present.
*
* @param string $name OID or attribute name
*/
public function has(string $name): bool
{
return $this->_findFirst($name) !== null;
}
/**
* Get first attribute by OID or attribute name.
*
* @param string $name OID or attribute name
*/
public function firstOf(string $name): Attribute
{
$attr = $this->_findFirst($name);
if ($attr === null) {
throw new UnexpectedValueException("No {$name} attribute.");
}
return $attr;
}
/**
* Get all attributes of given name.
*
* @param string $name OID or attribute name
*
* @return Attribute[]
*/
public function allOf(string $name): array
{
$oid = AttributeType::attrNameToOID($name);
return array_values(array_filter($this->_attributes, fn (Attribute $attr) => $attr->oid() === $oid));
}
/**
* Get all attributes.
*
* @return Attribute[]
*/
public function all(): array
{
return $this->_attributes;
}
/**
* Get self with additional attributes added.
*
* @param Attribute ...$attribs List of attributes to add
*/
public function withAdditional(Attribute ...$attribs): self
{
$obj = clone $this;
foreach ($attribs as $attr) {
$obj->_attributes[] = $attr;
}
return $obj;
}
/**
* Get self with single unique attribute added.
*
* All previous attributes of the same type are removed.
*
* @param Attribute $attr Attribute to add
*/
public function withUnique(Attribute $attr): static
{
$attribs = array_values(array_filter($this->_attributes, fn (Attribute $a) => $a->oid() !== $attr->oid()));
$attribs[] = $attr;
$obj = clone $this;
$obj->_attributes = $attribs;
return $obj;
}
/**
* Get number of attributes.
*
* @see \Countable::count()
*/
public function count(): int
{
return count($this->_attributes);
}
/**
* Get iterator for attributes.
*
* @return ArrayIterator|Attribute[]
* @see \IteratorAggregate::getIterator()
*/
public function getIterator(): ArrayIterator
{
return new ArrayIterator($this->_attributes);
}
/**
* Find first attribute of given name or OID.
*
* @param string $name OID or attribute name
*/
protected function _findFirst(string $name): ?Attribute
{
$oid = AttributeType::attrNameToOID($name);
foreach ($this->_attributes as $attr) {
if ($attr->oid() === $oid) {
return $attr;
}
}
return null;
}
/**
* Initialize from ASN.1 constructed element.
*
* @param Structure $struct ASN.1 structure
*/
protected static function _fromASN1Structure(Structure $struct): static
{
return static::create(...array_map(
static fn (UnspecifiedType $el) => static::_castAttributeValues(
Attribute::fromASN1($el->asSequence())
),
$struct->elements()
));
}
/**
* Cast Attribute's AttributeValues to implementation specific objects.
*
* Overridden in derived classes.
*
* @param Attribute $attribute Attribute to cast
*/
protected static function _castAttributeValues(Attribute $attribute): Attribute
{
// pass through by default
return $attribute;
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\ASN1\Collection;
use SpomkyLabs\Pki\ASN1\Type\Constructed\Sequence;
use SpomkyLabs\Pki\X501\ASN1\Attribute;
use SpomkyLabs\Pki\X501\ASN1\AttributeValue\AttributeValue;
/**
* Implements *Attributes* ASN.1 type as a *SEQUENCE OF Attribute*.
*
* Used in *AttributeCertificateInfo*.
*
* @see https://tools.ietf.org/html/rfc5755#section-4.1
* @see https://tools.ietf.org/html/rfc5755#section-4.2.7
*/
class SequenceOfAttributes extends AttributeCollection
{
/**
* Initialize from ASN.1.
*/
public static function fromASN1(Sequence $seq): self
{
return static::_fromASN1Structure($seq);
}
/**
* Initialize from attribute values.
*
* @param AttributeValue ...$values List of attribute values
*/
public static function fromAttributeValues(AttributeValue ...$values): static
{
return static::create(...array_map(static fn (AttributeValue $value) => $value->toAttribute(), $values));
}
/**
* Generate ASN.1 structure.
*/
public function toASN1(): Sequence
{
return Sequence::create(...array_map(static fn (Attribute $attr) => $attr->toASN1(), $this->_attributes));
}
}

View File

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\ASN1\Collection;
use SpomkyLabs\Pki\ASN1\Type\Constructed\Set;
use SpomkyLabs\Pki\X501\ASN1\Attribute;
use SpomkyLabs\Pki\X501\ASN1\AttributeValue\AttributeValue;
/**
* Implements *Attributes* ASN.1 type as a *SET OF Attribute*.
*
* Used in *CertificationRequestInfo* and *OneAsymmetricKey*.
*
* @see https://tools.ietf.org/html/rfc2986#section-4
* @see https://tools.ietf.org/html/rfc5958#section-2
*/
class SetOfAttributes extends AttributeCollection
{
/**
* Initialize from ASN.1.
*/
public static function fromASN1(Set $set): static
{
return static::_fromASN1Structure($set);
}
/**
* Initialize from attribute values.
*
* @param AttributeValue ...$values List of attribute values
*/
public static function fromAttributeValues(AttributeValue ...$values): static
{
return static::create(...array_map(static fn (AttributeValue $value) => $value->toAttribute(), $values));
}
/**
* Generate ASN.1 structure.
*/
public function toASN1(): Set
{
$set = Set::create(...array_map(static fn (Attribute $attr) => $attr->toASN1(), $this->_attributes));
return $set->sortedSetOf();
}
}

View File

@ -0,0 +1,193 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\ASN1;
use ArrayIterator;
use Countable;
use IteratorAggregate;
use RangeException;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Type\Constructed\Sequence;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
use SpomkyLabs\Pki\X501\ASN1\AttributeValue\AttributeValue;
use SpomkyLabs\Pki\X501\DN\DNParser;
use Stringable;
use function count;
/**
* Implements *Name* ASN.1 type.
*
* Since *Name* is a CHOICE only supporting *RDNSequence* type, this class implements *RDNSequence* semantics as well.
*
* @see https://www.itu.int/ITU-T/formal-language/itu-t/x/x501/2012/InformationFramework.html#InformationFramework.Name
*/
final class Name implements Countable, IteratorAggregate, Stringable
{
/**
* Relative distinguished name components.
*
* @var RDN[]
*/
private readonly array $rdns;
/**
* @param RDN ...$rdns RDN components
*/
private function __construct(RDN ...$rdns)
{
$this->rdns = $rdns;
}
public function __toString(): string
{
return $this->toString();
}
public static function create(RDN ...$rdns): self
{
return new self(...$rdns);
}
/**
* Initialize from ASN.1.
*/
public static function fromASN1(Sequence $seq): self
{
$rdns = array_map(static fn (UnspecifiedType $el) => RDN::fromASN1($el->asSet()), $seq->elements());
return self::create(...$rdns);
}
/**
* Initialize from distinguished name string.
*
* @see https://tools.ietf.org/html/rfc1779
*/
public static function fromString(string $str): self
{
$rdns = [];
foreach (DNParser::parseString($str) as $nameComponent) {
$attribs = [];
foreach ($nameComponent as [$name, $val]) {
$type = AttributeType::fromName($name);
// hexstrings are parsed to ASN.1 elements
if ($val instanceof Element) {
$el = $val;
} else {
$el = AttributeType::asn1StringForType($type->oid(), $val);
}
$value = AttributeValue::fromASN1ByOID($type->oid(), $el->asUnspecified());
$attribs[] = AttributeTypeAndValue::create($type, $value);
}
$rdns[] = RDN::create(...$attribs);
}
return self::create(...$rdns);
}
/**
* Generate ASN.1 structure.
*/
public function toASN1(): Sequence
{
$elements = array_map(static fn (RDN $rdn) => $rdn->toASN1(), $this->rdns);
return Sequence::create(...$elements);
}
/**
* Get distinguised name string conforming to RFC 2253.
*
* @see https://tools.ietf.org/html/rfc2253#section-2.1
*/
public function toString(): string
{
$parts = array_map(static fn (RDN $rdn) => $rdn->toString(), array_reverse($this->rdns));
return implode(',', $parts);
}
/**
* Whether name is semantically equal to other.
*
* Comparison conforms to RFC 4518 string preparation algorithm.
*
* @see https://tools.ietf.org/html/rfc4518
*
* @param Name $other Object to compare to
*/
public function equals(self $other): bool
{
// if RDN count doesn't match
if (count($this) !== count($other)) {
return false;
}
for ($i = count($this) - 1; $i >= 0; --$i) {
$rdn1 = $this->rdns[$i];
$rdn2 = $other->rdns[$i];
if (! $rdn1->equals($rdn2)) {
return false;
}
}
return true;
}
/**
* Get all RDN objects.
*
* @return RDN[]
*/
public function all(): array
{
return $this->rdns;
}
/**
* Get the first AttributeValue of given type.
*
* Relative name components shall be traversed in encoding order, which is reversed in regards to the string
* representation. Multi-valued RDN with multiple attributes of the requested type is ambiguous and shall throw an
* exception.
*
* @param string $name Attribute OID or name
*/
public function firstValueOf(string $name): AttributeValue
{
$oid = AttributeType::attrNameToOID($name);
foreach ($this->rdns as $rdn) {
$tvs = $rdn->allOf($oid);
if (count($tvs) > 1) {
throw new RangeException("RDN with multiple {$name} attributes.");
}
if (count($tvs) === 1) {
return $tvs[0]->value();
}
}
throw new RangeException("Attribute {$name} not found.");
}
/**
* @see \Countable::count()
*/
public function count(): int
{
return count($this->rdns);
}
/**
* Get the number of attributes of given type.
*
* @param string $name Attribute OID or name
*/
public function countOfType(string $name): int
{
$oid = AttributeType::attrNameToOID($name);
return array_sum(array_map(static fn (RDN $rdn): int => count($rdn->allOf($oid)), $this->rdns));
}
/**
* @see \IteratorAggregate::getIterator()
*/
public function getIterator(): ArrayIterator
{
return new ArrayIterator($this->rdns);
}
}

View File

@ -0,0 +1,167 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\ASN1;
use ArrayIterator;
use Countable;
use IteratorAggregate;
use SpomkyLabs\Pki\ASN1\Type\Constructed\Set;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
use SpomkyLabs\Pki\X501\ASN1\AttributeValue\AttributeValue;
use Stringable;
use UnexpectedValueException;
use function count;
/**
* Implements *RelativeDistinguishedName* ASN.1 type.
*
* @see https://www.itu.int/ITU-T/formal-language/itu-t/x/x501/2012/InformationFramework.html#InformationFramework.RelativeDistinguishedName
*/
final class RDN implements Countable, IteratorAggregate, Stringable
{
/**
* Attributes.
*
* @var AttributeTypeAndValue[]
*/
private readonly array $_attribs;
/**
* @param AttributeTypeAndValue ...$attribs One or more attributes
*/
private function __construct(AttributeTypeAndValue ...$attribs)
{
if (count($attribs) === 0) {
throw new UnexpectedValueException('RDN must have at least one AttributeTypeAndValue.');
}
$this->_attribs = $attribs;
}
public function __toString(): string
{
return $this->toString();
}
public static function create(AttributeTypeAndValue ...$attribs): self
{
return new self(...$attribs);
}
/**
* Convenience method to initialize RDN from AttributeValue objects.
*
* @param AttributeValue ...$values One or more attributes
*/
public static function fromAttributeValues(AttributeValue ...$values): self
{
$attribs = array_map(
static fn (AttributeValue $value) => AttributeTypeAndValue::create(AttributeType::create(
$value->oid()
), $value),
$values
);
return self::create(...$attribs);
}
/**
* Initialize from ASN.1.
*/
public static function fromASN1(Set $set): self
{
$attribs = array_map(
static fn (UnspecifiedType $el) => AttributeTypeAndValue::fromASN1($el->asSequence()),
$set->elements()
);
return self::create(...$attribs);
}
/**
* Generate ASN.1 structure.
*/
public function toASN1(): Set
{
$elements = array_map(static fn (AttributeTypeAndValue $tv) => $tv->toASN1(), $this->_attribs);
return Set::create(...$elements)->sortedSetOf();
}
/**
* Get name-component string conforming to RFC 2253.
*
* @see https://tools.ietf.org/html/rfc2253#section-2.2
*/
public function toString(): string
{
$parts = array_map(static fn (AttributeTypeAndValue $tv) => $tv->toString(), $this->_attribs);
return implode('+', $parts);
}
/**
* Check whether RDN is semantically equal to other.
*
* @param RDN $other Object to compare to
*/
public function equals(self $other): bool
{
// if attribute count doesn't match
if (count($this) !== count($other)) {
return false;
}
$attribs1 = $this->_attribs;
$attribs2 = $other->_attribs;
// if there's multiple attributes, sort using SET OF rules
if (count($attribs1) > 1) {
$attribs1 = self::fromASN1($this->toASN1())->_attribs;
$attribs2 = self::fromASN1($other->toASN1())->_attribs;
}
for ($i = count($attribs1) - 1; $i >= 0; --$i) {
$tv1 = $attribs1[$i];
$tv2 = $attribs2[$i];
if (! $tv1->equals($tv2)) {
return false;
}
}
return true;
}
/**
* Get all AttributeTypeAndValue objects.
*
* @return AttributeTypeAndValue[]
*/
public function all(): array
{
return $this->_attribs;
}
/**
* Get all AttributeTypeAndValue objects of the given attribute type.
*
* @param string $name Attribute OID or name
*
* @return AttributeTypeAndValue[]
*/
public function allOf(string $name): array
{
$oid = AttributeType::attrNameToOID($name);
$attribs = array_filter($this->_attribs, static fn (AttributeTypeAndValue $tv) => $tv->oid() === $oid);
return array_values($attribs);
}
/**
* @see \Countable::count()
*/
public function count(): int
{
return count($this->_attribs);
}
/**
* @see \IteratorAggregate::getIterator()
*/
public function getIterator(): ArrayIterator
{
return new ArrayIterator($this->_attribs);
}
}

View File

@ -0,0 +1,363 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\DN;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Exception\DecodeException;
use SpomkyLabs\Pki\ASN1\Feature\ElementBase;
use UnexpectedValueException;
use function mb_strlen;
/**
* Distinguished Name parsing conforming to RFC 2253 and RFC 1779.
*
* @see https://tools.ietf.org/html/rfc1779
* @see https://tools.ietf.org/html/rfc2253
*/
final class DNParser
{
/**
* RFC 2253 special characters.
*
* @var string
*/
final public const SPECIAL_CHARS = ',=+<>#;';
/**
* DN string length.
*/
private readonly int $_len;
/**
* @param string $_dn Distinguised name
*/
private function __construct(
private readonly string $_dn
) {
$this->_len = mb_strlen($_dn, '8bit');
}
/**
* Parse distinguished name string to name-components.
*
* @return array<array<string>>
*/
public static function parseString(string $dn): array
{
$parser = new self($dn);
return $parser->parse();
}
/**
* Escape a AttributeValue string conforming to RFC 2253.
*
* @see https://tools.ietf.org/html/rfc2253#section-2.4
*/
public static function escapeString(string $str): string
{
// one of the characters ",", "+", """, "\", "<", ">" or ";"
$str = preg_replace('/([,\+"\\\<\>;])/u', '\\\\$1', $str);
// a space character occurring at the end of the string
$str = preg_replace('/( )$/u', '\\\\$1', (string) $str);
// a space or "#" character occurring at the beginning of the string
$str = preg_replace('/^([ #])/u', '\\\\$1', (string) $str);
// implementation specific special characters
$str = preg_replace_callback(
'/([\pC])/u',
function ($m) {
$octets = mb_str_split(bin2hex($m[1]), 2, '8bit');
return implode('', array_map(static fn ($octet) => '\\' . mb_strtoupper($octet, '8bit'), $octets));
},
(string) $str
);
return $str;
}
/**
* Parse DN to name-components.
*
* @return array<array<string>>
*/
private function parse(): array
{
$offset = 0;
$name = $this->_parseName($offset);
if ($offset < $this->_len) {
$remains = mb_substr($this->_dn, $offset, null, '8bit');
throw new UnexpectedValueException(sprintf(
'Parser finished before the end of string, remaining: %s',
$remains
));
}
return $name;
}
/**
* Parse 'name'.
*
* name-component *("," name-component)
*
* @return array<array<string>> Array of name-components
*/
private function _parseName(int &$offset): array
{
$idx = $offset;
$names = [];
while ($idx < $this->_len) {
$names[] = $this->_parseNameComponent($idx);
if ($idx >= $this->_len) {
break;
}
$this->_skipWs($idx);
if ($this->_dn[$idx] !== ',' && $this->_dn[$idx] !== ';') {
break;
}
++$idx;
$this->_skipWs($idx);
}
$offset = $idx;
return array_reverse($names);
}
/**
* Parse 'name-component'.
*
* attributeTypeAndValue *("+" attributeTypeAndValue)
*
* @return array<array<string, string|ElementBase>> Array of [type, value] tuples
*/
private function _parseNameComponent(int &$offset): array
{
$idx = $offset;
$tvpairs = [];
while ($idx < $this->_len) {
$tvpairs[] = $this->_parseAttrTypeAndValue($idx);
$this->_skipWs($idx);
if ($idx >= $this->_len || $this->_dn[$idx] !== '+') {
break;
}
++$idx;
$this->_skipWs($idx);
}
$offset = $idx;
return $tvpairs;
}
/**
* Parse 'attributeTypeAndValue'.
*
* attributeType "=" attributeValue
*
* @return array<string, string|ElementBase> A tuple of [type, value]. Value may be either a string or
* an Element, if it's encoded as hexstring.
*/
private function _parseAttrTypeAndValue(int &$offset): array
{
$idx = $offset;
$type = $this->_parseAttrType($idx);
$this->_skipWs($idx);
if ($idx >= $this->_len || $this->_dn[$idx++] !== '=') {
throw new UnexpectedValueException('Invalid type and value pair.');
}
$this->_skipWs($idx);
// hexstring
if ($idx < $this->_len && $this->_dn[$idx] === '#') {
++$idx;
$data = $this->_parseAttrHexValue($idx);
try {
$value = Element::fromDER($data);
} catch (DecodeException $e) {
throw new UnexpectedValueException('Invalid DER encoding from hexstring.', 0, $e);
}
} else {
$value = $this->_parseAttrStringValue($idx);
}
$offset = $idx;
return [$type, $value];
}
/**
* Parse 'attributeType'.
*
* (ALPHA 1*keychar) / oid
*/
private function _parseAttrType(int &$offset): string
{
$idx = $offset;
// dotted OID
$type = $this->_regexMatch('/^(?:oid\.)?([0-9]+(?:\.[0-9]+)*)/i', $idx);
if ($type === null) {
// name
$type = $this->_regexMatch('/^[a-z][a-z0-9\-]*/i', $idx);
if ($type === null) {
throw new UnexpectedValueException('Invalid attribute type.');
}
}
$offset = $idx;
return $type;
}
/**
* Parse 'attributeValue' of string type.
*/
private function _parseAttrStringValue(int &$offset): string
{
$idx = $offset;
if ($idx >= $this->_len) {
return '';
}
if ($this->_dn[$idx] === '"') { // quoted string
$val = $this->_parseQuotedAttrString($idx);
} else { // string
$val = $this->_parseAttrString($idx);
}
$offset = $idx;
return $val;
}
/**
* Parse plain 'attributeValue' string.
*/
private function _parseAttrString(int &$offset): string
{
$idx = $offset;
$val = '';
$wsidx = null;
while ($idx < $this->_len) {
$c = $this->_dn[$idx];
// pair (escape sequence)
if ($c === '\\') {
++$idx;
$val .= $this->_parsePairAfterSlash($idx);
$wsidx = null;
continue;
}
if ($c === '"') {
throw new UnexpectedValueException('Unexpected quotation.');
}
if (mb_strpos(self::SPECIAL_CHARS, $c, 0, '8bit') !== false) {
break;
}
// keep track of the first consecutive whitespace
if ($c === ' ') {
if ($wsidx === null) {
$wsidx = $idx;
}
} else {
$wsidx = null;
}
// stringchar
$val .= $c;
++$idx;
}
// if there was non-escaped whitespace in the end of the value
if ($wsidx !== null) {
$val = mb_substr($val, 0, -($idx - $wsidx), '8bit');
}
$offset = $idx;
return $val;
}
/**
* Parse quoted 'attributeValue' string.
*
* @param int $offset Offset to starting quote
*/
private function _parseQuotedAttrString(int &$offset): string
{
$idx = $offset + 1;
$val = '';
while ($idx < $this->_len) {
$c = $this->_dn[$idx];
if ($c === '\\') { // pair
++$idx;
$val .= $this->_parsePairAfterSlash($idx);
continue;
}
if ($c === '"') {
++$idx;
break;
}
$val .= $c;
++$idx;
}
$offset = $idx;
return $val;
}
/**
* Parse 'attributeValue' of binary type.
*/
private function _parseAttrHexValue(int &$offset): string
{
$idx = $offset;
$hexstr = $this->_regexMatch('/^(?:[0-9a-f]{2})+/i', $idx);
if ($hexstr === null) {
throw new UnexpectedValueException('Invalid hexstring.');
}
$data = hex2bin($hexstr);
$offset = $idx;
return $data;
}
/**
* Parse 'pair' after leading slash.
*/
private function _parsePairAfterSlash(int &$offset): string
{
$idx = $offset;
if ($idx >= $this->_len) {
throw new UnexpectedValueException('Unexpected end of escape sequence.');
}
$c = $this->_dn[$idx++];
// special | \ | " | SPACE
if (mb_strpos(self::SPECIAL_CHARS . '\\" ', $c, 0, '8bit') !== false) {
$val = $c;
} else { // hexpair
if ($idx >= $this->_len) {
throw new UnexpectedValueException('Unexpected end of hexpair.');
}
$val = @hex2bin($c . $this->_dn[$idx++]);
if ($val === false) {
throw new UnexpectedValueException('Invalid hexpair.');
}
}
$offset = $idx;
return $val;
}
/**
* Match DN to pattern and extract the last capture group.
*
* Updates offset to fully matched pattern.
*
* @return null|string Null if pattern doesn't match
*/
private function _regexMatch(string $pattern, int &$offset): ?string
{
$idx = $offset;
if (preg_match($pattern, mb_substr($this->_dn, $idx, null, '8bit'), $match) !== 1) {
return null;
}
$idx += mb_strlen($match[0], '8bit');
$offset = $idx;
return end($match);
}
/**
* Skip consecutive spaces.
*/
private function _skipWs(int &$offset): void
{
$idx = $offset;
while ($idx < $this->_len) {
if ($this->_dn[$idx] !== ' ') {
break;
}
++$idx;
}
$offset = $idx;
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\MatchingRule;
/**
* Implements binary matching rule.
*
* Generally used only by UnknownAttribute and custom attributes.
*/
final class BinaryMatch extends MatchingRule
{
public function compare(string $assertion, string $value): ?bool
{
return strcmp($assertion, $value) === 0;
}
}

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\MatchingRule;
use SpomkyLabs\Pki\X501\StringPrep\StringPreparer;
/**
* Implements 'caseExactMatch' matching rule.
*
* @see https://tools.ietf.org/html/rfc4517#section-4.2.4
*/
final class CaseExactMatch extends StringPrepMatchingRule
{
/**
* @param int $stringType ASN.1 string type tag
*/
private function __construct(int $stringType)
{
parent::__construct(StringPreparer::forStringType($stringType));
}
public static function create(int $stringType): self
{
return new self($stringType);
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\MatchingRule;
use SpomkyLabs\Pki\X501\StringPrep\StringPreparer;
/**
* Implements 'caseIgnoreMatch' matching rule.
*
* @see https://tools.ietf.org/html/rfc4517#section-4.2.11
*/
final class CaseIgnoreMatch extends StringPrepMatchingRule
{
/**
* @param int $stringType ASN.1 string type tag
*/
private function __construct(int $stringType)
{
parent::__construct(
StringPreparer::forStringType($stringType)->withCaseFolding(true)
);
}
public static function create(int $stringType): self
{
return new self($stringType);
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\MatchingRule;
/**
* Base class for attribute matching rules.
*
* @see https://tools.ietf.org/html/rfc4517#section-4
*/
abstract class MatchingRule
{
/**
* Compare attribute value to assertion.
*
* @param string $assertion Value to assert
* @param string $value Attribute value
*
* @return null|bool True if value matches. Null shall be returned if match
* evaluates to Undefined.
*/
abstract public function compare(string $assertion, string $value): ?bool;
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\MatchingRule;
use SpomkyLabs\Pki\X501\StringPrep\StringPreparer;
/**
* Base class for matching rules employing string preparement semantics.
*/
abstract class StringPrepMatchingRule extends MatchingRule
{
protected function __construct(
private readonly StringPreparer $preparer
) {
}
public function compare(string $assertion, string $value): ?bool
{
$assertion = $this->preparer->prepare($assertion);
$value = $this->preparer->prepare($value);
return strcmp($assertion, $value) === 0;
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\StringPrep;
/**
* Implements 'Check bidi' step of the Internationalized String Preparation as specified by RFC 4518.
*
* @see https://tools.ietf.org/html/rfc4518#section-2.5
*/
final class CheckBidiStep implements PrepareStep
{
/**
* @param string $string UTF-8 encoded string
*/
public function apply(string $string): string
{
// @todo Implement
return $string;
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\StringPrep;
/**
* Implements 'Insignificant Space Handling' step of the Internationalized String Preparation as specified by RFC 4518.
*
* This variant handles input strings that are non-substring assertion values.
*
* @see https://tools.ietf.org/html/rfc4518#section-2.6.1
*/
final class InsignificantNonSubstringSpaceStep implements PrepareStep
{
/**
* @param string $string UTF-8 encoded string
*/
public function apply(string $string): string
{
// if value contains no non-space characters
if (preg_match('/^\p{Zs}*$/u', $string) === 1) {
return ' ';
}
// trim leading and trailing spaces
$string = preg_replace('/^\p{Zs}+/u', '', $string);
$string = preg_replace('/\p{Zs}+$/u', '', (string) $string);
// convert inner space sequences to two U+0020 characters
$string = preg_replace('/\p{Zs}+/u', ' ', (string) $string);
return " {$string} ";
}
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\StringPrep;
use const MB_CASE_LOWER;
/**
* Implements 'Map' step of the Internationalized String Preparation as specified by RFC 4518.
*
* @see https://tools.ietf.org/html/rfc4518#section-2.2
*/
final class MapStep implements PrepareStep
{
/**
* @param bool $fold Whether to apply case folding
*/
private function __construct(
protected bool $fold
) {
}
public static function create(bool $fold = false): self
{
return new self($fold);
}
/**
* @param string $string UTF-8 encoded string
*/
public function apply(string $string): string
{
// @todo Implement character mappings
if ($this->fold) {
$string = mb_convert_case($string, MB_CASE_LOWER, 'UTF-8');
}
return $string;
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\StringPrep;
use Normalizer;
/**
* Implements 'Normalize' step of the Internationalized String Preparation as specified by RFC 4518.
*
* @see https://tools.ietf.org/html/rfc4518#section-2.3
*/
final class NormalizeStep implements PrepareStep
{
/**
* @param string $string UTF-8 encoded string
*/
public function apply(string $string): string
{
return normalizer_normalize($string, Normalizer::NFKC);
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\StringPrep;
/**
* Interface for string preparation steps of Internationalized String Preparation algorithm specified by RFC 4518.
*
* @see https://tools.ietf.org/html/rfc4518#section-2
*/
interface PrepareStep
{
/**
* Apply string preparation step.
*
* @param string $string String to prepare
*
* @return string Prepared string
*/
public function apply(string $string): string;
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\StringPrep;
/**
* Implements 'Prohibit' step of the Internationalized String Preparation as specified by RFC 4518.
*
* @see https://tools.ietf.org/html/rfc4518#section-2.4
*/
final class ProhibitStep implements PrepareStep
{
/**
* @param string $string UTF-8 encoded string
*/
public function apply(string $string): string
{
// @todo Implement
return $string;
}
}

View File

@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\StringPrep;
/**
* Implement Internationalized String Preparation as specified by RFC 4518.
*
* @see https://tools.ietf.org/html/rfc4518
*/
final class StringPreparer
{
final public const STEP_TRANSCODE = 1;
final public const STEP_MAP = 2;
final public const STEP_NORMALIZE = 3;
final public const STEP_PROHIBIT = 4;
final public const STEP_CHECK_BIDI = 5;
final public const STEP_INSIGNIFICANT_CHARS = 6;
/**
* @param PrepareStep[] $_steps Preparation steps to apply
*/
private function __construct(
/**
* Preparation steps.
*/
protected array $_steps
) {
}
/**
* Get default instance for given string type.
*
* @param int $string_type ASN.1 string type tag.
*/
public static function forStringType(int $string_type): self
{
$steps = [
self::STEP_TRANSCODE => TranscodeStep::create($string_type),
self::STEP_MAP => MapStep::create(),
self::STEP_NORMALIZE => new NormalizeStep(),
self::STEP_PROHIBIT => new ProhibitStep(),
self::STEP_CHECK_BIDI => new CheckBidiStep(),
// @todo Vary by string type
self::STEP_INSIGNIFICANT_CHARS => new InsignificantNonSubstringSpaceStep(),
];
return new self($steps);
}
/**
* Get self with case folding set.
*
* @param bool $fold True to apply case folding
*/
public function withCaseFolding(bool $fold): self
{
$obj = clone $this;
$obj->_steps[self::STEP_MAP] = MapStep::create($fold);
return $obj;
}
/**
* Prepare string.
*/
public function prepare(string $string): string
{
foreach ($this->_steps as $step) {
$string = $step->apply($string);
}
return $string;
}
}

View File

@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace SpomkyLabs\Pki\X501\StringPrep;
use LogicException;
use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Type\Primitive\T61String;
use function in_array;
/**
* Implements 'Transcode' step of the Internationalized String Preparation as specified by RFC 4518.
*
* @see https://tools.ietf.org/html/rfc4518#section-2.1
*/
final class TranscodeStep implements PrepareStep
{
/**
* Supported ASN.1 types.
*
* @var array<int>
*/
private const SUPPORTED_TYPES = [
Element::TYPE_UTF8_STRING,
Element::TYPE_PRINTABLE_STRING,
Element::TYPE_BMP_STRING,
Element::TYPE_UNIVERSAL_STRING,
Element::TYPE_T61_STRING,
];
/**
* @param int $_type ASN.1 type tag of the string
*/
private function __construct(
private readonly int $_type
) {
}
public static function create(int $_type): self
{
return new self($_type);
}
/**
* Check whether transcoding from given ASN.1 type tag is supported.
*
* @param int $type ASN.1 type tag
*/
public static function isTypeSupported(int $type): bool
{
return in_array($type, self::SUPPORTED_TYPES, true);
}
/**
* @param string $string String to prepare
*
* @return string UTF-8 encoded string
*/
public function apply(string $string): string
{
switch ($this->_type) {
// UTF-8 string as is
case Element::TYPE_UTF8_STRING:
// PrintableString maps directly to UTF-8
case Element::TYPE_PRINTABLE_STRING:
return $string;
// UCS-2 to UTF-8
case Element::TYPE_BMP_STRING:
return mb_convert_encoding($string, 'UTF-8', 'UCS-2BE');
// UCS-4 to UTF-8
case Element::TYPE_UNIVERSAL_STRING:
return mb_convert_encoding($string, 'UTF-8', 'UCS-4BE');
// TeletexString mapping is a local matter.
// We take a shortcut here and encode it as a hexstring.
case Element::TYPE_T61_STRING:
$el = T61String::create($string);
return '#' . bin2hex($el->toDER());
}
throw new LogicException(sprintf('Unsupported string type %s.', Element::tagToName($this->_type)));
}
}