From 87d0a2752b9c91e7a13cfb5164a8661fe3070d56 Mon Sep 17 00:00:00 2001 From: Nicolas Bachschmidt Date: Tue, 17 Dec 2024 19:25:36 +0100 Subject: [PATCH] Support certificates with invalid RDN attribute values (#214) Some certificate authorities issue certificates with invalid RDN attribute values. Certificates must use UTF8String instead of PrintableString to represent strings that contains `*` or `@`, but mistakes happen. When a RDN attribute value is a PrintableString or UTF8String with an invalid content, rather than rejecting the certificate, fall back to storing the value as an Any. ## Rationale Certificate authorities using the wrong string type is a common mistake. For example, DigiCert issued intermediate certificates with invalid ampersands to Wells Fargo & Company (golang/go#22970). This PR adds *partial* support for certificates that contain invalid PrintableString or UTF8String (less likely) values. When failing to parse a PrintableString or UTF8String, the raw bytes are stored as an Any (same as IA5String, BMPString, ...). Though those certificates are now parsed without any error, they aren't fully supported yet. Trying to convert the value to a String returns `nil`, so the CN doesn't match. I don't know whether we should add support for converting invalid PrintableString values (and IA5String/BMPString/...) to String in the future. In the meantime, developers who want to read the CN have access the raw Any value. --- Sources/X509/RDNAttribute.swift | 31 ++++++++------------ Tests/X509Tests/DistinguishedNameTests.swift | 14 +++++++++ 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/Sources/X509/RDNAttribute.swift b/Sources/X509/RDNAttribute.swift index c83c081f..84442e61 100644 --- a/Sources/X509/RDNAttribute.swift +++ b/Sources/X509/RDNAttribute.swift @@ -119,12 +119,16 @@ extension RelativeDistinguishedName.Attribute.Value { 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: + do { + 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)) + } + } catch { self = .any(ASN1Any(derEncoded: node)) } } @@ -149,19 +153,10 @@ extension RelativeDistinguishedName.Attribute.Value: CustomStringConvertible { @inlinable public var description: String { let text: String - switch storage { - case .printable(let string), .utf8(let string): + if let string = String(self) { text = string - case .any(let any): - do { - text = try String(ASN1PrintableString(asn1Any: any)) - } catch { - do { - text = try String(ASN1UTF8String(asn1Any: any)) - } catch { - text = String(describing: any) - } - } + } else { + text = String(describing: ASN1Any(self)) } // This is a very slow way to do this, but until we have any evidence that diff --git a/Tests/X509Tests/DistinguishedNameTests.swift b/Tests/X509Tests/DistinguishedNameTests.swift index a049035f..76b9efe5 100644 --- a/Tests/X509Tests/DistinguishedNameTests.swift +++ b/Tests/X509Tests/DistinguishedNameTests.swift @@ -445,4 +445,18 @@ final class DistinguishedNameTests: XCTestCase { XCTAssertEqual(String(example.value), result) } } + + func testRDNAttributeValuesCanBeParsedWhenPrintableStringIsInvalid() throws { + // '&' is not allowed in PrintableString. + let value = try ASN1Any(erasing: ASN1UTF8String("Wells Fargo & Company"), withIdentifier: .printableString) + + let attribute = try RelativeDistinguishedName.Attribute(derEncoded: [ + 0x30, 0x1c, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x15, 0x57, 0x65, 0x6c, 0x6c, 0x73, 0x20, + 0x46, 0x61, 0x72, 0x67, 0x6f, 0x20, 0x26, 0x20, 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x6e, 0x79, + ]) + + XCTAssertEqual(attribute.type, .RDNAttributeType.organizationName) + XCTAssertEqual(attribute.value, RelativeDistinguishedName.Attribute.Value(asn1Any: value)) + XCTAssertNil(String(attribute.value)) + } }