From b86283ed091386c68a05127b5634b704fe655fb4 Mon Sep 17 00:00:00 2001 From: Daiki Matsudate <20222809+d-date@users.noreply.github.com> Date: Fri, 4 Sep 2026 10:02:17 +0100 Subject: [PATCH] Validate UTF8String contents when decoding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ASN1UTF8String` accepted any bytes, so `0C 02 C3 28` decoded even though `C3` must be followed by one continuation byte and `28` is not one. X.690 ยง8.23.10 defines the contents as the UTF-8 encoding of an ISO/IEC 10646 string, so those octets are not a value of the type. `ASN1PrintableString`, `ASN1VisibleString` and `ASN1IA5String` already validate from their decoding initializers; this brings UTF8String in line with them. `init(contentBytes:)` and `bytes` are left alone: validating there would change a signature or trap in existing code, which is a separate decision from what decoding accepts. --- .../Basic ASN1 Types/ASN1Strings.swift | 23 ++++++++++++++++++ Tests/SwiftASN1Tests/ASN1StringTests.swift | 24 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/Sources/SwiftASN1/Basic ASN1 Types/ASN1Strings.swift b/Sources/SwiftASN1/Basic ASN1 Types/ASN1Strings.swift index d3e93fb..649db66 100644 --- a/Sources/SwiftASN1/Basic ASN1 Types/ASN1Strings.swift +++ b/Sources/SwiftASN1/Basic ASN1 Types/ASN1Strings.swift @@ -27,11 +27,17 @@ public struct ASN1UTF8String: DERImplicitlyTaggable, BERImplicitlyTaggable, Hash @inlinable public init(derEncoded node: ASN1Node, withIdentifier identifier: ASN1Identifier) throws { self.bytes = try ASN1OctetString(derEncoded: node, withIdentifier: identifier).bytes + guard Self._isValid(self.bytes) else { + throw ASN1Error.invalidStringRepresentation(reason: "Invalid bytes for ASN1UTF8String") + } } @inlinable public init(berEncoded node: ASN1Node, withIdentifier identifier: ASN1Identifier) throws { self.bytes = try ASN1OctetString(berEncoded: node, withIdentifier: identifier).bytes + guard Self._isValid(self.bytes) else { + throw ASN1Error.invalidStringRepresentation(reason: "Invalid bytes for ASN1UTF8String") + } } /// Construct a UTF8STRING from raw bytes. @@ -61,6 +67,23 @@ public struct ASN1UTF8String: DERImplicitlyTaggable, BERImplicitlyTaggable, Hash public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { return try self.bytes.withUnsafeBytes(body) } + + @inlinable + static func _isValid(_ bytes: ArraySlice) -> Bool { + var iterator = bytes.makeIterator() + var decoder = UTF8() + + while true { + switch decoder.decode(&iterator) { + case .scalarValue: + continue + case .emptyInput: + return true + case .error: + return false + } + } + } } /// TeletexString is an uncommon ASN.1 string type. diff --git a/Tests/SwiftASN1Tests/ASN1StringTests.swift b/Tests/SwiftASN1Tests/ASN1StringTests.swift index 4a72219..76375d8 100644 --- a/Tests/SwiftASN1Tests/ASN1StringTests.swift +++ b/Tests/SwiftASN1Tests/ASN1StringTests.swift @@ -39,6 +39,30 @@ final class ASN1StringTests: XCTestCase { string.withUnsafeBytes { XCTAssertTrue($0.elementsEqual([1, 2, 3, 4])) } } + func testUTF8StringRejectsInvalidUTF8() throws { + // A lead byte that must be followed by one continuation byte, and 0x28 is not one. + XCTAssertThrowsError(try ASN1UTF8String(derEncoded: [0x0c, 0x02, 0xc3, 0x28])) + // An overlong encoding of U+002F. + XCTAssertThrowsError(try ASN1UTF8String(derEncoded: [0x0c, 0x02, 0xc0, 0xaf])) + // U+D800, the high half of a surrogate pair, encoded as three bytes. + XCTAssertThrowsError(try ASN1UTF8String(derEncoded: [0x0c, 0x03, 0xed, 0xa0, 0x80])) + // A scalar above U+10FFFF. + XCTAssertThrowsError(try ASN1UTF8String(derEncoded: [0x0c, 0x04, 0xf5, 0x80, 0x80, 0x80])) + // A sequence cut short by the end of the contents. + XCTAssertThrowsError(try ASN1UTF8String(derEncoded: [0x0c, 0x02, 0xe2, 0x82])) + // A continuation byte with nothing leading it. + XCTAssertThrowsError(try ASN1UTF8String(derEncoded: [0x0c, 0x01, 0x80])) + } + + func testUTF8StringAcceptsValidUTF8() throws { + // Empty, ASCII, two-, three- and four-byte scalars. + XCTAssertEqual(try ASN1UTF8String(derEncoded: [0x0c, 0x00]).bytes, []) + XCTAssertEqual(try ASN1UTF8String(derEncoded: [0x0c, 0x01, 0x41]).bytes, [0x41]) + XCTAssertNoThrow(try ASN1UTF8String(derEncoded: [0x0c, 0x02, 0xc2, 0xa9])) + XCTAssertNoThrow(try ASN1UTF8String(derEncoded: [0x0c, 0x03, 0xe6, 0x97, 0xa5])) + XCTAssertNoThrow(try ASN1UTF8String(derEncoded: [0x0c, 0x04, 0xf0, 0x9f, 0x94, 0x92])) + } + func testTeletexStringEncoding() throws { var serializer = DER.Serializer() let originalString = ASN1TeletexString(contentBytes: [1, 2, 3, 4])