1 /* 2 * Copyright 2024-2025 The OpenSSL Project Authors. All Rights Reserved. 3 * 4 * Licensed under the Apache License 2.0 (the "License"). You may not use 5 * this file except in compliance with the License. You can obtain a copy 6 * in the file LICENSE in the source distribution or at 7 * https://www.openssl.org/source/license.html 8 */ 9 #include <stdio.h> 10 #include <string.h> 11 #include <openssl/bio.h> 12 #include <openssl/evp.h> 13 #include <openssl/rand.h> 14 15 #include "testutil.h" 16 17 /* 2047 bytes of "#ooooooooo..." + NUL terminator */ 18 static char gunk[2048]; 19 20 typedef struct { 21 char *prefix; 22 char *encoded; 23 unsigned bytes; 24 int trunc; 25 char *suffix; 26 int retry; 27 int no_nl; 28 } test_case; 29 30 #define BUFMAX 0xa0000 /* Encode at most 640kB. */ 31 #define sEOF "-EOF" /* '-' as in PEM and MIME boundaries */ 32 #define junk "#foo" /* Skipped initial content */ 33 34 #define EOF_RETURN (-1729) /* Distinct from -1, etc., internal results */ 35 #define NLEN 6 36 #define NVAR 5 37 /* 38 * Junk suffixed variants don't make sense with padding or truncated groups 39 * because we will typically stop with an error before seeing the suffix, but 40 * with retriable BIOs may never look at the suffix after detecting padding. 41 */ 42 #define NPAD 6 43 #define NVARPAD (NVAR * NPAD - NPAD + 1) 44 45 static char *prefixes[NVAR] = { "", junk, gunk, "", "" }; 46 static char *suffixes[NVAR] = { "", "", "", sEOF, junk }; 47 static unsigned lengths[6] = { 0, 3, 48, 192, 768, 1536 }; 48 static unsigned linelengths[] = { 49 4, 8, 16, 28, 40, 64, 80, 128, 256, 512, 1023, 0 50 }; 51 static unsigned wscnts[] = { 0, 1, 2, 4, 8, 16, 0xFFFF }; 52 53 /* Generate `len` random octets */ 54 static unsigned char *genbytes(unsigned len) 55 { 56 unsigned char *buf = NULL; 57 58 if (len > 0 && len <= BUFMAX && (buf = OPENSSL_malloc(len)) != NULL) 59 RAND_bytes(buf, len); 60 61 return buf; 62 } 63 64 /* Append one base64 codepoint, adding newlines after every `llen` bytes */ 65 static int memout(BIO *mem, char c, int llen, int *pos) 66 { 67 if (BIO_write(mem, &c, 1) != 1) 68 return 0; 69 if (++*pos == llen) { 70 *pos = 0; 71 c = '\n'; 72 if (BIO_write(mem, &c, 1) != 1) 73 return 0; 74 } 75 return 1; 76 } 77 78 /* Encode and append one 6-bit slice, randomly prepending some whitespace */ 79 static int memoutws(BIO *mem, char c, unsigned wscnt, unsigned llen, int *pos) 80 { 81 if (wscnt > 0 82 && (test_random() % llen) < wscnt 83 && memout(mem, ' ', llen, pos) == 0) 84 return 0; 85 return memout(mem, c, llen, pos); 86 } 87 88 /* 89 * Encode an octet string in base64, approximately `llen` bytes per line, 90 * with up to roughly `wscnt` additional space characters inserted at random 91 * before some of the base64 code points. 92 */ 93 static int encode(unsigned const char *buf, unsigned buflen, char *encoded, 94 int trunc, unsigned llen, unsigned wscnt, BIO *mem) 95 { 96 static const unsigned char b64[65] = 97 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 98 int pos = 0; 99 char nl = '\n'; 100 101 /* Use a verbatim encoding when provided */ 102 if (encoded != NULL) { 103 int elen = strlen(encoded); 104 105 return BIO_write(mem, encoded, elen) == elen; 106 } 107 108 /* Encode full 3-octet groups */ 109 while (buflen > 2) { 110 unsigned long v = buf[0] << 16 | buf[1] << 8 | buf[2]; 111 112 if (memoutws(mem, b64[v >> 18], wscnt, llen, &pos) == 0 113 || memoutws(mem, b64[(v >> 12) & 0x3f], wscnt, llen, &pos) == 0 114 || memoutws(mem, b64[(v >> 6) & 0x3f], wscnt, llen, &pos) == 0 115 || memoutws(mem, b64[v & 0x3f], wscnt, llen, &pos) == 0) 116 return 0; 117 buf += 3; 118 buflen -= 3; 119 } 120 121 /* Encode and pad final 1 or 2 octet group */ 122 if (buflen == 2) { 123 unsigned long v = buf[0] << 8 | buf[1]; 124 125 if (memoutws(mem, b64[(v >> 10) & 0x3f], wscnt, llen, &pos) == 0 126 || memoutws(mem, b64[(v >> 4) & 0x3f], wscnt, llen, &pos) == 0 127 || memoutws(mem, b64[(v & 0xf) << 2], wscnt, llen, &pos) == 0 128 || memoutws(mem, '=', wscnt, llen, &pos) == 0) 129 return 0; 130 } else if (buflen == 1) { 131 unsigned long v = buf[0]; 132 133 if (memoutws(mem, b64[v >> 2], wscnt, llen, &pos) == 0 134 || memoutws(mem, b64[(v & 0x3) << 4], wscnt, llen, &pos) == 0 135 || memoutws(mem, '=', wscnt, llen, &pos) == 0 136 || memoutws(mem, '=', wscnt, llen, &pos) == 0) 137 return 0; 138 } 139 140 while (trunc-- > 0) 141 if (memoutws(mem, 'A', wscnt, llen, &pos) == 0) 142 return 0; 143 144 /* Terminate last line */ 145 if (pos > 0 && BIO_write(mem, &nl, 1) != 1) 146 return 0; 147 148 return 1; 149 } 150 151 static int genb64(char *prefix, char *suffix, unsigned const char *buf, 152 unsigned buflen, int trunc, char *encoded, unsigned llen, 153 unsigned wscnt, char **out) 154 { 155 int preflen = strlen(prefix); 156 int sufflen = strlen(suffix); 157 int outlen; 158 char newline = '\n'; 159 BUF_MEM *bptr; 160 BIO *mem = BIO_new(BIO_s_mem()); 161 162 if (mem == NULL) 163 return -1; 164 165 if ((*prefix && (BIO_write(mem, prefix, preflen) != preflen 166 || BIO_write(mem, &newline, 1) != 1)) 167 || encode(buf, buflen, encoded, trunc, llen, wscnt, mem) <= 0 168 || (*suffix && (BIO_write(mem, suffix, sufflen) != sufflen 169 || BIO_write(mem, &newline, 1) != 1))) { 170 BIO_free(mem); 171 return -1; 172 } 173 174 /* Orphan the memory BIO's data buffer */ 175 BIO_get_mem_ptr(mem, &bptr); 176 *out = bptr->data; 177 outlen = bptr->length; 178 bptr->data = NULL; 179 (void) BIO_set_close(mem, BIO_NOCLOSE); 180 BIO_free(mem); 181 BUF_MEM_free(bptr); 182 183 return outlen; 184 } 185 186 static int test_bio_base64_run(test_case *t, int llen, int wscnt) 187 { 188 unsigned char *raw; 189 unsigned char *out; 190 unsigned out_len; 191 char *encoded = NULL; 192 int elen; 193 BIO *bio, *b64; 194 int n, n1, n2; 195 int ret; 196 197 /* 198 * Pre-encoded data always encodes NUL octets. If all we care about is the 199 * length, and not the payload, use random bytes. 200 */ 201 if (t->encoded != NULL) 202 raw = OPENSSL_zalloc(t->bytes); 203 else 204 raw = genbytes(t->bytes); 205 206 if (raw == NULL && t->bytes > 0) { 207 TEST_error("out of memory"); 208 return -1; 209 } 210 211 out_len = t->bytes + 1024; 212 out = OPENSSL_malloc(out_len); 213 if (out == NULL) { 214 OPENSSL_free(raw); 215 TEST_error("out of memory"); 216 return -1; 217 } 218 219 elen = genb64(t->prefix, t->suffix, raw, t->bytes, t->trunc, t->encoded, 220 llen, wscnt, &encoded); 221 if (elen < 0 || (bio = BIO_new(BIO_s_mem())) == NULL) { 222 OPENSSL_free(raw); 223 OPENSSL_free(out); 224 OPENSSL_free(encoded); 225 TEST_error("out of memory"); 226 return -1; 227 } 228 if (t->retry) 229 BIO_set_mem_eof_return(bio, EOF_RETURN); 230 else 231 BIO_set_mem_eof_return(bio, 0); 232 233 /* 234 * When the input is long enough, and the source bio is retriable, exercise 235 * retries by writting the input to the underlying BIO in two steps (1024 236 * bytes, then the rest) and trying to decode some data after each write. 237 */ 238 n1 = elen; 239 if (t->retry) 240 n1 = elen / 2; 241 if (n1 > 0) 242 BIO_write(bio, encoded, n1); 243 244 b64 = BIO_new(BIO_f_base64()); 245 if (t->no_nl) 246 BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); 247 BIO_push(b64, bio); 248 249 n = BIO_read(b64, out, out_len); 250 251 if (n1 < elen) { 252 /* Append the rest of the input, and read again */ 253 BIO_write(bio, encoded + n1, elen - n1); 254 if (n > 0) { 255 n2 = BIO_read(b64, out + n, out_len - n); 256 if (n2 > 0) 257 n += n2; 258 } else if (n == EOF_RETURN) { 259 n = BIO_read(b64, out, out_len); 260 } 261 } 262 263 /* Turn retry-related negative results to normal (0) EOF */ 264 if (n < 0 && n == EOF_RETURN) 265 n = 0; 266 267 /* Turn off retries */ 268 if (t->retry) 269 BIO_set_mem_eof_return(bio, 0); 270 271 if (n < (int) out_len) 272 /* Perform the last read, checking its result */ 273 ret = BIO_read(b64, out + n, out_len - n); 274 else { 275 /* Should not happen, given extra space in out_len */ 276 TEST_error("Unexpectedly long decode output"); 277 ret = -1; 278 } 279 280 /* 281 * Expect an error to be detected with: 282 * 283 * - truncated groups, 284 * - non-base64 suffixes (other than soft EOF) for non-empty or oneline 285 * input 286 * - non-base64 prefixes in NO_NL mode 287 * 288 * Otherwise, check the decoded content 289 */ 290 if (t->trunc > 0 291 || ((t->bytes > 0 || t->no_nl) && *t->suffix && *t->suffix != '-') 292 || (t->no_nl && *t->prefix)) { 293 if ((ret = ret < 0 ? 0 : -1) != 0) 294 TEST_error("Final read result was non-negative"); 295 } else if (ret != 0 296 || n != (int) t->bytes 297 || (n > 0 && memcmp(raw, out, n) != 0)) { 298 TEST_error("Failed to decode expected data"); 299 ret = -1; 300 } 301 302 BIO_free_all(b64); 303 OPENSSL_free(out); 304 OPENSSL_free(raw); 305 OPENSSL_free(encoded); 306 307 return ret; 308 } 309 310 static int generic_case(test_case *t, int verbose) 311 { 312 unsigned *llen; 313 unsigned *wscnt; 314 int ok = 1; 315 316 for (llen = linelengths; *llen > 0; ++llen) { 317 for (wscnt = wscnts; *wscnt * 2 < *llen; ++wscnt) { 318 int extra = t->no_nl ? 64 : 0; 319 320 /* 321 * Use a longer line for NO_NL tests, in particular, eventually 322 * exceeding 1k bytes. 323 */ 324 if (test_bio_base64_run(t, *llen + extra, *wscnt) != 0) 325 ok = 0; 326 327 if (verbose) { 328 fprintf(stderr, "bio_base64_test: ok=%d", ok); 329 if (*t->prefix) 330 fprintf(stderr, ", prefix='%s'", t->prefix); 331 if (t->encoded) 332 fprintf(stderr, ", data='%s'", t->encoded); 333 else 334 fprintf(stderr, ", datalen=%u", t->bytes); 335 if (t->trunc) 336 fprintf(stderr, ", trunc=%d", t->trunc); 337 if (*t->suffix) 338 fprintf(stderr, ", suffix='%s'", t->suffix); 339 fprintf(stderr, ", linelen=%u", *llen); 340 fprintf(stderr, ", wscount=%u", *wscnt); 341 if (t->retry) 342 fprintf(stderr, ", retriable"); 343 if (t->no_nl) 344 fprintf(stderr, ", oneline"); 345 fputc('\n', stderr); 346 } 347 348 /* For verbatim input no effect from varying llen or wscnt */ 349 if (t->encoded) 350 return ok; 351 } 352 /* 353 * Longer 'llen' has no effect once we're sure to not have multiple 354 * lines of data 355 */ 356 if (*llen > t->bytes + (t->bytes >> 1)) 357 break; 358 } 359 return ok; 360 } 361 362 static int quotrem(int i, unsigned int m, int *q) 363 { 364 *q = i / m; 365 return i - *q * m; 366 } 367 368 static int test_bio_base64_generated(int idx) 369 { 370 test_case t; 371 int variant; 372 int lencase; 373 int padcase; 374 int q = idx; 375 376 lencase = quotrem(q, NLEN, &q); 377 variant = quotrem(q, NVARPAD, &q); 378 padcase = quotrem(variant, NPAD, &variant); 379 t.retry = quotrem(q, 2, &q); 380 t.no_nl = quotrem(q, 2, &q); 381 382 if (q != 0) { 383 fprintf(stderr, "Test index out of range: %d", idx); 384 return 0; 385 } 386 387 t.prefix = prefixes[variant]; 388 t.encoded = NULL; 389 t.bytes = lengths[lencase]; 390 t.trunc = 0; 391 if (padcase && padcase < 3) 392 t.bytes += padcase; 393 else if (padcase >= 3) 394 t.trunc = padcase - 2; 395 t.suffix = suffixes[variant]; 396 397 if (padcase != 0 && (*t.suffix && *t.suffix != '-')) { 398 TEST_error("Unexpected suffix test after padding"); 399 return 0; 400 } 401 402 return generic_case(&t, 0); 403 } 404 405 static int test_bio_base64_corner_case_bug(int idx) 406 { 407 test_case t; 408 int q = idx; 409 410 t.retry = quotrem(q, 2, &q); 411 t.no_nl = quotrem(q, 2, &q); 412 413 if (q != 0) { 414 fprintf(stderr, "Test index out of range: %d", idx); 415 return 0; 416 } 417 418 /* 9 bytes of skipped non-base64 input + newline */ 419 t.prefix = "#foo\n#bar"; 420 421 /* 9 bytes on 2nd and subsequent lines */ 422 t.encoded = "A\nAAA\nAAAA\n"; 423 t.suffix = ""; 424 425 /* Expected decode length */ 426 t.bytes = 6; 427 t.trunc = 0; /* ignored */ 428 429 return generic_case(&t, 0); 430 } 431 432 int setup_tests(void) 433 { 434 int numidx; 435 436 memset(gunk, 'o', sizeof(gunk)); 437 gunk[0] = '#'; 438 gunk[sizeof(gunk) - 1] = '\0'; 439 440 /* 441 * Test 5 variants of prefix or suffix 442 * 443 * - both empty 444 * - short junk prefix 445 * - long gunk prefix (> internal BIO 1k buffer size), 446 * - soft EOF suffix 447 * - junk suffix (expect to detect an error) 448 * 449 * For 6 input lengths of randomly generated raw input: 450 * 451 * 0, 3, 48, 192, 768 and 1536 452 * 453 * corresponding to encoded lengths (plus linebreaks and ignored 454 * whitespace) of: 455 * 456 * 0, 4, 64, 256, 1024 and 2048 457 * 458 * Followed by zero, one or two additional bytes that may involve padding, 459 * or else (truncation) 1, 2 or 3 bytes with missing padding. 460 * Only the first four variants make sense with padding or truncated 461 * groups. 462 * 463 * With two types of underlying BIO 464 * 465 * - Non-retriable underlying BIO 466 * - Retriable underlying BIO 467 * 468 * And with/without the BIO_FLAGS_BASE64_NO_NL flag, where now an error is 469 * expected with the junk and gunk prefixes, however, but the "soft EOF" 470 * suffix is still accepted. 471 * 472 * Internally, each test may loop over a range of encoded line lengths and 473 * whitespace average "densities". 474 */ 475 numidx = NLEN * (NVAR * NPAD - NPAD + 1) * 2 * 2; 476 ADD_ALL_TESTS(test_bio_base64_generated, numidx); 477 478 /* 479 * Corner case in original code that skips ignored input, when the ignored 480 * length is one byte longer than the total of the second and later lines 481 * of valid input in the first 1k bytes of input. No content variants, 482 * just BIO retry status and oneline flags vary. 483 */ 484 numidx = 2 * 2; 485 ADD_ALL_TESTS(test_bio_base64_corner_case_bug, numidx); 486 487 return 1; 488 } 489