Skip to content

Commit

Permalink
Use case-insensitive comparison when validating cert domain name (#175)
Browse files Browse the repository at this point in the history
  • Loading branch information
baarde authored May 15, 2024
1 parent 83640c8 commit 6f2dc4a
Show file tree
Hide file tree
Showing 2 changed files with 35 additions and 25 deletions.
20 changes: 15 additions & 5 deletions Sources/X509/Verifier/ServerIdentityPolicy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,12 @@ extension Collection {
}
}

extension Sequence<UInt8> {
fileprivate func caseInsensitiveElementsEqual(_ other: some Sequence<UInt8>) -> Bool {
self.elementsEqual(other) { $0.lowercased() == $1.lowercased() }
}
}

extension UInt8 {
/// Whether this character is a valid DNS character, which is the ASCII
/// letters, digits, the hypen, and the period.
Expand All @@ -394,6 +400,10 @@ extension UInt8 {
return false
}
}

fileprivate func lowercased() -> UInt8 {
asciiCapitals.contains(self) ? self | 0x20 : self
}
}

/// This structure contains a certificate hostname that has been analysed and prepared for matching.
Expand Down Expand Up @@ -457,7 +467,7 @@ private struct AnalysedCertificateHostname<
// Now we can finally initialize ourself.
if let asteriskIndex = asteriskIndex {
// One final check: if we found a wildcard, we need to confirm that the first label isn't an IDNA A label.
if baseName.prefix(4).elementsEqual(asciiIDNAIdentifier) {
if baseName.prefix(4).caseInsensitiveElementsEqual(asciiIDNAIdentifier) {
return nil
}

Expand All @@ -474,7 +484,7 @@ private struct AnalysedCertificateHostname<
switch self.name {
case .singleName(let baseName):
// For non-wildcard names, we just do a straightforward comparison.
return baseName.elementsEqual(target.bytes)
return baseName.caseInsensitiveElementsEqual(target.bytes)

case .wildcard(let baseName, asteriskIndex: let asteriskIndex, firstPeriodIndex: let firstPeriodIndex):
// The wildcard can appear more-or-less anywhere in the first label. The wildcard
Expand All @@ -490,7 +500,7 @@ private struct AnalysedCertificateHostname<
let (wildcardLabel, remainingComponents) = baseName.splitAroundIndex(firstPeriodIndex)
let (targetFirstLabel, targetRemainingComponents) = target.bytes.splitAroundIndex(target.firstPeriodIndex)

guard remainingComponents.elementsEqual(targetRemainingComponents) else {
guard remainingComponents.caseInsensitiveElementsEqual(targetRemainingComponents) else {
// Wildcard is irrelevant, the remaining components don't match.
return false
}
Expand All @@ -504,8 +514,8 @@ private struct AnalysedCertificateHostname<
let targetBeforeWildcard = targetFirstLabel.prefix(wildcardLabelPrefix.count)
let targetAfterWildcard = targetFirstLabel.suffix(wildcardLabelSuffix.count)

let leadingBytesMatch = targetBeforeWildcard.elementsEqual(wildcardLabelPrefix)
let trailingBytesMatch = targetAfterWildcard.elementsEqual(wildcardLabelSuffix)
let leadingBytesMatch = targetBeforeWildcard.caseInsensitiveElementsEqual(wildcardLabelPrefix)
let trailingBytesMatch = targetAfterWildcard.caseInsensitiveElementsEqual(wildcardLabelSuffix)

return leadingBytesMatch && trailingBytesMatch
}
Expand Down
40 changes: 20 additions & 20 deletions Tests/X509Tests/ServerIdentityPolicyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,16 @@ private let unicodeCNName = try! DistinguishedName {
}

/// This cert contains the following SAN fields:
/// DNS:*.wildcard.example.com - A straightforward wildcard, should be accepted
/// DNS:fo*.example.com - A suffix wildcard, should be accepted
/// DNS:*ar.example.com - A prefix wildcard, should be accepted
/// DNS:b*z.example.com - An infix wildcard
/// DNS:trailing.period.example.com. - A domain with a trailing period, should match
/// DNS:xn--strae-oqa.unicode.example.com. - An IDN A-label, should match.
/// DNS:xn--x*-gia.unicode.example.com. - An IDN A-label with a wildcard, invalid.
/// DNS:weirdwildcard.*.example.com. - A wildcard not in the leftmost label, invalid.
/// DNS:*.*.double.example.com. - Two wildcards, invalid.
/// DNS:*.xn--strae-oqa.example.com. - A wildcard followed by a new IDN A-label, this is fine.
/// DNS:*.WILDCARD.EXAMPLE.com - A straightforward wildcard, should be accepted
/// DNS:FO*.EXAMPLE.com - A suffix wildcard, should be accepted
/// DNS:*AR.EXAMPLE.com - A prefix wildcard, should be accepted
/// DNS:B*Z.EXAMPLE.com - An infix wildcard
/// DNS:TRAILING.PERIOD.EXAMPLE.com. - A domain with a trailing period, should match
/// DNS:XN--STRAE-OQA.UNICODE.EXAMPLE.com. - An IDN A-label, should match.
/// DNS:XN--X*-GIA.UNICODE.EXAMPLE.com. - An IDN A-label with a wildcard, invalid.
/// DNS:WEIRDWILDCARD.*.EXAMPLE.com. - A wildcard not in the leftmost label, invalid.
/// DNS:*.*.DOUBLE.EXAMPLE.com. - Two wildcards, invalid.
/// DNS:*.XN--STRAE-OQA.EXAMPLE.com. - A wildcard followed by a new IDN A-label, this is fine.
/// A SAN with a null in it, should be ignored.
///
/// This also contains a commonName of httpbin.org.
Expand All @@ -67,34 +67,34 @@ private let weirdoSANCert = try! Certificate(

SubjectAlternativeNames([
// A straightforward wildcard, should be accepted
.dnsName("*.wildcard.example.com"),
.dnsName("*.WILDCARD.EXAMPLE.com"),

// A suffix wildcard, should be accepted
.dnsName("fo*.example.com"),
.dnsName("FO*.EXAMPLE.com"),

/// A prefix wildcard, should be accepted
.dnsName("*ar.example.com"),
.dnsName("*AR.EXAMPLE.com"),

/// An infix wildcard
.dnsName("b*z.example.com"),
.dnsName("B*Z.EXAMPLE.com"),

/// A domain with a trailing period, should match
.dnsName("trailing.period.example.com."),
.dnsName("TRAILING.PERIOD.EXAMPLE.com."),

/// An IDN A-label, should match.
.dnsName("xn--strae-oqa.unicode.example.com."),
.dnsName("XN--STRAE-OQA.UNICODE.EXAMPLE.com."),

/// An IDN A-label with a wildcard, invalid.
.dnsName("xn--x*-gia.unicode.example.com."),
.dnsName("XN--X*-GIA.UNICODE.EXAMPLE.com."),

/// A wildcard not in the leftmost label, invalid.
.dnsName("weirdwildcard.*.example.com."),
.dnsName("WEIRDWILDCARD.*.EXAMPLE.com."),

/// Two wildcards, invalid.
.dnsName("*.*.double.example.com."),
.dnsName("*.*.DOUBLE.EXAMPLE.com."),

/// A wildcard followed by a new IDN A-label, this is fine.
.dnsName("*.xn--strae-oqa.example.com."),
.dnsName("*.XN--STRAE-OQA.EXAMPLE.com."),

/// A SAN with a null in it, should be ignored.
.dnsName("\u{0000}"),
Expand Down

0 comments on commit 6f2dc4a

Please sign in to comment.