Skip to content

Attributes

An attribute is a type and one or more values. The same structure appears in a distinguished name, in the extension request of a certification request, in the subjectDirectoryAttributes extension, and as the payload of an attribute certificate.

Types

AttributeType wraps an OID and knows the short names of the standard types:

use SpomkyLabs\Pki\X501\ASN1\AttributeType;

$type = AttributeType::fromName('cn');
$type->oid();        // "2.5.4.3"
$type->typeName();   // "commonName"

$type = AttributeType::create('2.5.4.3');
AttributeType::attrNameToOID('emailAddress');   // "1.2.840.113549.1.9.1"

The table covers X.520 (2.5.4.*), the LDAP attributes of RFC 4519 and RFC 2798 — uid, mail, dc, memberOf — and the PKCS #9 emailAddress. Names are matched case-insensitively, and several types have aliases: mail is also rfc822Mailbox, dc is also domainComponent, email is also emailAddress and pkcs9email. An OID passed where a name is expected is returned as it is.

typeName() falls back to the OID for a type with no entry, so it is always safe to print.

Values

Class, under ASN1\AttributeValue Type
CommonNameValue cn
SurnameValue sn
GivenNameValue givenName
NameValue name
PseudonymValue pseudonym
TitleValue title
DescriptionValue description
OrganizationNameValue o
OrganizationalUnitNameValue ou
LocalityNameValue l
StateOrProvinceNameValue st
CountryNameValue c
SerialNumberValue serialNumber
UnknownAttributeValue anything else

Each is built with create() and read with stringValue():

use SpomkyLabs\Pki\X501\ASN1\AttributeValue\CommonNameValue;

$value = CommonNameValue::create('example.com');
$value->stringValue();      // "example.com"
$value->oid();              // "2.5.4.3"
$value->rfc2253String();    // the escaped string form
echo $value;                // the same as stringValue()

The string type a value is written in

Most of these are DirectoryString values, which means their ASN.1 type is a choice: UTF8String, PrintableString, BMPString, UniversalString or TeletexString. create() writes a UTF8String unless you say otherwise:

use SpomkyLabs\Pki\X501\ASN1\AttributeValue\Feature\DirectoryString;

$value = CommonNameValue::create('example.com', DirectoryString::PRINTABLE);

Three types are not a directory string and are always written as a PrintableString: countryName, serialNumber and dnQualifier. Name::fromString() applies that rule for you — which is why a country code round-trips as a PrintableString and a common name as a UTF8String.

The string type is part of the encoding, not of the meaning

Two names that differ only in the string type of a value are still equal, because comparison transcodes before it compares. See Comparing names. The type still matters for an exact byte comparison, and for a peer that re-encodes what you sent.

Values of an unknown type

An attribute whose OID has no class comes back as UnknownAttributeValue, which keeps the element as it was. Nothing is lost, and nothing throws:

$value->oid();           // the OID you were given
$value->stringValue();   // the element, as a string where that makes sense

If you later write a class for that type, Attribute::castValues() converts in place:

$attribute = $attribute->castValues(MyAttributeValue::class);

The class must derive from AttributeValue and declare the same OID.

Collections

Attribute holds a type and its values:

use SpomkyLabs\Pki\X501\ASN1\Attribute;

$attribute = Attribute::fromAttributeValues(CommonNameValue::create('example.com'));

$attribute->type();      // AttributeType
$attribute->oid();
$attribute->first();     // the first value
$attribute->values();    // all of them
count($attribute);

Two collections hold attributes, and they differ only in the ASN.1 they encode to:

Class, under ASN1\Collection Encodes to
SequenceOfAttributes a SEQUENCE OF
SetOfAttributes a SET OF

Both share the AttributeCollection API:

$attributes->has('cn');
$attributes->firstOf('cn');          // an Attribute
$attributes->allOf('cn');            // Attribute[]
$attributes->all();
$attributes = $attributes->withAdditional($attribute);
$attributes = $attributes->withUnique($attribute);   // replaces any attribute of the same type
count($attributes);

foreach ($attributes as $attribute) {
}

withUnique() is what you want for an attribute that must appear once: it replaces rather than appends.

Writing a value class

Extend AttributeValue, or DirectoryString when the syntax is a directory string, and implement the four abstract methods plus fromASN1():

use SpomkyLabs\Pki\ASN1\Element;
use SpomkyLabs\Pki\ASN1\Type\Primitive\UTF8String;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
use SpomkyLabs\Pki\X501\ASN1\AttributeValue\AttributeValue;
use SpomkyLabs\Pki\X501\MatchingRule\CaseIgnoreMatch;
use SpomkyLabs\Pki\X501\MatchingRule\MatchingRule;

final class EmployeeNumberValue extends AttributeValue
{
    public const OID = '1.3.6.1.4.1.99999.1';

    private function __construct(private readonly string $value)
    {
        parent::__construct(self::OID);
    }

    public static function create(string $value): self
    {
        return new self($value);
    }

    public static function fromASN1(UnspecifiedType $el): self
    {
        return self::create($el->asUTF8String()->string());
    }

    public function toASN1(): Element
    {
        return UTF8String::create($this->value);
    }

    public function stringValue(): string
    {
        return $this->value;
    }

    public function equalityMatchingRule(): MatchingRule
    {
        return CaseIgnoreMatch::create(Element::TYPE_UTF8_STRING);
    }

    public function rfc2253String(): string
    {
        return $this->value;
    }
}

equalityMatchingRule() is what name comparison uses, so choosing it is choosing how two of your values compare. Then toAttribute() and toAttributeTypeAndValue() come for free from the base class.