1 /* $NetBSD: midna_domain.c,v 1.6 2026/05/09 18:49:22 christos Exp $ */ 2 3 /*++ 4 /* NAME 5 /* midna_domain 3 6 /* SUMMARY 7 /* ASCII/UTF-8 domain name conversion 8 /* SYNOPSIS 9 /* #include <midna_domain.h> 10 /* 11 /* int midna_domain_cache_size; 12 /* int midna_domain_transitional; 13 /* 14 /* const char *midna_domain_to_ascii( 15 /* const char *name) 16 /* 17 /* const char *midna_domain_to_utf8( 18 /* const char *name) 19 /* 20 /* const char *midna_domain_suffix_to_ascii( 21 /* const char *name) 22 /* 23 /* const char *midna_domain_suffix_to_utf8( 24 /* const char *name) 25 /* AUXILIARY FUNCTIONS 26 /* void midna_domain_pre_chroot(void) 27 /* DESCRIPTION 28 /* The functions in this module transform domain names from/to 29 /* ASCII and UTF-8 form. The result is cached to avoid repeated 30 /* conversion. 31 /* 32 /* This module builds on the ICU library implementation of the 33 /* UTS #46 specification, using default ICU library options 34 /* because those are likely best tested: with transitional 35 /* processing, with case mapping, with normalization, with 36 /* limited IDNA2003 compatibility, without STD3 ASCII rules. 37 /* 38 /* midna_domain_to_ascii() converts an UTF-8 or ASCII domain 39 /* name to ASCII. The result is a null pointer in case of 40 /* error. This function verifies that the result passes 41 /* valid_hostname(). 42 /* 43 /* midna_domain_to_utf8() converts an UTF-8 or ASCII domain 44 /* name to UTF-8. The result is a null pointer in case of 45 /* error. This function verifies that the result, after 46 /* conversion to ASCII, passes valid_hostname(). 47 /* 48 /* midna_domain_suffix_to_ascii() and midna_domain_suffix_to_utf8() 49 /* take a name that starts with '.' and otherwise perform the 50 /* same operations as midna_domain_to_ascii() and 51 /* midna_domain_to_utf8(). 52 /* 53 /* midna_domain_cache_size specifies the size of the conversion 54 /* result cache. This value is used only once, upon the first 55 /* lookup request. 56 /* 57 /* midna_domain_transitional enables transitional conversion 58 /* between UTF8 and ASCII labels. 59 /* 60 /* midna_domain_pre_chroot() does some pre-chroot initialization. 61 /* SEE ALSO 62 /* https://unicode.org/reports/tr46/ Unicode IDNA Compatibility processing 63 /* msg(3) diagnostics interface 64 /* DIAGNOSTICS 65 /* Fatal errors: memory allocation problem. 66 /* Warnings: conversion error or result validation error. 67 /* LICENSE 68 /* .ad 69 /* .fi 70 /* The Secure Mailer license must be distributed with this software. 71 /* AUTHOR(S) 72 /* Arnt Gulbrandsen 73 /* 74 /* Wietse Venema 75 /* IBM T.J. Watson Research 76 /* P.O. Box 704 77 /* Yorktown Heights, NY 10598, USA 78 /* 79 /* Wietse Venema 80 /* Google, Inc. 81 /* 111 8th Avenue 82 /* New York, NY 10011, USA 83 /*--*/ 84 85 /* 86 * System library. 87 */ 88 #include <sys_defs.h> 89 #include <string.h> 90 #include <ctype.h> 91 92 #ifndef NO_EAI 93 #include <unicode/uidna.h> 94 95 /* 96 * Utility library. 97 */ 98 #include <mymalloc.h> 99 #include <msg.h> 100 #include <ctable.h> 101 #include <stringops.h> 102 #include <valid_hostname.h> 103 #include <name_mask.h> 104 #include <midna_domain.h> 105 106 /* 107 * Application-specific. 108 */ 109 #define DEF_MIDNA_CACHE_SIZE 256 110 111 int midna_domain_cache_size = DEF_MIDNA_CACHE_SIZE; 112 int midna_domain_transitional = 0; 113 static VSTRING *midna_domain_buf; /* x.suffix */ 114 115 #define STR(x) vstring_str(x) 116 117 /* midna_domain_strerror - pick one for error reporting */ 118 119 static const char *midna_domain_strerror(UErrorCode error, int info_errors) 120 { 121 122 /* 123 * XXX The UIDNA_ERROR_EMPTY_LABEL etc. names are defined in an ENUM, so 124 * we can't use #ifdef to dynamically determine which names exist. 125 */ 126 static LONG_NAME_MASK uidna_errors[] = { 127 "UIDNA_ERROR_EMPTY_LABEL", UIDNA_ERROR_EMPTY_LABEL, 128 "UIDNA_ERROR_LABEL_TOO_LONG", UIDNA_ERROR_LABEL_TOO_LONG, 129 "UIDNA_ERROR_DOMAIN_NAME_TOO_LONG", UIDNA_ERROR_DOMAIN_NAME_TOO_LONG, 130 "UIDNA_ERROR_LEADING_HYPHEN", UIDNA_ERROR_LEADING_HYPHEN, 131 "UIDNA_ERROR_TRAILING_HYPHEN", UIDNA_ERROR_TRAILING_HYPHEN, 132 "UIDNA_ERROR_HYPHEN_3_4", UIDNA_ERROR_HYPHEN_3_4, 133 "UIDNA_ERROR_LEADING_COMBINING_MARK", UIDNA_ERROR_LEADING_COMBINING_MARK, 134 "UIDNA_ERROR_DISALLOWED", UIDNA_ERROR_DISALLOWED, 135 "UIDNA_ERROR_PUNYCODE", UIDNA_ERROR_PUNYCODE, 136 "UIDNA_ERROR_LABEL_HAS_DOT", UIDNA_ERROR_LABEL_HAS_DOT, 137 "UIDNA_ERROR_INVALID_ACE_LABEL", UIDNA_ERROR_INVALID_ACE_LABEL, 138 "UIDNA_ERROR_BIDI", UIDNA_ERROR_BIDI, 139 "UIDNA_ERROR_CONTEXTJ", UIDNA_ERROR_CONTEXTJ, 140 /* The above errors are defined with ICU 46 and later. */ 141 0, 142 }; 143 144 if (info_errors) { 145 return (str_long_name_mask_opt((VSTRING *) 0, "idna error", 146 uidna_errors, info_errors, 147 NAME_MASK_NUMBER | NAME_MASK_COMMA)); 148 } else { 149 return u_errorName(error); 150 } 151 } 152 153 /* midna_domain_pre_chroot - pre-chroot initialization */ 154 155 void midna_domain_pre_chroot(void) 156 { 157 UErrorCode error = U_ZERO_ERROR; 158 UIDNAInfo info = UIDNA_INFO_INITIALIZER; 159 UIDNA *idna; 160 161 idna = uidna_openUTS46(midna_domain_transitional ? UIDNA_DEFAULT 162 : UIDNA_NONTRANSITIONAL_TO_ASCII, &error); 163 if (U_FAILURE(error)) 164 msg_warn("ICU library initialization failed: %s", 165 midna_domain_strerror(error, info.errors)); 166 uidna_close(idna); 167 } 168 169 /* midna_domain_to_ascii_create - convert domain to ASCII */ 170 171 static void *midna_domain_to_ascii_create(const char *name, void *unused_context) 172 { 173 static const char myname[] = "midna_domain_to_ascii_create"; 174 char buf[1024]; /* XXX */ 175 UErrorCode error = U_ZERO_ERROR; 176 UIDNAInfo info = UIDNA_INFO_INITIALIZER; 177 UIDNA *idna; 178 int anl; 179 180 /* 181 * Paranoia: do not expose uidna_*() to unfiltered network data. 182 */ 183 if (allascii(name) == 0 && valid_utf8_stringz(name) == 0) { 184 msg_warn("%s: Problem translating domain \"%.100s\" to ASCII form: %s", 185 myname, name, "malformed UTF-8"); 186 return (0); 187 } 188 189 /* 190 * Perform the requested conversion. 191 */ 192 idna = uidna_openUTS46(midna_domain_transitional ? UIDNA_DEFAULT 193 : UIDNA_NONTRANSITIONAL_TO_ASCII, &error); 194 /* 202604 Claude: avoid null deref after uidna_openUTS46() failure. */ 195 if (idna && U_SUCCESS(error)) 196 anl = uidna_nameToASCII_UTF8(idna, 197 name, strlen(name), 198 buf, sizeof(buf) - 1, 199 &info, 200 &error); 201 uidna_close(idna); 202 203 /* 204 * Paranoia: verify that the result passes valid_hostname(). A quick 205 * check shows that UTS46 ToASCII by default rejects inputs with labels 206 * that start or end in '-', with names or labels that are over-long, or 207 * "fake" A-labels, as required by UTS 46 section 4.1, but we rely on 208 * valid_hostname() on the output side just to be sure. 209 */ 210 if (idna && U_SUCCESS(error) && info.errors == 0 && anl > 0) { 211 buf[anl] = 0; /* XXX */ 212 if (!valid_hostname(buf, DONT_GRIPE)) { 213 msg_warn("%s: Problem translating domain \"%.100s\" to ASCII form: %s", 214 myname, name, "malformed ASCII label(s)"); 215 return (0); 216 } 217 return (mystrndup(buf, anl)); 218 } else { 219 msg_warn("%s: Problem translating domain \"%.100s\" to ASCII form: %s", 220 myname, name, midna_domain_strerror(error, info.errors)); 221 return (0); 222 } 223 } 224 225 /* midna_domain_to_utf8_create - convert domain to UTF8 */ 226 227 static void *midna_domain_to_utf8_create(const char *name, void *unused_context) 228 { 229 static const char myname[] = "midna_domain_to_utf8_create"; 230 char buf[1024]; /* XXX */ 231 UErrorCode error = U_ZERO_ERROR; 232 UIDNAInfo info = UIDNA_INFO_INITIALIZER; 233 UIDNA *idna; 234 int anl; 235 236 /* 237 * Paranoia: do not expose uidna_*() to unfiltered network data. 238 */ 239 if (allascii(name) == 0 && valid_utf8_stringz(name) == 0) { 240 msg_warn("%s: Problem translating domain \"%.100s\" to UTF-8 form: %s", 241 myname, name, "malformed UTF-8"); 242 return (0); 243 } 244 245 /* 246 * Perform the requested conversion. 247 */ 248 idna = uidna_openUTS46(midna_domain_transitional ? UIDNA_DEFAULT 249 : UIDNA_NONTRANSITIONAL_TO_UNICODE, &error); 250 /* 202604 Claude: avoid null deref after uidna_openUTS46() failure. */ 251 if (idna && U_SUCCESS(error)) 252 anl = uidna_nameToUnicodeUTF8(idna, 253 name, strlen(name), 254 buf, sizeof(buf) - 1, 255 &info, 256 &error); 257 uidna_close(idna); 258 259 /* 260 * Paranoia: UTS46 toUTF8 by default accepts and produces an over-long 261 * name or a name that contains an over-long NR-LDH label (and perhaps 262 * other invalid forms that are not covered in UTS 46, section 4.1). We 263 * rely on midna_domain_to_ascii() to validate the output. 264 */ 265 if (idna && U_SUCCESS(error) && info.errors == 0 && anl > 0) { 266 buf[anl] = 0; /* XXX */ 267 if (midna_domain_to_ascii(buf) == 0) 268 return (0); 269 return (mystrndup(buf, anl)); 270 } else { 271 msg_warn("%s: Problem translating domain \"%.100s\" to UTF8 form: %s", 272 myname, name, midna_domain_strerror(error, info.errors)); 273 return (0); 274 } 275 } 276 277 /* midna_domain_cache_free - cache element destructor */ 278 279 static void midna_domain_cache_free(void *value, void *unused_context) 280 { 281 if (value) 282 myfree(value); 283 } 284 285 /* midna_domain_to_ascii - convert name to ASCII */ 286 287 const char *midna_domain_to_ascii(const char *name) 288 { 289 static CTABLE *midna_domain_to_ascii_cache = 0; 290 291 if (midna_domain_to_ascii_cache == 0) 292 midna_domain_to_ascii_cache = ctable_create(midna_domain_cache_size, 293 midna_domain_to_ascii_create, 294 midna_domain_cache_free, 295 (void *) 0); 296 return (ctable_locate(midna_domain_to_ascii_cache, name)); 297 } 298 299 /* midna_domain_to_utf8 - convert name to UTF8 */ 300 301 const char *midna_domain_to_utf8(const char *name) 302 { 303 static CTABLE *midna_domain_to_utf8_cache = 0; 304 305 if (midna_domain_to_utf8_cache == 0) 306 midna_domain_to_utf8_cache = ctable_create(midna_domain_cache_size, 307 midna_domain_to_utf8_create, 308 midna_domain_cache_free, 309 (void *) 0); 310 return (ctable_locate(midna_domain_to_utf8_cache, name)); 311 } 312 313 /* midna_domain_suffix_to_ascii - convert .name to ASCII */ 314 315 const char *midna_domain_suffix_to_ascii(const char *suffix) 316 { 317 const char *cache_res; 318 319 /* 320 * If prepending x to .name causes the result to become too long, then 321 * the suffix is bad. 322 */ 323 if (midna_domain_buf == 0) 324 midna_domain_buf = vstring_alloc(100); 325 vstring_sprintf(midna_domain_buf, "x%s", suffix); 326 if ((cache_res = midna_domain_to_ascii(STR(midna_domain_buf))) == 0) 327 return (0); 328 else 329 return (cache_res + 1); 330 } 331 332 /* midna_domain_suffix_to_utf8 - convert .name to UTF8 */ 333 334 const char *midna_domain_suffix_to_utf8(const char *name) 335 { 336 const char *cache_res; 337 338 /* 339 * If prepending x to .name causes the result to become too long, then 340 * the suffix is bad. 341 */ 342 if (midna_domain_buf == 0) 343 midna_domain_buf = vstring_alloc(100); 344 vstring_sprintf(midna_domain_buf, "x%s", name); 345 if ((cache_res = midna_domain_to_utf8(STR(midna_domain_buf))) == 0) 346 return (0); 347 else 348 return (cache_res + 1); 349 } 350 351 #ifdef TEST 352 353 /* 354 * Test program - reads names from stdin, reports invalid names to stderr. 355 */ 356 #include <unistd.h> 357 #include <stdlib.h> 358 #include <locale.h> 359 360 #include <stringops.h> /* XXX util_utf8_enable */ 361 #include <vstring.h> 362 #include <vstream.h> 363 #include <vstring_vstream.h> 364 #include <msg_vstream.h> 365 366 int main(int argc, char **argv) 367 { 368 VSTRING *buffer = vstring_alloc(1); 369 const char *bp; 370 const char *ascii; 371 const char *utf8; 372 373 if (setlocale(LC_ALL, "C") == 0) 374 msg_fatal("setlocale(LC_ALL, C) failed: %m"); 375 376 msg_vstream_init(argv[0], VSTREAM_ERR); 377 /* msg_verbose = 1; */ 378 util_utf8_enable = 1; 379 380 if (geteuid() == 0) { 381 midna_domain_pre_chroot(); 382 if (chroot(".") != 0) 383 msg_fatal("chroot(\".\"): %m"); 384 } 385 while (vstring_fgets_nonl(buffer, VSTREAM_IN)) { 386 bp = STR(buffer); 387 msg_info("> %s", bp); 388 while (ISSPACE(*bp)) 389 bp++; 390 if (*bp == '#' || *bp == 0) 391 continue; 392 msg_info("unconditional conversions:"); 393 utf8 = midna_domain_to_utf8(bp); 394 msg_info("\"%s\" ->utf8 \"%s\"", bp, utf8 ? utf8 : "(error)"); 395 ascii = midna_domain_to_ascii(bp); 396 msg_info("\"%s\" ->ascii \"%s\"", bp, ascii ? ascii : "(error)"); 397 msg_info("conditional conversions:"); 398 if (!allascii(bp)) { 399 if (ascii != 0) { 400 utf8 = midna_domain_to_utf8(ascii); 401 msg_info("\"%s\" ->ascii \"%s\" ->utf8 \"%s\"", 402 bp, ascii, utf8 ? utf8 : "(error)"); 403 if (utf8 != 0) { 404 if (strcmp(utf8, bp) != 0) 405 msg_warn("\"%s\" != \"%s\"", bp, utf8); 406 } 407 } 408 } else { 409 if (utf8 != 0) { 410 ascii = midna_domain_to_ascii(utf8); 411 msg_info("\"%s\" ->utf8 \"%s\" ->ascii \"%s\"", 412 bp, utf8, ascii ? ascii : "(error)"); 413 if (ascii != 0) { 414 if (strcmp(ascii, bp) != 0) 415 msg_warn("\"%s\" != \"%s\"", bp, ascii); 416 } 417 } 418 } 419 } 420 exit(0); 421 } 422 423 #endif /* TEST */ 424 425 #endif /* NO_EAI */ 426