Botan 3.13.0
Crypto and TLS for C&
Botan::DNSName Class Referencefinal

#include <dns_name.h>

Public Member Functions

bool is_wildcard () const
bool matches_wildcard (std::string_view wildcard) const
const std::string & name () const
auto operator<=> (const DNSName &) const =default
bool operator== (const DNSName &) const =default
const std::string & to_string () const

Static Public Member Functions

static std::optional< DNSName > from_san_string (std::string_view name)
static std::optional< DNSName > from_string (std::string_view name)
static bool host_wildcard_match (std::string_view issued, std::string_view host)

Detailed Description

A DNS name (host name or wildcard pattern) in canonical form.

Construction validates that the input conforms to the Preferred Name Syntax (RFC 1035 / RFC 1123 LDH labels, length limits, no leading or trailing dot). Entirely numeric names ("1.2.3.4") are rejected. The stored form is lowercased ASCII.

Definition at line 25 of file dns_name.h.

Member Function Documentation

◆ from_san_string()

std::optional< DNSName > Botan::DNSName::from_san_string ( std::string_view name)
static

Like from_string, but additionally accepts the RFC 6125 6.4.3 wildcard form: a single "*" anywhere within the leftmost label of an otherwise-valid DNS name (e.g. "*.example.com", "foo*.example.com"). Shapes that could never produce a match - multiple "*" ("*.*.example.com"), "*" outside the leftmost label ("foo.*.example.com"), or patterns with fewer than three labels ("*", "*.com") - are rejected, as are wildcards embedded within an IDNA A-label ("xn--f*.example.com"). Intended for parsing X.509 SAN dnsName entries.

Definition at line 149 of file dns_name.cpp.

149 {
150 if(auto canon = check_and_canonicalize_dns_name(name)) {
151 /*
152 Validate the wildcard shape: at most one "*", and if present it must be in
153 the leftmost label (no "." before it). This matches the RFC 6125 6.4.3
154 form that host_wildcard_match accepts and rejects eg "*.*.example.com" or
155 "foo.*.example.com"
156 */
157 const auto first_star = canon->find('*');
158 if(first_star != std::string::npos) {
159 if(canon->find('*', first_star + 1) != std::string::npos) {
160 return std::nullopt;
161 }
162 const auto first_dot = canon->find('.');
163 if(first_dot != std::string::npos && first_dot < first_star) {
164 return std::nullopt;
165 }
166 /*
167 RFC 6125 6.4.3: "the client SHOULD NOT attempt to match a presented
168 identifier where the wildcard character is embedded within an
169 A-label or U-label"
170 */
171 if(canon->starts_with("xn--")) {
172 return std::nullopt;
173 }
174 // A wildcard match requires at least three labels, so shorter
175 // patterns ("*", "*.com") could never match any host
176 if(std::count(canon->begin(), canon->end(), '.') < 2) {
177 return std::nullopt;
178 }
179 }
180 return DNSName(std::move(*canon));
181 } else {
182 return {};
183 }
184}
const std::string & name() const
Definition dns_name.h:57

References name().

Referenced by Botan::AlternativeName::add_dns(), Botan::NameConstraints::is_excluded(), Botan::NameConstraints::is_permitted(), Botan::GeneralName::matches(), and Botan::X509_Certificate::matches_dns_name().

◆ from_string()

std::optional< DNSName > Botan::DNSName::from_string ( std::string_view name)
static

Parse and canonicalize a literal hostname. Returns nullopt if the input is not a valid DNS name per RFC 1035 / 1123, or if it contains a "*" label (use from_san_string for that).

Definition at line 136 of file dns_name.cpp.

136 {
137 if(auto canon = check_and_canonicalize_dns_name(name)) {
138 // TODO(C++23) std::string::contains
139 if(canon->find('*') != std::string::npos) {
140 return {};
141 }
142 return DNSName(std::move(*canon));
143 } else {
144 return {};
145 }
146}

References name().

Referenced by Botan::URI::Authority::from_string(), Botan::TLS::Server_Name_Indicator::hostname_acceptable_for_sni(), and Botan::X509_Certificate::matches_dns_name().

◆ host_wildcard_match()

bool Botan::DNSName::host_wildcard_match ( std::string_view issued,
std::string_view host )
static

Test if the issued name (which might be a wildcard pattern) can match the host, which should be a complete and valid DNS name.

Returns false if either the pattern or the host seem invalid

Definition at line 191 of file dns_name.cpp.

191 {
192 if(host.empty() || issued.empty()) {
193 return false;
194 }
195
196 // Maximum valid DNS name
197 if(host.size() > 253) {
198 return false;
199 }
200
201 /*
202 The wildcard if existing absorbs (host.size() - issued.size() + 1) chars,
203 which must be non-negative. So issued cannot possibly exceed host.size() + 1.
204 */
205 if(issued.size() > host.size() + 1) {
206 return false;
207 }
208
209 /*
210 If there are embedded nulls in your issued name
211 Well I feel bad for you son
212 */
213 if(issued.find('\0') != std::string_view::npos || host.find('\0') != std::string_view::npos) {
214 return false;
215 }
216
217 // '*' is not a valid character in DNS names so should not appear on the host side
218 if(host.find('*') != std::string_view::npos) {
219 return false;
220 }
221
222 // Similarly a DNS name can't end in .
223 if(host.back() == '.') {
224 return false;
225 }
226
227 // Nor can it start with one
228 if(host.front() == '.') {
229 return false;
230 }
231
232 // And a host can't have an empty name component, so reject that
233 if(host.find("..") != std::string_view::npos) {
234 return false;
235 }
236
237 // ASCII-only case-insensitive char equality, avoids locale overhead from tolower
238 auto dns_char_eq = [](char a, char b) -> bool {
239 if(a == b) {
240 return true;
241 }
242 const auto la = static_cast<unsigned char>(a | 0x20);
243 const auto lb = static_cast<unsigned char>(b | 0x20);
244 return la == lb && la >= 'a' && la <= 'z';
245 };
246
247 auto dns_char_eq_range = [&](std::string_view a, std::string_view b) -> bool {
248 if(a.size() != b.size()) {
249 return false;
250 }
251 for(size_t i = 0; i != a.size(); ++i) {
252 if(!dns_char_eq(a[i], b[i])) {
253 return false;
254 }
255 }
256 return true;
257 };
258
259 // Exact match: accept
260 if(dns_char_eq_range(issued, host)) {
261 return true;
262 }
263
264 // First detect offset of wildcard '*' if included
265 const size_t first_star = issued.find('*');
266 const bool has_wildcard = (first_star != std::string_view::npos);
267
268 // At most one wildcard is allowed
269 if(has_wildcard && issued.find('*', first_star + 1) != std::string_view::npos) {
270 return false;
271 }
272
273 // If no * at all then not a wildcard, and so not a match
274 if(!has_wildcard) {
275 return false;
276 }
277
278 /*
279 RFC 6125 6.4.3: "the client SHOULD NOT attempt to match a presented
280 identifier where the wildcard character is embedded within an
281 A-label or U-label"
282
283 The host side check rejects a partial wildcard absorbing part of an
284 A-label of the host, which would otherwise allow the same confusion.
285 */
286 const auto is_idna_prefixed = [&](std::string_view label) {
287 return label.size() >= 4 && dns_char_eq_range(label.substr(0, 4), "xn--");
288 };
289 const auto issued_label = issued.substr(0, issued.find('.'));
290 if(is_idna_prefixed(issued_label)) {
291 return false;
292 }
293 if(issued_label != "*" && is_idna_prefixed(host.substr(0, host.find('.')))) {
294 return false;
295 }
296
297 /*
298 Now walk through the issued string, making sure every character
299 matches. When we come to the (singular) '*', jump forward in the
300 hostname by the corresponding amount. We know exactly how much
301 space the wildcard takes because it must be exactly `len(host) -
302 len(issued) + 1 chars`.
303
304 We also verify that the '*' comes in the leftmost component, and
305 doesn't skip over any '.' in the hostname.
306 */
307 size_t dots_seen = 0;
308 size_t host_idx = 0;
309
310 for(size_t i = 0; i != issued.size(); ++i) {
311 if(issued[i] == '.') {
312 dots_seen += 1;
313 }
314
315 if(issued[i] == '*') {
316 // Fail: wildcard can only come in leftmost component
317 if(dots_seen > 0) {
318 return false;
319 }
320
321 /*
322 Since there is only one * we know the tail of the issued and
323 hostname must be an exact match. In this case advance host_idx
324 to match.
325 */
326 const size_t advance = (host.size() - issued.size() + 1);
327
328 if(host_idx + advance > host.size()) { // shouldn't happen
329 return false;
330 }
331
332 // Can't be any intervening .s that we would have skipped
333 for(size_t k = host_idx; k != host_idx + advance; ++k) {
334 if(host[k] == '.') {
335 return false;
336 }
337 }
338
339 host_idx += advance;
340 } else {
341 if(!dns_char_eq(issued[i], host[host_idx])) {
342 return false;
343 }
344
345 host_idx += 1;
346 }
347 }
348
349 // Wildcard issued name must have at least 3 components
350 if(dots_seen < 2) {
351 return false;
352 }
353
354 return true;
355}

Referenced by matches_wildcard(), and operator==().

◆ is_wildcard()

bool Botan::DNSName::is_wildcard ( ) const
inline

True if this name is a wildcard pattern: a single "*" somewhere in the leftmost label, per RFC 6125 6.4.3 (which permits in-label partial wildcards like "foo*.example.com" as well as the complete-leftmost-label "*.example.com" form). Shapes outside this form - multiple "*" or "*" not in the leftmost label - are rejected at construction by from_san_string, so any stored "*" is already in the leftmost label.

TODO(Botan4) when RFC 9525 wildcards are used, this fn can change to just looking at the first character of m_name.

Definition at line 71 of file dns_name.h.

71{ return m_name.find('*') != std::string::npos; }

◆ matches_wildcard()

bool Botan::DNSName::matches_wildcard ( std::string_view wildcard) const

Test whether this name matches a wildcard pattern (e.g. "*.example.com"). The wildcard label must be the leftmost label. Comparison is case-insensitive.

Definition at line 186 of file dns_name.cpp.

186 {
187 return host_wildcard_match(wildcard, m_name);
188}
static bool host_wildcard_match(std::string_view issued, std::string_view host)
Definition dns_name.cpp:191

References host_wildcard_match().

Referenced by Botan::X509_Certificate::matches_dns_name().

◆ name()

const std::string & Botan::DNSName::name ( ) const
inline

Access the canonicalized name

Returns
the lowercased ASCII form of the name

Definition at line 57 of file dns_name.h.

57{ return m_name; }

Referenced by from_san_string(), and from_string().

◆ operator<=>()

auto Botan::DNSName::operator<=> ( const DNSName & ) const
default

Order two names by their canonicalized form

Returns
the ordering of this name relative to the other

◆ operator==()

bool Botan::DNSName::operator== ( const DNSName & ) const
default

Compare two names by their canonicalized form

Returns
true if the two names are equal

References host_wildcard_match().

◆ to_string()

const std::string & Botan::DNSName::to_string ( ) const
inline

Access the canonicalized name

Returns
the lowercased ASCII form of the name

Definition at line 51 of file dns_name.h.

51{ return m_name; }

Referenced by Botan::GeneralName::matches_dns(), and Botan::GeneralName::matches_email().


The documentation for this class was generated from the following files: