Skip to content

Commit bc566f8

Browse files
dnadobaLukasa
andauthored
Simplify the internal representation of RDN.Attribute.Value (#156)
* 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]>
1 parent 66143b8 commit bc566f8

File tree

2 files changed

+58
-298
lines changed

2 files changed

+58
-298
lines changed

Sources/X509/RDNAttribute.swift

Lines changed: 40 additions & 298 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,13 @@ extension RelativeDistinguishedName {
2828
public struct Attribute {
2929
public struct Value: Hashable, Sendable {
3030
@usableFromInline
31-
enum Storage: Sendable {
31+
enum Storage: Hashable, Sendable {
3232
/// ``ASN1PrintableString``
3333
case printable(String)
3434
/// ``ASN1UTF8String``
3535
case utf8(String)
36-
/// `.any` can still contain bytes which are equal to the DER representation of `.printable` or `.utf8`
37-
/// the custom `Hashable` conformance takes care of this and treats them as equal.
36+
/// `.any` can never contain bytes which are equal to the DER representation of `.printable` or `.utf8`.
37+
/// This invariant must not be violated or otherwise the synthesised `Hashable` would be wrong.
3838
case any(ASN1Any)
3939
}
4040

@@ -66,172 +66,6 @@ extension RelativeDistinguishedName {
6666
}
6767
}
6868

69-
extension RelativeDistinguishedName.Attribute.Value.Storage: Hashable {
70-
@inlinable
71-
static func == (lhs: Self, rhs: Self) -> Bool {
72-
switch (lhs, rhs) {
73-
case let (.printable(lhs), .printable(rhs)):
74-
return lhs == rhs
75-
case let (.utf8(lhs), .utf8(rhs)):
76-
return lhs == rhs
77-
case (.printable, .utf8), (.utf8, .printable):
78-
return false
79-
80-
default:
81-
return ASN1Any(lhs) == ASN1Any(rhs)
82-
}
83-
}
84-
85-
@inlinable
86-
func hash(into hasher: inout Hasher) {
87-
switch self {
88-
case .printable(let string):
89-
hasher.combine(String.ASN1TaggedStringView(printable: string))
90-
91-
case .utf8(let string):
92-
hasher.combine(String.ASN1TaggedStringView(utf8: string))
93-
94-
case .any(let asn1Any):
95-
hasher.combine(asn1Any)
96-
}
97-
}
98-
}
99-
100-
extension String {
101-
@usableFromInline
102-
struct ASN1TaggedStringView {
103-
@usableFromInline
104-
let tag: UInt8
105-
106-
@usableFromInline
107-
let string: String
108-
109-
@usableFromInline
110-
let length: ASN1Length
111-
112-
@usableFromInline
113-
let count: Int
114-
115-
@inlinable
116-
init(tag: UInt8, string: String) {
117-
self.tag = tag
118-
self.string = string
119-
120-
let utf8Count = self.string.utf8.count
121-
self.length = ASN1Length(length: utf8Count)
122-
// tag + utf8 bytes length + utf8 bytes
123-
self.count = 1 + self.length.count + utf8Count
124-
}
125-
126-
@inlinable
127-
init(utf8 string: String) {
128-
// This tag represents a UTF8STRING.
129-
self.init(tag: 0x0c, string: string)
130-
}
131-
132-
@inlinable
133-
init(printable string: String) {
134-
// This tag represents a PRINTABLE STRING.
135-
self.init(tag: 0x13, string: string)
136-
}
137-
}
138-
}
139-
140-
extension String.ASN1TaggedStringView: RandomAccessCollection {
141-
@inlinable
142-
var startIndex: Int {
143-
0
144-
}
145-
146-
@inlinable
147-
var endIndex: Int {
148-
count
149-
}
150-
151-
@inlinable
152-
subscript(position: Int) -> UInt8 {
153-
switch position {
154-
case 0:
155-
return self.tag
156-
case 1...self.length.endIndex:
157-
// after the tag comes the length of the string
158-
return self.length[position &- 1]
159-
default:
160-
// and at the end the utf8 encoded string
161-
let index = self.string.utf8.index(self.string.utf8.startIndex, offsetBy: position - 1 - length.endIndex)
162-
return self.string.utf8[index]
163-
}
164-
}
165-
}
166-
167-
extension String.ASN1TaggedStringView: Hashable {
168-
@inlinable
169-
func hash(into hasher: inout Hasher) {
170-
hasher.combine(self.count)
171-
for byte in self {
172-
hasher.combine(byte)
173-
}
174-
}
175-
}
176-
177-
@usableFromInline
178-
struct ASN1Length: Hashable {
179-
@usableFromInline
180-
var length: Int
181-
182-
@usableFromInline
183-
var count: Int
184-
185-
@inlinable
186-
init(length: Int) {
187-
self.length = length
188-
189-
// ASN.1 lengths are in two forms. If we can store the length in 7 bits, we should:
190-
// that requires only one byte. Otherwise, we need multiple bytes: work out how many,
191-
// plus one for the length of the length bytes.
192-
if self.length <= 0x7F {
193-
self.count = 1
194-
} else {
195-
// We need to work out how many bytes we need. There are many fancy bit-twiddling
196-
// ways of doing this, but honestly we don't do this enough to need them, so we'll
197-
// do it the easy way. This math is done on UInt because it makes the shift semantics clean.
198-
// We save a branch here because we can never overflow this addition.
199-
let neededBits = self.length.bitWidth - self.length.leadingZeroBitCount
200-
let neededBytes = (neededBits &+ 7) / 8
201-
self.count = neededBytes &+ 1
202-
}
203-
}
204-
}
205-
206-
extension ASN1Length: RandomAccessCollection {
207-
@inlinable
208-
var startIndex: Int {
209-
0
210-
}
211-
212-
@inlinable
213-
var endIndex: Int {
214-
count
215-
}
216-
217-
@inlinable
218-
subscript(position: Int) -> UInt8 {
219-
precondition(position >= 0 && position < self.count)
220-
guard self.length <= 0x7F else {
221-
guard position == 0 else {
222-
//Then we write the bytes of the length.
223-
let integerBytesCollection = IntegerBytesCollection(self.length)
224-
let index = integerBytesCollection.index(integerBytesCollection.startIndex, offsetBy: position &- 1)
225-
return integerBytesCollection[index]
226-
}
227-
// We first write the number of length bytes
228-
// we needed, setting the high bit.
229-
return 0b1000_0000 | UInt8(self.count &- 1)
230-
}
231-
return UInt8(truncatingIfNeeded: self.length)
232-
}
233-
}
234-
23569
extension ASN1Any {
23670
@inlinable
23771
init(_ storage: RelativeDistinguishedName.Attribute.Value.Storage) {
@@ -274,7 +108,40 @@ extension RelativeDistinguishedName.Attribute.Value {
274108

275109
@inlinable
276110
public init(asn1Any: ASN1Any) {
277-
self.storage = .any(asn1Any)
111+
do {
112+
self.storage = try .init(asn1Any: asn1Any)
113+
} catch {
114+
self.storage = .any(asn1Any)
115+
}
116+
}
117+
}
118+
119+
extension RelativeDistinguishedName.Attribute.Value.Storage: DERParseable, DERSerializable {
120+
@inlinable
121+
init(derEncoded node: SwiftASN1.ASN1Node) throws {
122+
switch node.identifier {
123+
case ASN1UTF8String.defaultIdentifier:
124+
self = .utf8(String(try ASN1UTF8String(derEncoded: node)))
125+
case ASN1PrintableString.defaultIdentifier:
126+
self = .printable(String(try ASN1PrintableString(derEncoded: node)))
127+
default:
128+
self = .any(ASN1Any(derEncoded: node))
129+
}
130+
}
131+
132+
@inlinable
133+
func serialize(into coder: inout SwiftASN1.DER.Serializer) throws {
134+
switch self {
135+
case .printable(let printableString):
136+
// force try is safe because we verify in the initialiser that it is valid
137+
let printableString = try! ASN1PrintableString(printableString)
138+
try printableString.serialize(into: &coder)
139+
case .utf8(let utf8String):
140+
let string = ASN1UTF8String(utf8String)
141+
try string.serialize(into: &coder)
142+
case .any(let any):
143+
try any.serialize(into: &coder)
144+
}
278145
}
279146
}
280147

@@ -376,132 +243,7 @@ extension RelativeDistinguishedName.Attribute: DERImplicitlyTaggable {
376243
public init(derEncoded rootNode: ASN1Node, withIdentifier identifier: ASN1Identifier) throws {
377244
self = try DER.sequence(rootNode, identifier: identifier) { nodes in
378245
let type = try ASN1ObjectIdentifier(derEncoded: &nodes)
379-
guard let valueNode = nodes.next() else {
380-
throw ASN1Error.invalidASN1Object(reason: "RelativeDistinguishedName.Attribute.Value is missing")
381-
}
382-
383-
let value: Value
384-
switch type {
385-
/// ```
386-
/// id-at-commonName AttributeType ::= { id-at 3 }
387-
///
388-
/// -- Naming attributes of type X520CommonName:
389-
/// -- X520CommonName ::= DirectoryName (SIZE (1..ub-common-name))
390-
/// --
391-
/// -- Expanded to avoid parameterized type:
392-
/// X520CommonName ::= CHOICE {
393-
/// teletexString TeletexString (SIZE (1..ub-common-name)),
394-
/// printableString PrintableString (SIZE (1..ub-common-name)),
395-
/// universalString UniversalString (SIZE (1..ub-common-name)),
396-
/// utf8String UTF8String (SIZE (1..ub-common-name)),
397-
/// bmpString BMPString (SIZE (1..ub-common-name)) }
398-
///
399-
///
400-
/// -- Naming attributes of type X520LocalityName
401-
///
402-
/// id-at-localityName AttributeType ::= { id-at 7 }
403-
///
404-
/// -- Naming attributes of type X520LocalityName:
405-
/// -- X520LocalityName ::= DirectoryName (SIZE (1..ub-locality-name))
406-
/// --
407-
/// -- Expanded to avoid parameterized type:
408-
/// X520LocalityName ::= CHOICE {
409-
/// teletexString TeletexString (SIZE (1..ub-locality-name)),
410-
/// printableString PrintableString (SIZE (1..ub-locality-name)),
411-
/// universalString UniversalString (SIZE (1..ub-locality-name)),
412-
/// utf8String UTF8String (SIZE (1..ub-locality-name)),
413-
/// bmpString BMPString (SIZE (1..ub-locality-
414-
///
415-
/// id-at-stateOrProvinceName AttributeType ::= { id-at 8 }
416-
///
417-
///
418-
/// -- Naming attributes of type X520StateOrProvinceName:
419-
/// -- X520StateOrProvinceName ::= DirectoryName (SIZE (1..ub-state-name))
420-
/// --
421-
/// -- Expanded to avoid parameterized type:
422-
/// X520StateOrProvinceName ::= CHOICE {
423-
/// teletexString TeletexString (SIZE (1..ub-state-name)),
424-
/// printableString PrintableString (SIZE (1..ub-state-name)),
425-
/// universalString UniversalString (SIZE (1..ub-state-name)),
426-
/// utf8String UTF8String (SIZE (1..ub-state-name)),
427-
/// bmpString BMPString (SIZE (1..ub-state-name)) }
428-
///
429-
///
430-
/// -- Naming attributes of type X520OrganizationName
431-
///
432-
/// id-at-organizationName AttributeType ::= { id-at 10 }
433-
///
434-
/// -- Naming attributes of type X520OrganizationName:
435-
/// -- X520OrganizationName ::=
436-
/// -- DirectoryName (SIZE (1..ub-organization-name))
437-
/// --
438-
/// -- Expanded to avoid parameterized type:
439-
/// X520OrganizationName ::= CHOICE {
440-
/// teletexString TeletexString
441-
/// (SIZE (1..ub-organization-name)),
442-
/// printableString PrintableString
443-
/// (SIZE (1..ub-organization-name)),
444-
/// universalString UniversalString
445-
/// (SIZE (1..ub-organization-name)),
446-
/// utf8String UTF8String
447-
/// (SIZE (1..ub-organization-name)),
448-
/// bmpString BMPString
449-
/// (SIZE (1..ub-organization-name)) }
450-
///
451-
///
452-
/// id-at-organizationalUnitName AttributeType ::= { id-at 11 }
453-
///
454-
/// -- Naming attributes of type X520OrganizationalUnitName:
455-
/// -- X520OrganizationalUnitName ::=
456-
/// -- DirectoryName (SIZE (1..ub-organizational-unit-name))
457-
/// --
458-
/// -- Expanded to avoid parameterized type:
459-
/// X520OrganizationalUnitName ::= CHOICE {
460-
/// teletexString TeletexString
461-
/// (SIZE (1..ub-organizational-unit-name)),
462-
/// printableString PrintableString
463-
/// (SIZE (1..ub-organizational-unit-name)),
464-
/// universalString UniversalString
465-
/// (SIZE (1..ub-organizational-unit-name)),
466-
/// utf8String UTF8String
467-
/// (SIZE (1..ub-organizational-unit-name)),
468-
/// bmpString BMPString
469-
/// (SIZE (1..ub-organizational-unit-name)) }
470-
/// ```
471-
case .RDNAttributeType.commonName,
472-
.RDNAttributeType.localityName,
473-
.RDNAttributeType.stateOrProvinceName,
474-
.RDNAttributeType.organizationName,
475-
.RDNAttributeType.organizationalUnitName:
476-
477-
switch valueNode.identifier {
478-
case ASN1UTF8String.defaultIdentifier:
479-
value = try .init(utf8String: String(ASN1UTF8String(derEncoded: valueNode)))
480-
case ASN1PrintableString.defaultIdentifier:
481-
value = try .init(printableString: String(ASN1PrintableString(derEncoded: valueNode)))
482-
default:
483-
value = .init(storage: .any(ASN1Any(derEncoded: valueNode)))
484-
}
485-
486-
/// ```
487-
/// -- Naming attributes of type X520countryName (digraph from IS 3166)
488-
///
489-
/// id-at-countryName AttributeType ::= { id-at 6 }
490-
///
491-
/// X520countryName ::= PrintableString (SIZE (2))
492-
/// ```
493-
case .RDNAttributeType.countryName:
494-
switch valueNode.identifier {
495-
case ASN1PrintableString.defaultIdentifier:
496-
value = try .init(printableString: String(ASN1PrintableString(derEncoded: valueNode)))
497-
default:
498-
value = .init(storage: .any(ASN1Any(derEncoded: valueNode)))
499-
}
500-
501-
default:
502-
value = .init(storage: .any(ASN1Any(derEncoded: valueNode)))
503-
}
504-
246+
let value = try Value(storage: .init(derEncoded: &nodes))
505247
return .init(type: type, value: value)
506248
}
507249
}
@@ -510,7 +252,7 @@ extension RelativeDistinguishedName.Attribute: DERImplicitlyTaggable {
510252
public func serialize(into coder: inout DER.Serializer, withIdentifier identifier: ASN1Identifier) throws {
511253
try coder.appendConstructedNode(identifier: identifier) { coder in
512254
try coder.serialize(self.type)
513-
try coder.serialize(ASN1Any(self.value))
255+
try coder.serialize(self.value.storage)
514256
}
515257
}
516258
}
@@ -594,7 +336,7 @@ extension String {
594336
self = printable
595337
case .utf8(let utf8):
596338
self = utf8
597-
default:
339+
case .any:
598340
return nil
599341
}
600342
}

0 commit comments

Comments
 (0)