Skip to content

Commit

Permalink
Simplify the internal representation of RDN.Attribute.Value (#156)
Browse files Browse the repository at this point in the history
* Simplify the internal representation of RDNAttribute.Value

Motivation

The 1.1.0 release included a patch (#154) that supported converting
RDNAttribute values into strings. This patch missed that the `.any`
case may also have strings in it.

While investigating this limitation, I realised that the constructor
function was excessively complex. While it attempted to coerce based
on extra information in the ASN.1 specification, it always fell back
into the `.any` case. This meant that as a practical matter we always
took the same few paths, and so could safely simplify the code.

Modifications

Simplify DER decoding of RDNAttribute.Values.
Introspect the .any cases for UTF8 strings and Printable strings
when converting to String.

Result

Faster construction, more flexible representation.

* normalise `RDN.Value.Storage` representation

---------

Co-authored-by: Cory Benfield <[email protected]>
  • Loading branch information
dnadoba and Lukasa authored Dec 6, 2023
1 parent 66143b8 commit bc566f8
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 298 deletions.
338 changes: 40 additions & 298 deletions Sources/X509/RDNAttribute.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ extension RelativeDistinguishedName {
public struct Attribute {
public struct Value: Hashable, Sendable {
@usableFromInline
enum Storage: Sendable {
enum Storage: Hashable, Sendable {
/// ``ASN1PrintableString``
case printable(String)
/// ``ASN1UTF8String``
case utf8(String)
/// `.any` can still contain bytes which are equal to the DER representation of `.printable` or `.utf8`
/// the custom `Hashable` conformance takes care of this and treats them as equal.
/// `.any` can never contain bytes which are equal to the DER representation of `.printable` or `.utf8`.
/// This invariant must not be violated or otherwise the synthesised `Hashable` would be wrong.
case any(ASN1Any)
}

Expand Down Expand Up @@ -66,172 +66,6 @@ extension RelativeDistinguishedName {
}
}

extension RelativeDistinguishedName.Attribute.Value.Storage: Hashable {
@inlinable
static func == (lhs: Self, rhs: Self) -> Bool {
switch (lhs, rhs) {
case let (.printable(lhs), .printable(rhs)):
return lhs == rhs
case let (.utf8(lhs), .utf8(rhs)):
return lhs == rhs
case (.printable, .utf8), (.utf8, .printable):
return false

default:
return ASN1Any(lhs) == ASN1Any(rhs)
}
}

@inlinable
func hash(into hasher: inout Hasher) {
switch self {
case .printable(let string):
hasher.combine(String.ASN1TaggedStringView(printable: string))

case .utf8(let string):
hasher.combine(String.ASN1TaggedStringView(utf8: string))

case .any(let asn1Any):
hasher.combine(asn1Any)
}
}
}

extension String {
@usableFromInline
struct ASN1TaggedStringView {
@usableFromInline
let tag: UInt8

@usableFromInline
let string: String

@usableFromInline
let length: ASN1Length

@usableFromInline
let count: Int

@inlinable
init(tag: UInt8, string: String) {
self.tag = tag
self.string = string

let utf8Count = self.string.utf8.count
self.length = ASN1Length(length: utf8Count)
// tag + utf8 bytes length + utf8 bytes
self.count = 1 + self.length.count + utf8Count
}

@inlinable
init(utf8 string: String) {
// This tag represents a UTF8STRING.
self.init(tag: 0x0c, string: string)
}

@inlinable
init(printable string: String) {
// This tag represents a PRINTABLE STRING.
self.init(tag: 0x13, string: string)
}
}
}

extension String.ASN1TaggedStringView: RandomAccessCollection {
@inlinable
var startIndex: Int {
0
}

@inlinable
var endIndex: Int {
count
}

@inlinable
subscript(position: Int) -> UInt8 {
switch position {
case 0:
return self.tag
case 1...self.length.endIndex:
// after the tag comes the length of the string
return self.length[position &- 1]
default:
// and at the end the utf8 encoded string
let index = self.string.utf8.index(self.string.utf8.startIndex, offsetBy: position - 1 - length.endIndex)
return self.string.utf8[index]
}
}
}

extension String.ASN1TaggedStringView: Hashable {
@inlinable
func hash(into hasher: inout Hasher) {
hasher.combine(self.count)
for byte in self {
hasher.combine(byte)
}
}
}

@usableFromInline
struct ASN1Length: Hashable {
@usableFromInline
var length: Int

@usableFromInline
var count: Int

@inlinable
init(length: Int) {
self.length = length

// ASN.1 lengths are in two forms. If we can store the length in 7 bits, we should:
// that requires only one byte. Otherwise, we need multiple bytes: work out how many,
// plus one for the length of the length bytes.
if self.length <= 0x7F {
self.count = 1
} else {
// We need to work out how many bytes we need. There are many fancy bit-twiddling
// ways of doing this, but honestly we don't do this enough to need them, so we'll
// do it the easy way. This math is done on UInt because it makes the shift semantics clean.
// We save a branch here because we can never overflow this addition.
let neededBits = self.length.bitWidth - self.length.leadingZeroBitCount
let neededBytes = (neededBits &+ 7) / 8
self.count = neededBytes &+ 1
}
}
}

extension ASN1Length: RandomAccessCollection {
@inlinable
var startIndex: Int {
0
}

@inlinable
var endIndex: Int {
count
}

@inlinable
subscript(position: Int) -> UInt8 {
precondition(position >= 0 && position < self.count)
guard self.length <= 0x7F else {
guard position == 0 else {
//Then we write the bytes of the length.
let integerBytesCollection = IntegerBytesCollection(self.length)
let index = integerBytesCollection.index(integerBytesCollection.startIndex, offsetBy: position &- 1)
return integerBytesCollection[index]
}
// We first write the number of length bytes
// we needed, setting the high bit.
return 0b1000_0000 | UInt8(self.count &- 1)
}
return UInt8(truncatingIfNeeded: self.length)
}
}

extension ASN1Any {
@inlinable
init(_ storage: RelativeDistinguishedName.Attribute.Value.Storage) {
Expand Down Expand Up @@ -274,7 +108,40 @@ extension RelativeDistinguishedName.Attribute.Value {

@inlinable
public init(asn1Any: ASN1Any) {
self.storage = .any(asn1Any)
do {
self.storage = try .init(asn1Any: asn1Any)
} catch {
self.storage = .any(asn1Any)
}
}
}

extension RelativeDistinguishedName.Attribute.Value.Storage: DERParseable, DERSerializable {
@inlinable
init(derEncoded node: SwiftASN1.ASN1Node) throws {
switch node.identifier {
case ASN1UTF8String.defaultIdentifier:
self = .utf8(String(try ASN1UTF8String(derEncoded: node)))
case ASN1PrintableString.defaultIdentifier:
self = .printable(String(try ASN1PrintableString(derEncoded: node)))
default:
self = .any(ASN1Any(derEncoded: node))
}
}

@inlinable
func serialize(into coder: inout SwiftASN1.DER.Serializer) throws {
switch self {
case .printable(let printableString):
// force try is safe because we verify in the initialiser that it is valid
let printableString = try! ASN1PrintableString(printableString)
try printableString.serialize(into: &coder)
case .utf8(let utf8String):
let string = ASN1UTF8String(utf8String)
try string.serialize(into: &coder)
case .any(let any):
try any.serialize(into: &coder)
}
}
}

Expand Down Expand Up @@ -376,132 +243,7 @@ extension RelativeDistinguishedName.Attribute: DERImplicitlyTaggable {
public init(derEncoded rootNode: ASN1Node, withIdentifier identifier: ASN1Identifier) throws {
self = try DER.sequence(rootNode, identifier: identifier) { nodes in
let type = try ASN1ObjectIdentifier(derEncoded: &nodes)
guard let valueNode = nodes.next() else {
throw ASN1Error.invalidASN1Object(reason: "RelativeDistinguishedName.Attribute.Value is missing")
}

let value: Value
switch type {
/// ```
/// id-at-commonName AttributeType ::= { id-at 3 }
///
/// -- Naming attributes of type X520CommonName:
/// -- X520CommonName ::= DirectoryName (SIZE (1..ub-common-name))
/// --
/// -- Expanded to avoid parameterized type:
/// X520CommonName ::= CHOICE {
/// teletexString TeletexString (SIZE (1..ub-common-name)),
/// printableString PrintableString (SIZE (1..ub-common-name)),
/// universalString UniversalString (SIZE (1..ub-common-name)),
/// utf8String UTF8String (SIZE (1..ub-common-name)),
/// bmpString BMPString (SIZE (1..ub-common-name)) }
///
///
/// -- Naming attributes of type X520LocalityName
///
/// id-at-localityName AttributeType ::= { id-at 7 }
///
/// -- Naming attributes of type X520LocalityName:
/// -- X520LocalityName ::= DirectoryName (SIZE (1..ub-locality-name))
/// --
/// -- Expanded to avoid parameterized type:
/// X520LocalityName ::= CHOICE {
/// teletexString TeletexString (SIZE (1..ub-locality-name)),
/// printableString PrintableString (SIZE (1..ub-locality-name)),
/// universalString UniversalString (SIZE (1..ub-locality-name)),
/// utf8String UTF8String (SIZE (1..ub-locality-name)),
/// bmpString BMPString (SIZE (1..ub-locality-
///
/// id-at-stateOrProvinceName AttributeType ::= { id-at 8 }
///
///
/// -- Naming attributes of type X520StateOrProvinceName:
/// -- X520StateOrProvinceName ::= DirectoryName (SIZE (1..ub-state-name))
/// --
/// -- Expanded to avoid parameterized type:
/// X520StateOrProvinceName ::= CHOICE {
/// teletexString TeletexString (SIZE (1..ub-state-name)),
/// printableString PrintableString (SIZE (1..ub-state-name)),
/// universalString UniversalString (SIZE (1..ub-state-name)),
/// utf8String UTF8String (SIZE (1..ub-state-name)),
/// bmpString BMPString (SIZE (1..ub-state-name)) }
///
///
/// -- Naming attributes of type X520OrganizationName
///
/// id-at-organizationName AttributeType ::= { id-at 10 }
///
/// -- Naming attributes of type X520OrganizationName:
/// -- X520OrganizationName ::=
/// -- DirectoryName (SIZE (1..ub-organization-name))
/// --
/// -- Expanded to avoid parameterized type:
/// X520OrganizationName ::= CHOICE {
/// teletexString TeletexString
/// (SIZE (1..ub-organization-name)),
/// printableString PrintableString
/// (SIZE (1..ub-organization-name)),
/// universalString UniversalString
/// (SIZE (1..ub-organization-name)),
/// utf8String UTF8String
/// (SIZE (1..ub-organization-name)),
/// bmpString BMPString
/// (SIZE (1..ub-organization-name)) }
///
///
/// id-at-organizationalUnitName AttributeType ::= { id-at 11 }
///
/// -- Naming attributes of type X520OrganizationalUnitName:
/// -- X520OrganizationalUnitName ::=
/// -- DirectoryName (SIZE (1..ub-organizational-unit-name))
/// --
/// -- Expanded to avoid parameterized type:
/// X520OrganizationalUnitName ::= CHOICE {
/// teletexString TeletexString
/// (SIZE (1..ub-organizational-unit-name)),
/// printableString PrintableString
/// (SIZE (1..ub-organizational-unit-name)),
/// universalString UniversalString
/// (SIZE (1..ub-organizational-unit-name)),
/// utf8String UTF8String
/// (SIZE (1..ub-organizational-unit-name)),
/// bmpString BMPString
/// (SIZE (1..ub-organizational-unit-name)) }
/// ```
case .RDNAttributeType.commonName,
.RDNAttributeType.localityName,
.RDNAttributeType.stateOrProvinceName,
.RDNAttributeType.organizationName,
.RDNAttributeType.organizationalUnitName:

switch valueNode.identifier {
case ASN1UTF8String.defaultIdentifier:
value = try .init(utf8String: String(ASN1UTF8String(derEncoded: valueNode)))
case ASN1PrintableString.defaultIdentifier:
value = try .init(printableString: String(ASN1PrintableString(derEncoded: valueNode)))
default:
value = .init(storage: .any(ASN1Any(derEncoded: valueNode)))
}

/// ```
/// -- Naming attributes of type X520countryName (digraph from IS 3166)
///
/// id-at-countryName AttributeType ::= { id-at 6 }
///
/// X520countryName ::= PrintableString (SIZE (2))
/// ```
case .RDNAttributeType.countryName:
switch valueNode.identifier {
case ASN1PrintableString.defaultIdentifier:
value = try .init(printableString: String(ASN1PrintableString(derEncoded: valueNode)))
default:
value = .init(storage: .any(ASN1Any(derEncoded: valueNode)))
}

default:
value = .init(storage: .any(ASN1Any(derEncoded: valueNode)))
}

let value = try Value(storage: .init(derEncoded: &nodes))
return .init(type: type, value: value)
}
}
Expand All @@ -510,7 +252,7 @@ extension RelativeDistinguishedName.Attribute: DERImplicitlyTaggable {
public func serialize(into coder: inout DER.Serializer, withIdentifier identifier: ASN1Identifier) throws {
try coder.appendConstructedNode(identifier: identifier) { coder in
try coder.serialize(self.type)
try coder.serialize(ASN1Any(self.value))
try coder.serialize(self.value.storage)
}
}
}
Expand Down Expand Up @@ -594,7 +336,7 @@ extension String {
self = printable
case .utf8(let utf8):
self = utf8
default:
case .any:
return nil
}
}
Expand Down
Loading

0 comments on commit bc566f8

Please sign in to comment.