• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

randombit / botan / 15280040946

27 May 2025 03:45PM UTC coverage: 90.975% (-0.004%) from 90.979%
15280040946

push

github

web-flow
Merge pull request #4876 from KaganCanSit/todo-check-dns-label-length-in-parsing-cpp

[TODO] Add RFC 1035 Compliant DNS Tag Length Check

98024 of 107748 relevant lines covered (90.98%)

12543548.34 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.27
/src/lib/utils/parsing.cpp
1
/*
2
* Various string utils and parsing functions
3
* (C) 1999-2007,2013,2014,2015,2018 Jack Lloyd
4
* (C) 2015 Simon Warta (Kullo GmbH)
5
* (C) 2017 René Korthaus, Rohde & Schwarz Cybersecurity
6
*
7
* Botan is released under the Simplified BSD License (see license.txt)
8
*/
9

10
#include <botan/internal/parsing.h>
11

12
#include <botan/exceptn.h>
13
#include <botan/internal/fmt.h>
14
#include <botan/internal/loadstor.h>
15
#include <algorithm>
16
#include <cctype>
17
#include <limits>
18
#include <sstream>
19

20
namespace Botan {
21

22
uint16_t to_uint16(std::string_view str) {
391✔
23
   const uint32_t x = to_u32bit(str);
391✔
24

25
   if(x >> 16) {
391✔
26
      throw Invalid_Argument("Integer value exceeds 16 bit range");
×
27
   }
28

29
   return static_cast<uint16_t>(x);
391✔
30
}
31

32
uint32_t to_u32bit(std::string_view str_view) {
366,383✔
33
   const std::string str(str_view);
366,383✔
34

35
   // std::stoul is not strict enough. Ensure that str is digit only [0-9]*
36
   for(const char chr : str) {
1,131,257✔
37
      if(chr < '0' || chr > '9') {
765,457✔
38
         throw Invalid_Argument("to_u32bit invalid decimal string '" + str + "'");
1,749✔
39
      }
40
   }
41

42
   const unsigned long int x = std::stoul(str);
365,800✔
43

44
   if constexpr(sizeof(unsigned long int) > 4) {
365,800✔
45
      // x might be uint64
46
      if(x > std::numeric_limits<uint32_t>::max()) {
365,800✔
47
         throw Invalid_Argument("Integer value of " + str + " exceeds 32 bit range");
×
48
      }
49
   }
50

51
   return static_cast<uint32_t>(x);
365,800✔
52
}
366,383✔
53

54
/*
55
* Parse a SCAN-style algorithm name
56
*/
57
std::vector<std::string> parse_algorithm_name(std::string_view namex) {
21,647✔
58
   if(namex.find('(') == std::string::npos && namex.find(')') == std::string::npos) {
21,647✔
59
      return {std::string(namex)};
41,244✔
60
   }
61

62
   std::string name(namex);
1,025✔
63
   std::string substring;
1,025✔
64
   std::vector<std::string> elems;
1,025✔
65
   size_t level = 0;
1,025✔
66

67
   elems.push_back(name.substr(0, name.find('(')));
2,050✔
68
   name = name.substr(name.find('('));
1,025✔
69

70
   for(auto i = name.begin(); i != name.end(); ++i) {
4,187✔
71
      char c = *i;
4,187✔
72

73
      if(c == '(') {
4,187✔
74
         ++level;
1,025✔
75
      }
76
      if(c == ')') {
4,187✔
77
         if(level == 1 && i == name.end() - 1) {
1,025✔
78
            if(elems.size() == 1) {
1,025✔
79
               elems.push_back(substring.substr(1));
1,630✔
80
            } else {
81
               elems.push_back(substring);
210✔
82
            }
83
            return elems;
1,025✔
84
         }
85

86
         if(level == 0 || (level == 1 && i != name.end() - 1)) {
×
87
            throw Invalid_Algorithm_Name(namex);
×
88
         }
89
         --level;
×
90
      }
91

92
      if(c == ',' && level == 1) {
3,162✔
93
         if(elems.size() == 1) {
210✔
94
            elems.push_back(substring.substr(1));
420✔
95
         } else {
96
            elems.push_back(substring);
×
97
         }
98
         substring.clear();
210✔
99
      } else {
100
         substring += c;
6,114✔
101
      }
102
   }
103

104
   if(!substring.empty()) {
×
105
      throw Invalid_Algorithm_Name(namex);
×
106
   }
107

108
   return elems;
×
109
}
42,269✔
110

111
std::vector<std::string> split_on(std::string_view str, char delim) {
215,347✔
112
   std::vector<std::string> elems;
215,347✔
113
   if(str.empty()) {
215,347✔
114
      return elems;
115
   }
116

117
   std::string substr;
215,231✔
118
   for(auto i = str.begin(); i != str.end(); ++i) {
2,796,821✔
119
      if(*i == delim) {
2,581,590✔
120
         if(!substr.empty()) {
171,896✔
121
            elems.push_back(substr);
171,854✔
122
         }
123
         substr.clear();
171,896✔
124
      } else {
125
         substr += *i;
4,991,284✔
126
      }
127
   }
128

129
   if(substr.empty()) {
215,231✔
130
      throw Invalid_Argument(fmt("Unable to split string '{}", str));
2✔
131
   }
132
   elems.push_back(substr);
215,230✔
133

134
   return elems;
215,230✔
135
}
215,231✔
136

137
/*
138
* Join a string
139
*/
140
std::string string_join(const std::vector<std::string>& strs, char delim) {
19✔
141
   std::ostringstream out;
19✔
142

143
   for(size_t i = 0; i != strs.size(); ++i) {
237✔
144
      if(i != 0) {
218✔
145
         out << delim;
199✔
146
      }
147
      out << strs[i];
218✔
148
   }
149

150
   return out.str();
38✔
151
}
19✔
152

153
/*
154
* Convert a decimal-dotted string to binary IP
155
*/
156
std::optional<uint32_t> string_to_ipv4(std::string_view str) {
4,847✔
157
   // At least 3 dots + 4 1-digit integers
158
   // At most 3 dots + 4 3-digit integers
159
   if(str.size() < 3 + 4 * 1 || str.size() > 3 + 4 * 3) {
4,847✔
160
      return {};
1,242✔
161
   }
162

163
   // the final result
164
   uint32_t ip = 0;
3,605✔
165
   // the number of '.' seen so far
166
   size_t dots = 0;
3,605✔
167
   // accumulates one quad (range 0-255)
168
   uint32_t accum = 0;
3,605✔
169
   // # of digits pushed to accum since last dot
170
   size_t cur_digits = 0;
3,605✔
171

172
   for(char c : str) {
3,840✔
173
      if(c == '.') {
3,826✔
174
         // . without preceding digit is invalid
175
         if(cur_digits == 0) {
87✔
176
            return {};
4✔
177
         }
178
         dots += 1;
83✔
179
         // too many dots
180
         if(dots > 3) {
83✔
181
            return {};
6✔
182
         }
183

184
         cur_digits = 0;
77✔
185
         ip = (ip << 8) | accum;
77✔
186
         accum = 0;
77✔
187
      } else if(c >= '0' && c <= '9') {
3,739✔
188
         const auto d = static_cast<uint8_t>(c - '0');
169✔
189

190
         // prohibit leading zero in quad (used for octal)
191
         if(cur_digits > 0 && accum == 0) {
169✔
192
            return {};
7✔
193
         }
194
         accum = (accum * 10) + d;
162✔
195

196
         if(accum > 255) {
162✔
197
            return {};
4✔
198
         }
199

200
         cur_digits++;
158✔
201
         BOTAN_ASSERT_NOMSG(cur_digits <= 3);
158✔
202
      } else {
203
         return {};
3,570✔
204
      }
205
   }
206

207
   // no trailing digits?
208
   if(cur_digits == 0) {
14✔
209
      return {};
×
210
   }
211

212
   // insufficient # of dots
213
   if(dots != 3) {
14✔
214
      return {};
×
215
   }
216

217
   ip = (ip << 8) | accum;
14✔
218

219
   return ip;
14✔
220
}
221

222
/*
223
* Convert an IP address to decimal-dotted string
224
*/
225
std::string ipv4_to_string(uint32_t ip) {
14✔
226
   uint8_t bits[4];
14✔
227
   store_be(ip, bits);
14✔
228

229
   std::string str;
14✔
230

231
   for(size_t i = 0; i != 4; ++i) {
70✔
232
      if(i > 0) {
56✔
233
         str += ".";
42✔
234
      }
235
      str += std::to_string(bits[i]);
112✔
236
   }
237

238
   return str;
14✔
239
}
×
240

241
std::string tolower_string(std::string_view in) {
2,205✔
242
   std::string s(in);
2,205✔
243
   for(size_t i = 0; i != s.size(); ++i) {
39,992✔
244
      const int cu = static_cast<unsigned char>(s[i]);
37,787✔
245
      if(std::isalpha(cu)) {
37,787✔
246
         s[i] = static_cast<char>(std::tolower(cu));
33,269✔
247
      }
248
   }
249
   return s;
2,205✔
250
}
251

252
bool host_wildcard_match(std::string_view issued_, std::string_view host_) {
217✔
253
   const std::string issued = tolower_string(issued_);
217✔
254
   const std::string host = tolower_string(host_);
217✔
255

256
   if(host.empty() || issued.empty()) {
217✔
257
      return false;
258
   }
259

260
   /*
261
   If there are embedded nulls in your issued name
262
   Well I feel bad for you son
263
   */
264
   if(std::count(issued.begin(), issued.end(), char(0)) > 0) {
428✔
265
      return false;
266
   }
267

268
   // If more than one wildcard, then issued name is invalid
269
   const size_t stars = std::count(issued.begin(), issued.end(), '*');
214✔
270
   if(stars > 1) {
214✔
271
      return false;
272
   }
273

274
   // '*' is not a valid character in DNS names so should not appear on the host side
275
   if(std::count(host.begin(), host.end(), '*') != 0) {
420✔
276
      return false;
277
   }
278

279
   // Similarly a DNS name can't end in .
280
   if(host[host.size() - 1] == '.') {
207✔
281
      return false;
282
   }
283

284
   // And a host can't have an empty name component, so reject that
285
   if(host.find("..") != std::string::npos) {
207✔
286
      return false;
287
   }
288

289
   // Exact match: accept
290
   if(issued == host) {
205✔
291
      return true;
292
   }
293

294
   /*
295
   Otherwise it might be a wildcard
296

297
   If the issued size is strictly longer than the hostname size it
298
   couldn't possibly be a match, even if the issued value is a
299
   wildcard. The only exception is when the wildcard ends up empty
300
   (eg www.example.com matches www*.example.com)
301
   */
302
   if(issued.size() > host.size() + 1) {
48✔
303
      return false;
304
   }
305

306
   // If no * at all then not a wildcard, and so not a match
307
   if(stars != 1) {
39✔
308
      return false;
309
   }
310

311
   /*
312
   Now walk through the issued string, making sure every character
313
   matches. When we come to the (singular) '*', jump forward in the
314
   hostname by the corresponding amount. We know exactly how much
315
   space the wildcard takes because it must be exactly `len(host) -
316
   len(issued) + 1 chars`.
317

318
   We also verify that the '*' comes in the leftmost component, and
319
   doesn't skip over any '.' in the hostname.
320
   */
321
   size_t dots_seen = 0;
322
   size_t host_idx = 0;
323

324
   for(size_t i = 0; i != issued.size(); ++i) {
291✔
325
      dots_seen += (issued[i] == '.');
275✔
326

327
      if(issued[i] == '*') {
275✔
328
         // Fail: wildcard can only come in leftmost component
329
         if(dots_seen > 0) {
30✔
330
            return false;
331
         }
332

333
         /*
334
         Since there is only one * we know the tail of the issued and
335
         hostname must be an exact match. In this case advance host_idx
336
         to match.
337
         */
338
         const size_t advance = (host.size() - issued.size() + 1);
23✔
339

340
         if(host_idx + advance > host.size()) {  // shouldn't happen
23✔
341
            return false;
342
         }
343

344
         // Can't be any intervening .s that we would have skipped
345
         if(std::count(host.begin() + host_idx, host.begin() + host_idx + advance, '.') != 0) {
46✔
346
            return false;
347
         }
348

349
         host_idx += advance;
350
      } else {
351
         if(issued[i] != host[host_idx]) {
245✔
352
            return false;
353
         }
354

355
         host_idx += 1;
242✔
356
      }
357
   }
358

359
   // Wildcard issued name must have at least 3 components
360
   if(dots_seen < 2) {
16✔
361
      return false;
362
   }
363

364
   return true;
365
}
217✔
366

367
std::string check_and_canonicalize_dns_name(std::string_view name) {
10,391✔
368
   if(name.size() > 255) {
10,391✔
369
      throw Decoding_Error("DNS name exceeds maximum allowed length");
88✔
370
   }
371

372
   if(name.empty()) {
10,303✔
373
      throw Decoding_Error("DNS name cannot be empty");
5✔
374
   }
375

376
   if(name.starts_with(".") || name.ends_with(".")) {
10,298✔
377
      throw Decoding_Error("DNS name cannot start or end with a dot");
7✔
378
   }
379

380
   /*
381
   * Table mapping uppercase to lowercase and only including values for valid DNS names
382
   * namely A-Z, a-z, 0-9, hyphen, and dot, plus '*' for wildcarding. (RFC 1035)
383
   */
384
   // clang-format off
385
   constexpr uint8_t DNS_CHAR_MAPPING[128] = {
10,291✔
386
      '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0',
387
      '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0',
388
      '\0', '\0', '\0', '\0',  '*', '\0', '\0',  '-',  '.', '\0',  '0',  '1',  '2',  '3',  '4',  '5',  '6',  '7',  '8',
389
       '9', '\0', '\0', '\0', '\0', '\0', '\0', '\0',  'a',  'b',  'c',  'd',  'e',  'f',  'g',  'h',  'i',  'j',  'k',
390
       'l',  'm',  'n',  'o',  'p',  'q',  'r',  's',  't',  'u',  'v',  'w',  'x',  'y',  'z', '\0', '\0', '\0', '\0',
391
      '\0', '\0',  'a',  'b',  'c',  'd',  'e',  'f',  'g',  'h',  'i',  'j',  'k',  'l',  'm',  'n',  'o',  'p',  'q',
392
       'r',  's',  't',  'u',  'v',  'w',  'x',  'y',  'z', '\0', '\0', '\0', '\0', '\0',
393
   };
394
   // clang-format on
395

396
   std::string canon;
10,291✔
397
   canon.reserve(name.size());
10,291✔
398

399
   // RFC 1035: DNS labels must not exceed 63 characters
400
   size_t current_label_length = 0;
401

402
   for(size_t i = 0; i != name.size(); ++i) {
123,089✔
403
      char c = name[i];
113,226✔
404

405
      if(c == '.') {
113,226✔
406
         if(i > 0 && name[i - 1] == '.') {
8,796✔
407
            throw Decoding_Error("DNS name contains sequential period chars");
4✔
408
         }
409

410
         if(current_label_length == 0) {
8,792✔
411
            throw Decoding_Error("DNS name contains empty label");
×
412
         }
413
         current_label_length = 0;  // Reset for next label
414
      } else {
415
         current_label_length++;
104,430✔
416

417
         if(current_label_length > 63) {  // RFC 1035 Maximum DNS label length
104,430✔
418
            throw Decoding_Error("DNS name label exceeds maximum length of 63 characters");
1✔
419
         }
420
      }
421

422
      const uint8_t cu = static_cast<uint8_t>(c);
113,221✔
423
      if(cu >= 128) {
113,221✔
424
         throw Decoding_Error("DNS name must not contain any extended ASCII code points");
151✔
425
      }
426
      const uint8_t mapped = DNS_CHAR_MAPPING[cu];
113,070✔
427
      if(mapped == 0) {
113,070✔
428
         throw Decoding_Error("DNS name includes invalid character");
266✔
429
      }
430

431
      if(mapped == '-') {
112,804✔
432
         if(i == 0 || (i > 0 && name[i - 1] == '.')) {
661✔
433
            throw Decoding_Error("DNS name has label with leading hyphen");
3✔
434
         } else if(i == name.size() - 1 || (i < name.size() - 1 && name[i + 1] == '.')) {
658✔
435
            throw Decoding_Error("DNS name has label with trailing hyphen");
3✔
436
         }
437
      }
438
      canon.push_back(static_cast<char>(mapped));
112,798✔
439
   }
440

441
   if(current_label_length == 0) {
9,863✔
442
      throw Decoding_Error("DNS name contains empty label");
×
443
   }
444
   return canon;
9,863✔
445
}
428✔
446

447
}  // namespace Botan
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc