Skip to content

Commit bdcbccd

Browse files
Feature: Enhance RDN parsing to support IA5String (#236)
Add capability of converting RDN value of ASN1IA5String to String, if I am correct all RFC expect that RDN value can be ASN1IA5String.. - additionally fix tests after this change - added DN components emailAddress(E) and domainComponent(DC)
1 parent 63d06f4 commit bdcbccd

File tree

6 files changed

+164
-5
lines changed

6 files changed

+164
-5
lines changed

Sources/X509/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ add_library(X509
4040
"DistinguishedNameBuilder/CommonName.swift"
4141
"DistinguishedNameBuilder/CountryName.swift"
4242
"DistinguishedNameBuilder/DNBuilder.swift"
43+
"DistinguishedNameBuilder/DomainComponent.swift"
44+
"DistinguishedNameBuilder/EmailAddress.swift"
4345
"DistinguishedNameBuilder/LocalityName.swift"
4446
"DistinguishedNameBuilder/OrganizationName.swift"
4547
"DistinguishedNameBuilder/OrganizationalUnitName.swift"
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftCertificates open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the SwiftCertificates project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftCertificates project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import SwiftASN1
16+
17+
/// Set the Domain Component (DC) of a ``DistinguishedName``.
18+
///
19+
/// This type is used in ``DistinguishedNameBuilder`` contexts.
20+
public struct DomainComponent: RelativeDistinguishedNameConvertible {
21+
/// The value of the organizational unit name field.
22+
public var name: String
23+
24+
/// Construct a new organizational unit name
25+
///
26+
/// - Parameter name: The value of the organizational unit name
27+
@inlinable
28+
public init(_ name: String) {
29+
self.name = name
30+
}
31+
32+
@inlinable
33+
public func makeRDN() throws -> RelativeDistinguishedName {
34+
return RelativeDistinguishedName(
35+
try .init(type: .RDNAttributeType.domainComponent, ia5String: name)
36+
)
37+
}
38+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftCertificates open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the SwiftCertificates project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftCertificates project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import SwiftASN1
16+
17+
/// Set the Domain Component (E) of a ``DistinguishedName``.
18+
///
19+
/// This type is used in ``DistinguishedNameBuilder`` contexts.
20+
public struct EmailAddress: RelativeDistinguishedNameConvertible {
21+
/// The value of the email name field.
22+
public var name: String
23+
24+
/// Construct a new organizational unit name
25+
///
26+
/// - Parameter name: The value of the organizational unit name
27+
@inlinable
28+
public init(_ name: String) {
29+
self.name = name
30+
}
31+
32+
@inlinable
33+
public func makeRDN() throws -> RelativeDistinguishedName {
34+
return RelativeDistinguishedName(
35+
try .init(type: .RDNAttributeType.emailAddress, ia5String: name)
36+
)
37+
}
38+
}

Sources/X509/Docs.docc/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ certificate authorities, authenticating peers, and more.
7575
- ``OrganizationName``
7676
- ``StateOrProvinceName``
7777
- ``StreetAddress``
78+
- ``DomainComponent``
79+
- ``EmailAddress``
7880

7981
### Verifying Certificates
8082

Sources/X509/RDNAttribute.swift

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ extension RelativeDistinguishedName {
3333
case printable(String)
3434
/// ``ASN1UTF8String``
3535
case utf8(String)
36-
/// `.any` can never contain bytes which are equal to the DER representation of `.printable` or `.utf8`.
36+
/// ``ASN1IA5String``
37+
case ia5(String)
38+
/// `.any` can never contain bytes which are equal to the DER representation of `.printable`, `.utf8` or `.ia5`.
3739
/// This invariant must not be violated or otherwise the synthesised `Hashable` would be wrong.
3840
case any(ASN1Any)
3941
}
@@ -76,6 +78,9 @@ extension ASN1Any {
7678
case .utf8(let utf8String):
7779
// force try is safe because we verify in the initialiser that it is valid
7880
self = try! .init(erasing: ASN1UTF8String(utf8String))
81+
case .ia5(let ia5String):
82+
// force try is safe because we verify in the initialiser that it is valid
83+
self = try! .init(erasing: ASN1IA5String(ia5String))
7984
case .any(let any):
8085
self = any
8186
}
@@ -106,6 +111,14 @@ extension RelativeDistinguishedName.Attribute.Value {
106111
self.storage = .printable(printableString)
107112
}
108113

114+
/// A helper constructor to construct a ``RelativeDistinguishedName/Attribute/Value`` with an `ASN1IA5String`.
115+
@inlinable
116+
public init(ia5String: String) throws {
117+
// verify that it is indeed a ASN1IA5String
118+
_ = try ASN1IA5String(ia5String)
119+
self.storage = .ia5(ia5String)
120+
}
121+
109122
@inlinable
110123
public init(asn1Any: ASN1Any) {
111124
do {
@@ -125,6 +138,8 @@ extension RelativeDistinguishedName.Attribute.Value.Storage: DERParseable, DERSe
125138
self = .utf8(String(try ASN1UTF8String(derEncoded: node)))
126139
case ASN1PrintableString.defaultIdentifier:
127140
self = .printable(String(try ASN1PrintableString(derEncoded: node)))
141+
case ASN1IA5String.defaultIdentifier:
142+
self = .ia5(String(try ASN1IA5String(derEncoded: node)))
128143
default:
129144
self = .any(ASN1Any(derEncoded: node))
130145
}
@@ -143,6 +158,10 @@ extension RelativeDistinguishedName.Attribute.Value.Storage: DERParseable, DERSe
143158
case .utf8(let utf8String):
144159
let string = ASN1UTF8String(utf8String)
145160
try string.serialize(into: &coder)
161+
case .ia5(let ia5String):
162+
// force try is safe because we verify in the initialiser that it is valid
163+
let string = try! ASN1IA5String(ia5String)
164+
try string.serialize(into: &coder)
146165
case .any(let any):
147166
try any.serialize(into: &coder)
148167
}
@@ -220,6 +239,10 @@ extension RelativeDistinguishedName.Attribute: CustomStringConvertible {
220239
attributeKey = "OU"
221240
case .RDNAttributeType.streetAddress:
222241
attributeKey = "STREET"
242+
case .RDNAttributeType.domainComponent:
243+
attributeKey = "DC"
244+
case .RDNAttributeType.emailAddress:
245+
attributeKey = "E"
223246
case let type:
224247
attributeKey = String(describing: type)
225248
}
@@ -275,6 +298,12 @@ extension RelativeDistinguishedName.Attribute {
275298
self.value = try .init(printableString: printableString)
276299
}
277300

301+
@inlinable
302+
public init(type: ASN1ObjectIdentifier, ia5String: String) throws {
303+
self.type = type
304+
self.value = try .init(ia5String: ia5String)
305+
}
306+
278307
/// Create a new attribute from a given type and value.
279308
///
280309
/// - Parameter type: The type of the attribute.
@@ -318,6 +347,13 @@ extension ASN1ObjectIdentifier {
318347
/// information from a postal address (i.e., the street name, place,
319348
/// avenue, and the house number).
320349
public static let streetAddress: ASN1ObjectIdentifier = [2, 5, 4, 9]
350+
351+
/// The `domainComponent` attribute type contains parts (labels) of a DNS domain name
352+
public static let domainComponent: ASN1ObjectIdentifier = [0, 9, 2342, 19_200_300, 100, 1, 25]
353+
354+
/// The `emailAddress` attribute type contains email address defined in PCKS#9 (RFC2985).
355+
/// Be aware that, modern best practices (e.g., RFC 5280) discourage embedding email addresses in the `Subject DN` instead it should be in `Subject Alternative Name (SAN)
356+
public static let emailAddress: ASN1ObjectIdentifier = [1, 2, 840, 113549, 1, 9, 1]
321357
}
322358
}
323359

@@ -331,6 +367,8 @@ extension String {
331367
self = printable
332368
case .utf8(let utf8):
333369
self = utf8
370+
case .ia5(let ia5):
371+
self = ia5
334372
case .any:
335373
return nil
336374
}

Tests/X509Tests/DistinguishedNameTests.swift

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,9 @@ final class DistinguishedNameTests: XCTestCase {
211211
OrganizationName("DigiCert Inc")
212212
OrganizationalUnitName("www.digicert.com")
213213
CommonName("DigiCert Global Root G3")
214+
EmailAddress("[email protected]")
215+
DomainComponent("apple")
216+
DomainComponent("com")
214217
}
215218
XCTAssertEqual(
216219
name,
@@ -228,6 +231,12 @@ final class DistinguishedNameTests: XCTestCase {
228231
type: .RDNAttributeType.commonName,
229232
utf8String: "DigiCert Global Root G3"
230233
),
234+
RelativeDistinguishedName.Attribute(
235+
type: .RDNAttributeType.emailAddress,
236+
ia5String: "[email protected]"
237+
),
238+
RelativeDistinguishedName.Attribute(type: .RDNAttributeType.domainComponent, ia5String: "apple"),
239+
RelativeDistinguishedName.Attribute(type: .RDNAttributeType.domainComponent, ia5String: "com"),
231240
])
232241
)
233242
}
@@ -280,6 +289,9 @@ final class DistinguishedNameTests: XCTestCase {
280289

281290
func testDistinguishedNameRepresentation() throws {
282291
let name = try DistinguishedName([
292+
RelativeDistinguishedName.Attribute(type: .RDNAttributeType.domainComponent, ia5String: "com"),
293+
RelativeDistinguishedName.Attribute(type: .RDNAttributeType.domainComponent, ia5String: "apple"),
294+
RelativeDistinguishedName.Attribute(type: .RDNAttributeType.emailAddress, ia5String: "[email protected]"),
283295
RelativeDistinguishedName.Attribute(type: .RDNAttributeType.countryName, utf8String: "US"),
284296
RelativeDistinguishedName.Attribute(type: .RDNAttributeType.organizationName, utf8String: "DigiCert Inc"),
285297
RelativeDistinguishedName.Attribute(
@@ -290,14 +302,30 @@ final class DistinguishedNameTests: XCTestCase {
290302
type: .RDNAttributeType.commonName,
291303
utf8String: "DigiCert Global Root G3"
292304
),
305+
293306
])
294307

295308
let s = String(describing: name)
296-
XCTAssertEqual(s, "CN=DigiCert Global Root G3,OU=www.digicert.com,O=DigiCert Inc,C=US")
309+
XCTAssertEqual(
310+
s,
311+
"CN=DigiCert Global Root G3,OU=www.digicert.com,O=DigiCert Inc,C=US,[email protected],DC=apple,DC=com"
312+
)
297313
}
298314

299315
func testDistinguishedNameRepresentationWithNestedAttributes() throws {
300316
let name = try DistinguishedName([
317+
RelativeDistinguishedName([
318+
RelativeDistinguishedName.Attribute(type: .RDNAttributeType.domainComponent, ia5String: "com")
319+
]),
320+
RelativeDistinguishedName([
321+
RelativeDistinguishedName.Attribute(type: .RDNAttributeType.domainComponent, ia5String: "apple")
322+
]),
323+
RelativeDistinguishedName([
324+
RelativeDistinguishedName.Attribute(
325+
type: .RDNAttributeType.emailAddress,
326+
ia5String: "[email protected]"
327+
)
328+
]),
301329
RelativeDistinguishedName([
302330
RelativeDistinguishedName.Attribute(type: .RDNAttributeType.countryName, utf8String: "US")
303331
]),
@@ -329,7 +357,10 @@ final class DistinguishedNameTests: XCTestCase {
329357
])
330358

331359
let s = String(describing: name)
332-
XCTAssertEqual(s, "CN=DigiCert Global Root G3,OU=www.digicert.com,O=DigiCert Inc,ST=CA+ST=California,C=US")
360+
XCTAssertEqual(
361+
s,
362+
"CN=DigiCert Global Root G3,OU=www.digicert.com,O=DigiCert Inc,ST=CA+ST=California,C=US,[email protected],DC=apple,DC=com"
363+
)
333364
}
334365

335366
func testDistinguishedNameRepresentationWithCommasAndNewlines() throws {
@@ -420,7 +451,17 @@ final class DistinguishedNameTests: XCTestCase {
420451
let examplesAndResults: [(RelativeDistinguishedName.Attribute, String?)] = try [
421452
(.init(type: .RDNAttributeType.commonName, printableString: "foo"), "foo"),
422453
(.init(type: .RDNAttributeType.commonName, utf8String: "bar"), "bar"),
423-
(.init(type: .RDNAttributeType.commonName, value: ASN1Any(erasing: ASN1IA5String("foo"))), nil),
454+
(.init(type: .RDNAttributeType.commonName, ia5String: "foo"), "foo"),
455+
/// ASN1IA5String with wrong tag
456+
(
457+
.init(type: .RDNAttributeType.commonName, value: ASN1Any(derEncoded: [0x19, 0x03, 0x41, 0x42, 0x43])),
458+
nil
459+
),
460+
/// ASN1IA5String byte that falls outside the range of 7-bit ASCII
461+
(
462+
.init(type: .RDNAttributeType.commonName, value: ASN1Any(derEncoded: [0x16, 0x03, 0x41, 0x42, 0x80])),
463+
nil
464+
),
424465
]
425466

426467
for (example, result) in examplesAndResults {
@@ -436,7 +477,7 @@ final class DistinguishedNameTests: XCTestCase {
436477
(.init(type: weirdOID, utf8String: "bar"), "bar"),
437478
(.init(type: weirdOID, value: ASN1Any(erasing: ASN1UTF8String("foo"))), "foo"),
438479
(.init(type: weirdOID, value: ASN1Any(erasing: ASN1PrintableString("baz"))), "baz"),
439-
(.init(type: weirdOID, value: ASN1Any(erasing: ASN1IA5String("foo"))), nil),
480+
(.init(type: weirdOID, value: ASN1Any(erasing: ASN1IA5String("foo"))), "foo"),
440481
(.init(type: weirdOID, value: ASN1Any(erasing: 5)), nil),
441482
(.init(type: weirdOID, value: ASN1Any(erasing: ASN1OctetString(contentBytes: [1, 2, 3, 4]))), nil),
442483
]

0 commit comments

Comments
 (0)