From 556dc5ae8548b8168f7f7a87134245eaa53b29b2 Mon Sep 17 00:00:00 2001 From: Aidan Keefe Date: Mon, 31 Aug 2026 16:44:31 -0600 Subject: [PATCH 1/4] added read/write io functions that can be used to interact with any file pointer --- src/include.am | 1 + src/tools/clu_base64.c | 134 ++++++++-------------- src/tools/clu_io.c | 229 ++++++++++++++++++++++++++++++++++++++ wolfCLU.vcxproj | 1 + wolfclu/clu_header_main.h | 21 ++++ 5 files changed, 298 insertions(+), 88 deletions(-) create mode 100644 src/tools/clu_io.c diff --git a/src/include.am b/src/include.am index 776b7c65..8d1b44a2 100644 --- a/src/include.am +++ b/src/include.am @@ -9,6 +9,7 @@ wolfssl_SOURCES = src/clu_main.c \ src/tools/clu_funcs.c \ src/tools/clu_hex_to_bin.c \ src/tools/clu_rand.c \ + src/tools/clu_io.c \ src/crypto/clu_crypto_setup.c \ src/crypto/clu_encrypt.c \ src/crypto/clu_decrypt.c \ diff --git a/src/tools/clu_base64.c b/src/tools/clu_base64.c index 20a9d8b3..31f911f0 100644 --- a/src/tools/clu_base64.c +++ b/src/tools/clu_base64.c @@ -49,14 +49,15 @@ static void wolfCLU_Base64Help(void) int wolfCLU_Base64Setup(int argc, char** argv) { #if !defined(WOLFCLU_NO_FILESYSTEM) && !defined(NO_CODING) - WOLFSSL_BIO *bioIn = NULL; - WOLFSSL_BIO *bioOut = NULL; + char *bioInFile = NULL; + char *bioOutFile = NULL; byte* input = NULL; byte* output = NULL; int ret = WOLFCLU_SUCCESS; int decode = 0; int isPEM = 0; - sword32 inputSz = 8000; + /* initial buffer size to read stdin */ + word32 inputSz = 0; word32 outputSz = 0; int option; int longIndex = 1; @@ -75,20 +76,19 @@ int wolfCLU_Base64Setup(int argc, char** argv) break; case WOLFCLU_INFILE: - bioIn = wolfSSL_BIO_new_file(optarg, "rb"); - if (bioIn == NULL) { - wolfCLU_LogError("unable to open file %s", optarg); + if (optarg == NULL) { + wolfCLU_LogError("-in expected a value"); ret = WOLFCLU_FATAL_ERROR; } + bioInFile = optarg; break; case WOLFCLU_OUTFILE: - bioOut = wolfSSL_BIO_new_file(optarg, "wb"); - if (bioOut == NULL) { - wolfCLU_LogError("unable to open output file %s", - optarg); + if (optarg == NULL) { + wolfCLU_LogError("-out expected a value"); ret = WOLFCLU_FATAL_ERROR; } + bioOutFile = optarg; break; case 'd': @@ -97,12 +97,6 @@ int wolfCLU_Base64Setup(int argc, char** argv) case WOLFCLU_HELP: wolfCLU_Base64Help(); - if (bioIn != NULL) { - wolfSSL_BIO_free(bioIn); - } - if (bioOut != NULL) { - wolfSSL_BIO_free(bioOut); - } return WOLFCLU_SUCCESS; case ':': @@ -118,56 +112,39 @@ int wolfCLU_Base64Setup(int argc, char** argv) } } - if (ret == WOLFCLU_SUCCESS && bioIn == NULL) { - bioIn = wolfSSL_BIO_new(wolfSSL_BIO_s_file()); - if (bioIn != NULL) - wolfSSL_BIO_set_fp(bioIn, stdin, BIO_NOCLOSE); - } - else if (ret == WOLFCLU_SUCCESS) { - /* get data size using raw FILE pointer and seek */ - XFILE f; - if (wolfSSL_BIO_get_fp(bioIn, &f) != WOLFSSL_SUCCESS) { - wolfCLU_LogError("Unable to get raw file pointer"); - ret = WOLFCLU_FATAL_ERROR; - } - - if (ret == WOLFCLU_SUCCESS && XFSEEK(f, 0, XSEEK_END) != 0) { - wolfCLU_LogError("Unable to seek end of file"); - ret = WOLFCLU_FATAL_ERROR; - } - - if (ret == WOLFCLU_SUCCESS) { - inputSz = (sword32)XFTELL(f); - wolfSSL_BIO_reset(bioIn); - } - } - if (ret == WOLFCLU_SUCCESS) { - input = (byte*)XMALLOC(inputSz, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - if (input == NULL) { - wolfCLU_LogError("Memory allocation error for input buffer"); - ret = MEMORY_E; + if (bioInFile == NULL) { + ret = wolfCLU_readInIo(WOLFCLU_IO_STDIN, stdin, (char**)(&input), + &inputSz); } else { - inputSz = wolfSSL_BIO_read(bioIn, input, inputSz); - - if (inputSz < 0) { - wolfCLU_LogError("Could not read input."); + XFILE fp = XFOPEN(bioInFile, "rb"); + if (fp == NULL) { + wolfCLU_LogError("Could not open file %s", bioOutFile); ret = WOLFCLU_FATAL_ERROR; } - /* For decoding, check if input is in PEM format */ - else if (decode && inputSz > 11) { - /* Check if the input starts with a PEM header */ - if (XMEMCMP(input, "-----BEGIN", 10) == 0) { - isPEM = 1; - } + else { + ret = wolfCLU_readInIo(WOLFCLU_IO_FILE, fp, + (char**)(&input), &inputSz); + XFCLOSE(fp); } + } + } + /* For decoding, check if input is in PEM format */ + if (ret == WOLFCLU_SUCCESS && decode && inputSz > 11) { + /* Check if the input starts with a PEM header */ + if (XMEMCMP(input, "-----BEGIN", 10) == 0) { + isPEM = 1; } } /* Perform encoding/decoding */ - if (ret == WOLFCLU_SUCCESS && decode) { + if (ret == WOLFCLU_SUCCESS && inputSz == 0) { + /* empty input produces empty output, matching 'openssl base64' */ + outputSz = 0; + } + else if (ret == WOLFCLU_SUCCESS && decode) { if (isPEM) { #ifdef WOLFSSL_PEM_TO_DER /* Try different PEM types */ @@ -259,47 +236,34 @@ int wolfCLU_Base64Setup(int argc, char** argv) } if (ret == WOLFCLU_SUCCESS) { - if (Base64_Encode(input, inputSz, output, &outputSz) < 0) { - wolfCLU_LogError("Base64 encode failed: %d", ret); + int encRet = Base64_Encode(input, inputSz, output, &outputSz); + if (encRet < 0) { + wolfCLU_LogError("Base64 encode failed: %d", encRet); ret = WOLFCLU_FATAL_ERROR; } - else { - ret = WOLFCLU_SUCCESS; - } } } - if (ret == WOLFCLU_SUCCESS && bioOut != NULL) { - /* Write output */ - ret = wolfSSL_BIO_write(bioOut, output, outputSz); - if (ret <= 0) { - wolfCLU_LogError("Failed to write output data: %d", ret); - ret = WOLFCLU_FATAL_ERROR; + if (ret == WOLFCLU_SUCCESS) { + if (bioOutFile == NULL) { + ret = wolfCLU_writeOutIo(WOLFCLU_IO_STDOUT, stdout, (char*)output, + outputSz); } else { - ret = WOLFCLU_SUCCESS; - } - } - else if (ret == WOLFCLU_SUCCESS) { - /* Write to stdout */ - bioOut = wolfSSL_BIO_new(wolfSSL_BIO_s_file()); - if (bioOut != NULL) { - wolfSSL_BIO_set_fp(bioOut, stdout, BIO_NOCLOSE); - ret = wolfSSL_BIO_write(bioOut, output, outputSz); - if (ret <= 0) { - wolfCLU_LogError("Failed to write to stdout: %d", ret); + XFILE fp = XFOPEN(bioOutFile, "wb"); + if (fp == NULL) { + wolfCLU_LogError("Could not open file %s", bioInFile); ret = WOLFCLU_FATAL_ERROR; } else { - ret = WOLFCLU_SUCCESS; + ret = wolfCLU_writeOutIo(WOLFCLU_IO_FILE, fp, + (char*)output, outputSz); + XFCLOSE(fp); } } - else { - wolfCLU_LogError("Failed to create stdout BIO"); - ret = MEMORY_E; - } } + /* Clean up */ if (input != NULL) { XFREE(input, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); @@ -312,12 +276,6 @@ int wolfCLU_Base64Setup(int argc, char** argv) wc_FreeDer(&der); } #endif - if (bioIn != NULL) { - wolfSSL_BIO_free(bioIn); - } - if (bioOut != NULL) { - wolfSSL_BIO_free(bioOut); - } return ret; #else diff --git a/src/tools/clu_io.c b/src/tools/clu_io.c new file mode 100644 index 00000000..1783526b --- /dev/null +++ b/src/tools/clu_io.c @@ -0,0 +1,229 @@ +#include +#include +#include + +/* Windows opens stdout and stdin in text mode, translating 0x0A <-> 0x0D 0x0A. + * We don't want that */ +#if defined(_WIN32) + #include + #include +#endif + +int wolfCLU_readInIo(enum WOLFCLU_IO_TYPE ioType, XFILE fp, char** buf, + word32* len) +{ + WOLFSSL_BIO* bio = NULL; + int ret = WOLFCLU_SUCCESS; + + struct { + char* outBuf; + sword32 len; + sword32 cap; + } buffer = {0}; + + switch (ioType) { + case WOLFCLU_IO_STDIN: + bio = wolfSSL_BIO_new_fp(fp, BIO_NOCLOSE); + if (bio == NULL) { + wolfCLU_LogError("Could not create BIO with stdin"); + return WOLFCLU_FATAL_ERROR; + } +#ifdef _WIN32 + /* Put stdin in binary mode so raw bytes pass through + * untranslated on windows. */ + (void)_setmode(_fileno(fp), _O_BINARY); +#endif + break; + + case WOLFCLU_IO_FILE: + bio = wolfSSL_BIO_new_fp(fp, BIO_NOCLOSE); + if (bio == NULL) { + wolfCLU_LogError("Could not create BIO"); + return WOLFCLU_FATAL_ERROR; + } + break; + + case WOLFCLU_IO_STDOUT: + default: + wolfCLU_LogError("Could not open file: unknown file type"); + return WOLFCLU_FATAL_ERROR; + } + + switch (ioType) { + case WOLFCLU_IO_STDIN: + { + char* tmp = NULL; + sword32 read = 0; + char cannotBump = 0; + while (1) { + if (buffer.cap == buffer.len) { + if (cannotBump == 1) { + wolfCLU_LogError("input too big needs to be %d " + "bytes or less", INT_MAX); + ret = WOLFCLU_FATAL_ERROR; + break; + } + if (buffer.cap > (INT_MAX - 1024 / 2)) { + buffer.cap = INT_MAX; + cannotBump = 1; + } + else { + buffer.cap *= 2; + buffer.cap += 1024; + } + tmp = XREALLOC(buffer.outBuf, buffer.cap, + HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + if (tmp == NULL) { + wolfCLU_LogError("Could not allocate space for io " + "read."); + ret = WOLFCLU_FATAL_ERROR; + break; + } + } + buffer.outBuf = tmp; + read = wolfSSL_BIO_read(bio, buffer.outBuf + buffer.len, + buffer.cap - buffer.len); + if (read < 0) { + wolfCLU_LogError("Error while reading from stdin."); + ret = WOLFCLU_FATAL_ERROR; + break; + } + + if (read == 0) { + break; + } + + buffer.len += read; + } + + tmp = XREALLOC(buffer.outBuf, buffer.cap, + HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + if (tmp == NULL) { + wolfCLU_LogError("Could not allocate space for io " + "read."); + ret = WOLFCLU_FATAL_ERROR; + break; + } + buffer.outBuf = tmp; + } + break; + + case WOLFCLU_IO_FILE: { + sword32 read = 0; + long fileSize = 0; + XFILE innerFp = NULL; + if (wolfSSL_BIO_get_fp(bio, &innerFp) != WOLFSSL_SUCCESS) { + wolfCLU_LogError("Could not get file pointer from BIO"); + ret = WOLFCLU_FATAL_ERROR; + break; + } + if (XFSEEK(innerFp, 0, SEEK_END) != 0) { + wolfCLU_LogError("Could not seek input file"); + ret = WOLFCLU_FATAL_ERROR; + break; + } + if ((fileSize = XFTELL(innerFp)) < 0) { + wolfCLU_LogError("Could not get length of file"); + ret = WOLFCLU_FATAL_ERROR; + break; + } + if (wolfSSL_BIO_reset(bio) != WOLFSSL_SUCCESS) { + wolfCLU_LogError("Could not reset Bio"); + ret = WOLFCLU_FATAL_ERROR; + break; + } + buffer.len = (sword32)fileSize; + if (buffer.len < 0) { + wolfCLU_LogError("Could not get length of file data"); + ret = WOLFCLU_FATAL_ERROR; + break; + } + + buffer.outBuf = XMALLOC(buffer.len, HEAP_HINT, + DYNAMIC_TYPE_TMP_BUFFER); + if (buffer.outBuf == NULL) { + wolfCLU_LogError("Could not allocate space for io " + "read."); + ret = WOLFCLU_FATAL_ERROR; + break; + } + buffer.cap = buffer.len; + read = wolfSSL_BIO_read(bio, buffer.outBuf, buffer.len); + if (read != buffer.len) { + wolfCLU_LogError("Could not create BIO"); + ret = WOLFCLU_FATAL_ERROR; + break; + } + break; + } + + case WOLFCLU_IO_STDOUT: + default: + ret = WOLFCLU_FATAL_ERROR; + } + + if (bio != NULL) { + wolfSSL_BIO_free(bio); + } + if (ret != WOLFCLU_SUCCESS) { + if (buffer.outBuf != NULL) { + XFREE(buffer.outBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + XMEMSET(&buffer, 0, sizeof(buffer)); + } + return ret; + } + else { + *len = buffer.len; + *buf = buffer.outBuf; + return ret; + } +} + +int wolfCLU_writeOutIo(enum WOLFCLU_IO_TYPE ioType, XFILE fp, + char* buf, word32 len) +{ + WOLFSSL_BIO* bio = NULL; + int ret = WOLFCLU_SUCCESS; + switch (ioType) { + + case WOLFCLU_IO_STDOUT: { + bio = wolfSSL_BIO_new_fp(fp, BIO_NOCLOSE); + if (bio == NULL) { + wolfCLU_LogError("Could not get BIO from stdout"); + return WOLFCLU_FATAL_ERROR; + } +#ifdef _WIN32 + /* Put stdout in binary mode so raw bytes pass through + * untranslated. */ + (void)_setmode(_fileno(stdout), _O_BINARY); +#endif + break; + } + + case WOLFCLU_IO_FILE: { + bio = wolfSSL_BIO_new_fp(fp, BIO_NOCLOSE); + if (bio == NULL) { + wolfCLU_LogError("Could not create BIO"); + return WOLFCLU_FATAL_ERROR; + } + break; + } + + case WOLFCLU_IO_STDIN: + default: + wolfCLU_LogError("Could not open BIO: Wrong Io type."); + return WOLFCLU_FATAL_ERROR; + } + + if (wolfSSL_BIO_write(bio, buf, len) != (int)len) { + wolfCLU_LogError("Could not write buffer out to target"); + ret = WOLFCLU_FATAL_ERROR; + } + + if (bio != NULL) { + wolfSSL_BIO_free(bio); + } + + return ret; +} + diff --git a/wolfCLU.vcxproj b/wolfCLU.vcxproj index 8faba25e..e0c7affc 100644 --- a/wolfCLU.vcxproj +++ b/wolfCLU.vcxproj @@ -175,6 +175,7 @@ + diff --git a/wolfclu/clu_header_main.h b/wolfclu/clu_header_main.h index b3fa9c93..24d10d83 100644 --- a/wolfclu/clu_header_main.h +++ b/wolfclu/clu_header_main.h @@ -851,6 +851,27 @@ int wolfCLU_ReadCertDer(const char* filename, byte** outDer); */ int wolfCLU_GetStdinPassword(byte* password, word32* passwordSz); +/** + * @brief Io types that passed to Io fucntions + */ +enum WOLFCLU_IO_TYPE { + WOLFCLU_IO_STDIN, + WOLFCLU_IO_STDOUT, + WOLFCLU_IO_FILE, +}; + +/* + * @brief read contence of fp to buf paramter. You must free buffer + * yourself. + */ +int wolfCLU_readInIo(enum WOLFCLU_IO_TYPE ioType, XFILE fp, char** buf, + word32* len); + +/* + * @brief write the contense of buf parameter to fp + */ +int wolfCLU_writeOutIo(enum WOLFCLU_IO_TYPE ioType, XFILE fp, + char* buf, word32 len); #ifdef __cplusplus } #endif From 4e85cad8fa4794b0841fe6779f8e99a0018dbe3b Mon Sep 17 00:00:00 2001 From: Aidan Keefe Date: Tue, 1 Sep 2026 09:44:11 -0600 Subject: [PATCH 2/4] harden io untils --- src/tools/clu_base64.c | 4 +- src/tools/clu_io.c | 239 ++++++++++++++++++++---------------- tests/base64/base64-test.py | 28 +++++ 3 files changed, 161 insertions(+), 110 deletions(-) diff --git a/src/tools/clu_base64.c b/src/tools/clu_base64.c index 31f911f0..7e60e440 100644 --- a/src/tools/clu_base64.c +++ b/src/tools/clu_base64.c @@ -120,7 +120,7 @@ int wolfCLU_Base64Setup(int argc, char** argv) else { XFILE fp = XFOPEN(bioInFile, "rb"); if (fp == NULL) { - wolfCLU_LogError("Could not open file %s", bioOutFile); + wolfCLU_LogError("Could not open file %s", bioInFile); ret = WOLFCLU_FATAL_ERROR; } else { @@ -252,7 +252,7 @@ int wolfCLU_Base64Setup(int argc, char** argv) else { XFILE fp = XFOPEN(bioOutFile, "wb"); if (fp == NULL) { - wolfCLU_LogError("Could not open file %s", bioInFile); + wolfCLU_LogError("Could not open file %s", bioOutFile); ret = WOLFCLU_FATAL_ERROR; } else { diff --git a/src/tools/clu_io.c b/src/tools/clu_io.c index 1783526b..6b424243 100644 --- a/src/tools/clu_io.c +++ b/src/tools/clu_io.c @@ -9,17 +9,134 @@ #include #endif +typedef struct WOLFCLU_IO_BUFFER { + char* outBuf; + int len; + int cap; +} WOLFCLU_IO_BUFFER; + +static int StreamRead(WOLFSSL_BIO* bio, WOLFCLU_IO_BUFFER* buffer) +{ + char* tmp = NULL; + sword32 read = 0; + char cannotBump = 0; + int ret = WOLFCLU_SUCCESS; + while (1) { + if (buffer->cap == buffer->len) { + if (cannotBump == 1) { + wolfCLU_LogError("input too big needs to be %d " + "bytes or less", INT_MAX); + ret = WOLFCLU_FATAL_ERROR; + break; + } + if (buffer->cap > ((INT_MAX - 1024) / 2)) { + buffer->cap = INT_MAX; + cannotBump = 1; + } + else { + buffer->cap *= 2; + buffer->cap += 1024; + } + tmp = XREALLOC(buffer->outBuf, buffer->cap, + HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + if (tmp == NULL) { + wolfCLU_LogError("Could not allocate space for io " + "read."); + ret = WOLFCLU_FATAL_ERROR; + break; + } + buffer->outBuf = tmp; + } + read = wolfSSL_BIO_read(bio, buffer->outBuf + buffer->len, + buffer->cap - buffer->len); + if (read < 0) { + wolfCLU_LogError("Error while reading from stdin."); + ret = WOLFCLU_FATAL_ERROR; + break; + } + + if (read == 0) { + break; + } + + buffer->len += read; + } + + /* shrink the over allocated buffer down to what was actually read. A + * zero length read is left alone since XREALLOC to 0 may free the buffer + * and hand back NULL */ + if (ret == WOLFCLU_SUCCESS && buffer->len > 0 && + buffer->len < buffer->cap) { + tmp = XREALLOC(buffer->outBuf, buffer->len, + HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + if (tmp == NULL) { + wolfCLU_LogError("Could not allocate space for io " + "read"); + ret = WOLFCLU_FATAL_ERROR; + } + else { + buffer->outBuf = tmp; + buffer->cap = buffer->len; + } + } + + return ret; +} + +static int FileRead(WOLFSSL_BIO* bio, WOLFCLU_IO_BUFFER* buffer) +{ + sword32 read = 0; + long fileSize = 0; + XFILE innerFp = NULL; + if (wolfSSL_BIO_get_fp(bio, &innerFp) != WOLFSSL_SUCCESS) { + wolfCLU_LogError("Could not get file pointer from BIO"); + return WOLFCLU_FATAL_ERROR; + } + if (XFSEEK(innerFp, 0, SEEK_END) != 0) { + wolfCLU_LogError("Could not seek input file"); + return WOLFCLU_FATAL_ERROR; + } + if ((fileSize = XFTELL(innerFp)) < 0) { + wolfCLU_LogError("Could not get length of file"); + return WOLFCLU_FATAL_ERROR; + } + if (wolfSSL_BIO_reset(bio) != WOLFSSL_SUCCESS) { + wolfCLU_LogError("Could not reset Bio"); + return WOLFCLU_FATAL_ERROR; + } + buffer->len = (sword32)fileSize; + if (buffer->len < 0) { + wolfCLU_LogError("Could not get length of file data"); + return WOLFCLU_FATAL_ERROR; + } + + buffer->outBuf = XMALLOC(buffer->len, HEAP_HINT, + DYNAMIC_TYPE_TMP_BUFFER); + if (buffer->outBuf == NULL) { + wolfCLU_LogError("Could not allocate space for io " + "read."); + return WOLFCLU_FATAL_ERROR; + } + buffer->cap = buffer->len; + read = wolfSSL_BIO_read(bio, buffer->outBuf, buffer->len); + if (read != buffer->len) { + wolfCLU_LogError("Could not read all of the file data"); + return WOLFCLU_FATAL_ERROR; + } + return WOLFCLU_SUCCESS; +} + int wolfCLU_readInIo(enum WOLFCLU_IO_TYPE ioType, XFILE fp, char** buf, word32* len) { WOLFSSL_BIO* bio = NULL; int ret = WOLFCLU_SUCCESS; + WOLFCLU_IO_BUFFER buffer = {0}; - struct { - char* outBuf; - sword32 len; - sword32 cap; - } buffer = {0}; + if (fp == XBADFILE || buf == NULL || len == NULL) { + wolfCLU_LogError("Bad arg passed to wolfCLU_readInIo"); + return BAD_FUNC_ARG; + } switch (ioType) { case WOLFCLU_IO_STDIN: @@ -51,111 +168,12 @@ int wolfCLU_readInIo(enum WOLFCLU_IO_TYPE ioType, XFILE fp, char** buf, switch (ioType) { case WOLFCLU_IO_STDIN: - { - char* tmp = NULL; - sword32 read = 0; - char cannotBump = 0; - while (1) { - if (buffer.cap == buffer.len) { - if (cannotBump == 1) { - wolfCLU_LogError("input too big needs to be %d " - "bytes or less", INT_MAX); - ret = WOLFCLU_FATAL_ERROR; - break; - } - if (buffer.cap > (INT_MAX - 1024 / 2)) { - buffer.cap = INT_MAX; - cannotBump = 1; - } - else { - buffer.cap *= 2; - buffer.cap += 1024; - } - tmp = XREALLOC(buffer.outBuf, buffer.cap, - HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - if (tmp == NULL) { - wolfCLU_LogError("Could not allocate space for io " - "read."); - ret = WOLFCLU_FATAL_ERROR; - break; - } - } - buffer.outBuf = tmp; - read = wolfSSL_BIO_read(bio, buffer.outBuf + buffer.len, - buffer.cap - buffer.len); - if (read < 0) { - wolfCLU_LogError("Error while reading from stdin."); - ret = WOLFCLU_FATAL_ERROR; - break; - } - - if (read == 0) { - break; - } - - buffer.len += read; - } - - tmp = XREALLOC(buffer.outBuf, buffer.cap, - HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - if (tmp == NULL) { - wolfCLU_LogError("Could not allocate space for io " - "read."); - ret = WOLFCLU_FATAL_ERROR; - break; - } - buffer.outBuf = tmp; - } + ret = StreamRead(bio, &buffer); break; - case WOLFCLU_IO_FILE: { - sword32 read = 0; - long fileSize = 0; - XFILE innerFp = NULL; - if (wolfSSL_BIO_get_fp(bio, &innerFp) != WOLFSSL_SUCCESS) { - wolfCLU_LogError("Could not get file pointer from BIO"); - ret = WOLFCLU_FATAL_ERROR; - break; - } - if (XFSEEK(innerFp, 0, SEEK_END) != 0) { - wolfCLU_LogError("Could not seek input file"); - ret = WOLFCLU_FATAL_ERROR; - break; - } - if ((fileSize = XFTELL(innerFp)) < 0) { - wolfCLU_LogError("Could not get length of file"); - ret = WOLFCLU_FATAL_ERROR; - break; - } - if (wolfSSL_BIO_reset(bio) != WOLFSSL_SUCCESS) { - wolfCLU_LogError("Could not reset Bio"); - ret = WOLFCLU_FATAL_ERROR; - break; - } - buffer.len = (sword32)fileSize; - if (buffer.len < 0) { - wolfCLU_LogError("Could not get length of file data"); - ret = WOLFCLU_FATAL_ERROR; - break; - } - - buffer.outBuf = XMALLOC(buffer.len, HEAP_HINT, - DYNAMIC_TYPE_TMP_BUFFER); - if (buffer.outBuf == NULL) { - wolfCLU_LogError("Could not allocate space for io " - "read."); - ret = WOLFCLU_FATAL_ERROR; - break; - } - buffer.cap = buffer.len; - read = wolfSSL_BIO_read(bio, buffer.outBuf, buffer.len); - if (read != buffer.len) { - wolfCLU_LogError("Could not create BIO"); - ret = WOLFCLU_FATAL_ERROR; - break; - } + case WOLFCLU_IO_FILE: + ret = FileRead(bio, &buffer); break; - } case WOLFCLU_IO_STDOUT: default: @@ -184,6 +202,11 @@ int wolfCLU_writeOutIo(enum WOLFCLU_IO_TYPE ioType, XFILE fp, { WOLFSSL_BIO* bio = NULL; int ret = WOLFCLU_SUCCESS; + if (fp == XBADFILE || (buf == NULL && len > 0) || len > INT_MAX) { + wolfCLU_LogError("Bad arg passed to wolfCLU_writeOutIo"); + return BAD_FUNC_ARG; + } + switch (ioType) { case WOLFCLU_IO_STDOUT: { @@ -215,7 +238,7 @@ int wolfCLU_writeOutIo(enum WOLFCLU_IO_TYPE ioType, XFILE fp, return WOLFCLU_FATAL_ERROR; } - if (wolfSSL_BIO_write(bio, buf, len) != (int)len) { + if (len > 0 && wolfSSL_BIO_write(bio, buf, (int)len) != (int)len) { wolfCLU_LogError("Could not write buffer out to target"); ret = WOLFCLU_FATAL_ERROR; } diff --git a/tests/base64/base64-test.py b/tests/base64/base64-test.py index c955ac71..32aac507 100644 --- a/tests/base64/base64-test.py +++ b/tests/base64/base64-test.py @@ -103,6 +103,34 @@ def test_stdin_input(self): self.assertEqual(result.returncode, 0, "Couldn't parse input from stdin") + def test_empty_stdin(self): + """Empty stdin produces empty output, matching 'openssl base64'.""" + result = subprocess.run( + [WOLFSSL_BIN, "base64"], + input=b"", + capture_output=True, + timeout=60, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout, b"", "empty input should give no output") + + def test_empty_file(self): + """An empty input file produces an empty output file.""" + empty_file = "test-b64-empty.txt" + out_file = "test-b64-empty.b64" + self.addCleanup(lambda: os.remove(empty_file) + if os.path.exists(empty_file) else None) + self.addCleanup(lambda: os.remove(out_file) + if os.path.exists(out_file) else None) + + with open(empty_file, "wb"): + pass + + result = run_wolfssl("base64", "-in", empty_file, "-out", out_file) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(os.path.getsize(out_file), 0, + "empty input should give an empty output file") + def test_help(self): """ Test help flag """ result = subprocess.run( From bc07bd86189269b8ae9dacf303264d34e98d0b15 Mon Sep 17 00:00:00 2001 From: Aidan Keefe Date: Tue, 1 Sep 2026 14:08:36 -0600 Subject: [PATCH 3/4] added gaurd to handle valid empty input added more tests for io for base64 and rewrote IO system for base64 looking to extend in the future --- src/tools/clu_base64.c | 55 +++---- src/tools/clu_io.c | 278 +++++++++++++++++------------------- tests/base64/base64-test.py | 137 ++++++++++++++++++ wolfCLU.vcxproj | 2 +- wolfCLU.vcxproj.filters | 3 + wolfclu/clu_header_main.h | 57 ++++++-- 6 files changed, 351 insertions(+), 181 deletions(-) diff --git a/src/tools/clu_base64.c b/src/tools/clu_base64.c index 7e60e440..1cab67f1 100644 --- a/src/tools/clu_base64.c +++ b/src/tools/clu_base64.c @@ -49,14 +49,14 @@ static void wolfCLU_Base64Help(void) int wolfCLU_Base64Setup(int argc, char** argv) { #if !defined(WOLFCLU_NO_FILESYSTEM) && !defined(NO_CODING) - char *bioInFile = NULL; - char *bioOutFile = NULL; + char *inFile = NULL; + char *outFile = NULL; byte* input = NULL; byte* output = NULL; int ret = WOLFCLU_SUCCESS; int decode = 0; int isPEM = 0; - /* initial buffer size to read stdin */ + /* set by wolfCLU_ReadIo */ word32 inputSz = 0; word32 outputSz = 0; int option; @@ -80,7 +80,9 @@ int wolfCLU_Base64Setup(int argc, char** argv) wolfCLU_LogError("-in expected a value"); ret = WOLFCLU_FATAL_ERROR; } - bioInFile = optarg; + else { + inFile = optarg; + } break; case WOLFCLU_OUTFILE: @@ -88,7 +90,9 @@ int wolfCLU_Base64Setup(int argc, char** argv) wolfCLU_LogError("-out expected a value"); ret = WOLFCLU_FATAL_ERROR; } - bioOutFile = optarg; + else { + outFile = optarg; + } break; case 'd': @@ -113,27 +117,26 @@ int wolfCLU_Base64Setup(int argc, char** argv) } if (ret == WOLFCLU_SUCCESS) { - if (bioInFile == NULL) { - ret = wolfCLU_readInIo(WOLFCLU_IO_STDIN, stdin, (char**)(&input), - &inputSz); + if (inFile == NULL) { + ret = wolfCLU_ReadIo(WOLFCLU_IO_READABLE_STREAM, stdin, &input, + &inputSz); } else { - XFILE fp = XFOPEN(bioInFile, "rb"); - if (fp == NULL) { - wolfCLU_LogError("Could not open file %s", bioInFile); + XFILE fp = XFOPEN(inFile, "rb"); + if (fp == XBADFILE) { + wolfCLU_LogError("Could not open file %s", inFile); ret = WOLFCLU_FATAL_ERROR; } else { - ret = wolfCLU_readInIo(WOLFCLU_IO_FILE, fp, - (char**)(&input), &inputSz); + ret = wolfCLU_ReadIo(WOLFCLU_IO_READABLE_FILE, fp, + &input, &inputSz); XFCLOSE(fp); } } } - /* For decoding, check if input is in PEM format */ - if (ret == WOLFCLU_SUCCESS && decode && inputSz > 11) { - /* Check if the input starts with a PEM header */ + /* when decoding, check for a PEM header on the input */ + if (ret == WOLFCLU_SUCCESS && decode && inputSz >= 10) { if (XMEMCMP(input, "-----BEGIN", 10) == 0) { isPEM = 1; } @@ -245,25 +248,27 @@ int wolfCLU_Base64Setup(int argc, char** argv) } if (ret == WOLFCLU_SUCCESS) { - if (bioOutFile == NULL) { - ret = wolfCLU_writeOutIo(WOLFCLU_IO_STDOUT, stdout, (char*)output, + if (outFile == NULL) { + ret = wolfCLU_WriteIo(WOLFCLU_IO_WRITABLE_STREAM, stdout, output, outputSz); } else { - XFILE fp = XFOPEN(bioOutFile, "wb"); - if (fp == NULL) { - wolfCLU_LogError("Could not open file %s", bioOutFile); + XFILE fp = XFOPEN(outFile, "wb"); + if (fp == XBADFILE) { + wolfCLU_LogError("Could not open file %s", outFile); ret = WOLFCLU_FATAL_ERROR; } else { - ret = wolfCLU_writeOutIo(WOLFCLU_IO_FILE, fp, - (char*)output, outputSz); - XFCLOSE(fp); + ret = wolfCLU_WriteIo(WOLFCLU_IO_WRITABLE_FILE, fp, + output, outputSz); + if (XFCLOSE(fp) != 0 && ret == WOLFCLU_SUCCESS) { + wolfCLU_LogError("Could not write file %s", outFile); + ret = WOLFCLU_FATAL_ERROR; + } } } } - /* Clean up */ if (input != NULL) { XFREE(input, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); diff --git a/src/tools/clu_io.c b/src/tools/clu_io.c index 6b424243..295eed74 100644 --- a/src/tools/clu_io.c +++ b/src/tools/clu_io.c @@ -1,3 +1,24 @@ +/* clu_io.c + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + #include #include #include @@ -10,107 +31,107 @@ #endif typedef struct WOLFCLU_IO_BUFFER { - char* outBuf; + byte* outBuf; int len; int cap; } WOLFCLU_IO_BUFFER; -static int StreamRead(WOLFSSL_BIO* bio, WOLFCLU_IO_BUFFER* buffer) +/* move the data to a new allocation of newCap bytes, wiping the old one so no + * stale copies are left on the heap */ +static int ResizeBuffer(WOLFCLU_IO_BUFFER* buffer, int newCap) +{ + byte* tmp = (byte*)XMALLOC(newCap, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + if (tmp == NULL) { + wolfCLU_LogError("Could not allocate space for io read."); + return WOLFCLU_FATAL_ERROR; + } + if (buffer->outBuf != NULL) { + XMEMCPY(tmp, buffer->outBuf, buffer->len); + wolfCLU_ForceZero(buffer->outBuf, buffer->len); + XFREE(buffer->outBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + } + buffer->outBuf = tmp; + buffer->cap = newCap; + return WOLFCLU_SUCCESS; +} + +/* read a stream of unknown length, growing the buffer as we go */ +static int StreamRead(XFILE fp, WOLFCLU_IO_BUFFER* buffer) { - char* tmp = NULL; - sword32 read = 0; - char cannotBump = 0; + sword32 bytesRead = 0; + int newCap; int ret = WOLFCLU_SUCCESS; while (1) { if (buffer->cap == buffer->len) { - if (cannotBump == 1) { + if (buffer->cap == INT_MAX) { + /* try to read one more byte to make sure that if the + * one more byte is EOF we break out with a full buffer */ + char lookOneMore; + if (XFREAD(&lookOneMore, 1, 1, fp) == 0 && !XFERROR(fp)) + break; + wolfCLU_LogError("input too big needs to be %d " "bytes or less", INT_MAX); ret = WOLFCLU_FATAL_ERROR; break; } if (buffer->cap > ((INT_MAX - 1024) / 2)) { - buffer->cap = INT_MAX; - cannotBump = 1; + newCap = INT_MAX; } else { - buffer->cap *= 2; - buffer->cap += 1024; + newCap = buffer->cap * 2 + 1024; } - tmp = XREALLOC(buffer->outBuf, buffer->cap, - HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - if (tmp == NULL) { - wolfCLU_LogError("Could not allocate space for io " - "read."); - ret = WOLFCLU_FATAL_ERROR; + ret = ResizeBuffer(buffer, newCap); + if (ret != WOLFCLU_SUCCESS) { break; } - buffer->outBuf = tmp; } - read = wolfSSL_BIO_read(bio, buffer->outBuf + buffer->len, - buffer->cap - buffer->len); - if (read < 0) { - wolfCLU_LogError("Error while reading from stdin."); + bytesRead = (sword32)XFREAD(buffer->outBuf + buffer->len, + sizeof(*buffer->outBuf), buffer->cap - buffer->len, fp); + /* check for errors first, ports without ferror define XFERROR as 0 */ + if (bytesRead < 0 || XFERROR(fp)) { + wolfCLU_LogError("Error while reading input stream."); ret = WOLFCLU_FATAL_ERROR; break; } - - if (read == 0) { - break; + if (bytesRead == 0) { + break; /* EOF */ } - buffer->len += read; - } - - /* shrink the over allocated buffer down to what was actually read. A - * zero length read is left alone since XREALLOC to 0 may free the buffer - * and hand back NULL */ - if (ret == WOLFCLU_SUCCESS && buffer->len > 0 && - buffer->len < buffer->cap) { - tmp = XREALLOC(buffer->outBuf, buffer->len, - HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - if (tmp == NULL) { - wolfCLU_LogError("Could not allocate space for io " - "read"); - ret = WOLFCLU_FATAL_ERROR; - } - else { - buffer->outBuf = tmp; - buffer->cap = buffer->len; - } + buffer->len += bytesRead; } return ret; } -static int FileRead(WOLFSSL_BIO* bio, WOLFCLU_IO_BUFFER* buffer) +/* seek to get the length, then read it in one allocation. Inputs that can't + * be sized this way, like pipes and devices, are read as a stream */ +static int FileRead(XFILE fp, WOLFCLU_IO_BUFFER* buffer) { - sword32 read = 0; + sword32 bytesRead = 0; long fileSize = 0; - XFILE innerFp = NULL; - if (wolfSSL_BIO_get_fp(bio, &innerFp) != WOLFSSL_SUCCESS) { - wolfCLU_LogError("Could not get file pointer from BIO"); - return WOLFCLU_FATAL_ERROR; - } - if (XFSEEK(innerFp, 0, SEEK_END) != 0) { - wolfCLU_LogError("Could not seek input file"); - return WOLFCLU_FATAL_ERROR; + if (XFSEEK(fp, 0, XSEEK_END) != 0) { + /* not seekable, e.g. a pipe */ + return StreamRead(fp, buffer); } - if ((fileSize = XFTELL(innerFp)) < 0) { + if ((fileSize = XFTELL(fp)) < 0) { wolfCLU_LogError("Could not get length of file"); return WOLFCLU_FATAL_ERROR; } - if (wolfSSL_BIO_reset(bio) != WOLFSSL_SUCCESS) { - wolfCLU_LogError("Could not reset Bio"); + else if (fileSize > INT_MAX) { + wolfCLU_LogError("File is too large max is %d bytes", INT_MAX); return WOLFCLU_FATAL_ERROR; } - buffer->len = (sword32)fileSize; - if (buffer->len < 0) { - wolfCLU_LogError("Could not get length of file data"); + if (XFSEEK(fp, 0, XSEEK_SET) != 0) { + wolfCLU_LogError("Could not seek input file"); return WOLFCLU_FATAL_ERROR; } - - buffer->outBuf = XMALLOC(buffer->len, HEAP_HINT, + if (fileSize == 0) { + /* devices report a size of 0, an empty file just reads nothing */ + return StreamRead(fp, buffer); + } + buffer->len = (int)fileSize; + buffer->outBuf = (byte*)XMALLOC(buffer->len, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); if (buffer->outBuf == NULL) { wolfCLU_LogError("Could not allocate space for io " @@ -118,135 +139,106 @@ static int FileRead(WOLFSSL_BIO* bio, WOLFCLU_IO_BUFFER* buffer) return WOLFCLU_FATAL_ERROR; } buffer->cap = buffer->len; - read = wolfSSL_BIO_read(bio, buffer->outBuf, buffer->len); - if (read != buffer->len) { + bytesRead = (sword32)XFREAD(buffer->outBuf, sizeof(*buffer->outBuf), + buffer->len, fp); + if (bytesRead != buffer->len) { wolfCLU_LogError("Could not read all of the file data"); return WOLFCLU_FATAL_ERROR; } return WOLFCLU_SUCCESS; } -int wolfCLU_readInIo(enum WOLFCLU_IO_TYPE ioType, XFILE fp, char** buf, +int wolfCLU_ReadIo(enum WOLFCLU_IO_TYPE ioType, XFILE fp, byte** buf, word32* len) { - WOLFSSL_BIO* bio = NULL; int ret = WOLFCLU_SUCCESS; WOLFCLU_IO_BUFFER buffer = {0}; if (fp == XBADFILE || buf == NULL || len == NULL) { - wolfCLU_LogError("Bad arg passed to wolfCLU_readInIo"); + wolfCLU_LogError("Bad arg passed to wolfCLU_ReadIo"); return BAD_FUNC_ARG; } - switch (ioType) { - case WOLFCLU_IO_STDIN: - bio = wolfSSL_BIO_new_fp(fp, BIO_NOCLOSE); - if (bio == NULL) { - wolfCLU_LogError("Could not create BIO with stdin"); - return WOLFCLU_FATAL_ERROR; - } + *buf = NULL; + *len = 0; + if (!(ioType & WOLFCLU_IO_READABLE)) { + wolfCLU_LogError("Invalid ioType passed to wolfCLU_ReadIo"); + return BAD_FUNC_ARG; + } +#ifdef WOLFCLU_NO_FILESYSTEM + if (ioType & WOLFCLU_IO_RW_FILE) { + wolfCLU_LogError("Cannot read files when compiled with " + "WOLFCLU_NO_FILESYSTEM"); + return NOT_COMPILED_IN; + } +#endif #ifdef _WIN32 - /* Put stdin in binary mode so raw bytes pass through - * untranslated on windows. */ - (void)_setmode(_fileno(fp), _O_BINARY); + /* binary mode, see note at top of file */ + (void)_setmode(_fileno(fp), _O_BINARY); #endif - break; - case WOLFCLU_IO_FILE: - bio = wolfSSL_BIO_new_fp(fp, BIO_NOCLOSE); - if (bio == NULL) { - wolfCLU_LogError("Could not create BIO"); - return WOLFCLU_FATAL_ERROR; - } - break; - - case WOLFCLU_IO_STDOUT: - default: - wolfCLU_LogError("Could not open file: unknown file type"); - return WOLFCLU_FATAL_ERROR; + if (ioType & WOLFCLU_IO_READABLE_FILE) { + ret = FileRead(fp, &buffer); } - - switch (ioType) { - case WOLFCLU_IO_STDIN: - ret = StreamRead(bio, &buffer); - break; - - case WOLFCLU_IO_FILE: - ret = FileRead(bio, &buffer); - break; - - case WOLFCLU_IO_STDOUT: - default: - ret = WOLFCLU_FATAL_ERROR; + else { + ret = StreamRead(fp, &buffer); } - if (bio != NULL) { - wolfSSL_BIO_free(bio); - } if (ret != WOLFCLU_SUCCESS) { if (buffer.outBuf != NULL) { + wolfCLU_ForceZero(buffer.outBuf, buffer.len); XFREE(buffer.outBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - XMEMSET(&buffer, 0, sizeof(buffer)); } return ret; } else { + if (buffer.len == 0 && buffer.outBuf != NULL) { + XFREE(buffer.outBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + buffer.outBuf = NULL; + } *len = buffer.len; *buf = buffer.outBuf; return ret; } } -int wolfCLU_writeOutIo(enum WOLFCLU_IO_TYPE ioType, XFILE fp, - char* buf, word32 len) +int wolfCLU_WriteIo(enum WOLFCLU_IO_TYPE ioType, XFILE fp, + const byte* buf, word32 len) { - WOLFSSL_BIO* bio = NULL; int ret = WOLFCLU_SUCCESS; if (fp == XBADFILE || (buf == NULL && len > 0) || len > INT_MAX) { - wolfCLU_LogError("Bad arg passed to wolfCLU_writeOutIo"); + wolfCLU_LogError("Bad arg passed to wolfCLU_WriteIo"); return BAD_FUNC_ARG; } - switch (ioType) { - - case WOLFCLU_IO_STDOUT: { - bio = wolfSSL_BIO_new_fp(fp, BIO_NOCLOSE); - if (bio == NULL) { - wolfCLU_LogError("Could not get BIO from stdout"); - return WOLFCLU_FATAL_ERROR; - } + if (!(ioType & WOLFCLU_IO_WRITABLE)) { + wolfCLU_LogError("Invalid ioType passed to wolfCLU_WriteIo"); + return BAD_FUNC_ARG; + } +#ifdef WOLFCLU_NO_FILESYSTEM + if (ioType & WOLFCLU_IO_RW_FILE) { + wolfCLU_LogError("Cannot write out to a file when compiled with " + "WOLFCLU_NO_FILESYSTEM"); + return NOT_COMPILED_IN; + } +#endif #ifdef _WIN32 - /* Put stdout in binary mode so raw bytes pass through - * untranslated. */ - (void)_setmode(_fileno(stdout), _O_BINARY); + /* binary mode, see note at top of file */ + (void)_setmode(_fileno(fp), _O_BINARY); #endif - break; - } - case WOLFCLU_IO_FILE: { - bio = wolfSSL_BIO_new_fp(fp, BIO_NOCLOSE); - if (bio == NULL) { - wolfCLU_LogError("Could not create BIO"); - return WOLFCLU_FATAL_ERROR; - } - break; - } - - case WOLFCLU_IO_STDIN: - default: - wolfCLU_LogError("Could not open BIO: Wrong Io type."); - return WOLFCLU_FATAL_ERROR; - } - - if (len > 0 && wolfSSL_BIO_write(bio, buf, (int)len) != (int)len) { + if (len > 0 && XFWRITE(buf, sizeof(*buf), (int)len, fp) != len) { wolfCLU_LogError("Could not write buffer out to target"); ret = WOLFCLU_FATAL_ERROR; } - - if (bio != NULL) { - wolfSSL_BIO_free(bio); +#ifdef XFFLUSH + /* the write may only be buffered, flush so errors like a full disk are + * caught here */ + if (ret == WOLFCLU_SUCCESS && XFFLUSH(fp) != 0) { + wolfCLU_LogError("Could not flush buffer out to target"); + ret = WOLFCLU_FATAL_ERROR; } - +#endif return ret; } diff --git a/tests/base64/base64-test.py b/tests/base64/base64-test.py index 32aac507..911ad601 100644 --- a/tests/base64/base64-test.py +++ b/tests/base64/base64-test.py @@ -1,8 +1,10 @@ #!/usr/bin/env python3 """Base64 encode/decode tests for wolfCLU.""" +import base64 import filecmp import os +import random import subprocess import sys import unittest @@ -12,6 +14,13 @@ from wolfclu_test import WOLFSSL_BIN, CERTS_DIR, run_wolfssl, test_main +def pem_lines(data): + """Base64 encode data as 64 character lines, the way wolfssl writes it.""" + encoded = base64.b64encode(data) + return b"".join(encoded[i:i + 64] + b"\n" + for i in range(0, len(encoded), 64)) + + class Base64Test(unittest.TestCase): @classmethod @@ -103,6 +112,51 @@ def test_stdin_input(self): self.assertEqual(result.returncode, 0, "Couldn't parse input from stdin") + def test_stdin_input_long(self): + """Encode 100,000 bytes from stdin, spanning several buffer grows.""" + data = random.Random(0).randbytes(100000) + + result = subprocess.run( + [WOLFSSL_BIN, "base64"], + input=data, + capture_output=True, + timeout=60, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout.replace(b"\n", b""), + base64.b64encode(data), + "stdin encode does not match python base64") + + def test_stdin_decode_long(self): + """Decode more than the old 8000 byte stdin limit.""" + data = random.Random(1).randbytes(100000) + + result = subprocess.run( + [WOLFSSL_BIN, "base64", "-d"], + input=pem_lines(data), + capture_output=True, + timeout=60, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout, data, + "stdin decode does not match the original data") + + @unittest.skipUnless(os.path.exists("/dev/stdin"), "needs /dev/stdin") + def test_pipe_input_file(self): + """-in on a pipe, which can't be sized with seek, reads like stdin.""" + data = random.Random(2).randbytes(5000) + + result = subprocess.run( + [WOLFSSL_BIN, "base64", "-in", "/dev/stdin"], + input=data, + capture_output=True, + timeout=60, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout.replace(b"\n", b""), + base64.b64encode(data), + "pipe input encode does not match python base64") + def test_empty_stdin(self): """Empty stdin produces empty output, matching 'openssl base64'.""" result = subprocess.run( @@ -114,6 +168,17 @@ def test_empty_stdin(self): self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(result.stdout, b"", "empty input should give no output") + def test_empty_stdin_decode(self): + """Empty stdin with -d produces empty output, like 'openssl base64'.""" + result = subprocess.run( + [WOLFSSL_BIN, "base64", "-d"], + input=b"", + capture_output=True, + timeout=60, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout, b"", "empty input should give no output") + def test_empty_file(self): """An empty input file produces an empty output file.""" empty_file = "test-b64-empty.txt" @@ -131,6 +196,78 @@ def test_empty_file(self): self.assertEqual(os.path.getsize(out_file), 0, "empty input should give an empty output file") + def test_missing_input_file(self): + """A missing -in file gives a non-zero exit.""" + result = run_wolfssl("base64", "-in", "test-b64-does-not-exist.bin") + self.assertNotEqual(result.returncode, 0, + "missing input file should fail") + + def test_output_dir_missing(self): + """An -out path in a missing directory gives a non-zero exit.""" + result = run_wolfssl("base64", "-in", + os.path.join(CERTS_DIR, "server-key.der"), + "-out", os.path.join("test-b64-no-such-dir", + "out.b64")) + self.assertNotEqual(result.returncode, 0, + "output in a missing directory should fail") + + def test_failed_decode_keeps_output(self): + """A failed -d does not create or truncate the -out file.""" + out_file = "test-b64-keep.txt" + self.addCleanup(lambda: os.remove(out_file) + if os.path.exists(out_file) else None) + + with open(out_file, "wb") as f: + f.write(b"keep") + + result = run_wolfssl("base64", "-d", "-out", out_file, + stdin_data="@@@@") + self.assertNotEqual(result.returncode, 0, "bad base64 should fail") + with open(out_file, "rb") as f: + self.assertEqual(f.read(), b"keep", + "failed decode should leave -out unchanged") + + def test_in_place(self): + """-in and -out naming the same file encodes it in place.""" + work_file = "test-b64-inplace.bin" + self.addCleanup(lambda: os.remove(work_file) + if os.path.exists(work_file) else None) + + with open(os.path.join(CERTS_DIR, "server-key.der"), "rb") as f: + original = f.read() + with open(work_file, "wb") as f: + f.write(original) + + result = run_wolfssl("base64", "-in", work_file, "-out", work_file) + self.assertEqual(result.returncode, 0, result.stderr) + with open(work_file, "rb") as f: + self.assertEqual(f.read().replace(b"\n", b""), + base64.b64encode(original), + "in place encode does not match python base64") + + @unittest.skipUnless(os.path.exists("/dev/full"), "needs /dev/full") + def test_stdout_write_error(self): + """A failed write to stdout gives a non-zero exit.""" + with open("/dev/full", "wb") as full: + result = subprocess.run( + [WOLFSSL_BIN, "base64", "-in", + os.path.join(CERTS_DIR, "server-key.der")], + stdout=full, + stderr=subprocess.PIPE, + timeout=60, + ) + self.assertNotEqual(result.returncode, 0, + "write to a full device should fail") + + @unittest.skipUnless(os.path.exists("/dev/full"), "needs /dev/full") + def test_output_file_write_error(self): + """A failed write to the -out file gives a non-zero exit.""" + result = run_wolfssl("base64", "-in", + os.path.join(CERTS_DIR, "server-key.der"), + "-out", "/dev/full") + self.assertNotEqual(result.returncode, 0, + "write to a full device should fail") + def test_help(self): """ Test help flag """ result = subprocess.run( diff --git a/wolfCLU.vcxproj b/wolfCLU.vcxproj index e0c7affc..b20e3335 100644 --- a/wolfCLU.vcxproj +++ b/wolfCLU.vcxproj @@ -175,9 +175,9 @@ - + diff --git a/wolfCLU.vcxproj.filters b/wolfCLU.vcxproj.filters index 9c43d2c1..9d76d5bc 100644 --- a/wolfCLU.vcxproj.filters +++ b/wolfCLU.vcxproj.filters @@ -102,6 +102,9 @@ Source Files + + Source Files + Source Files diff --git a/wolfclu/clu_header_main.h b/wolfclu/clu_header_main.h index 24d10d83..480343bf 100644 --- a/wolfclu/clu_header_main.h +++ b/wolfclu/clu_header_main.h @@ -852,26 +852,59 @@ int wolfCLU_ReadCertDer(const char* filename, byte** outDer); int wolfCLU_GetStdinPassword(byte* password, word32* passwordSz); /** - * @brief Io types that passed to Io fucntions + * @brief IO types passed to the IO functions below */ enum WOLFCLU_IO_TYPE { - WOLFCLU_IO_STDIN, - WOLFCLU_IO_STDOUT, - WOLFCLU_IO_FILE, + /* base types */ + WOLFCLU_IO_READABLE_STREAM = 1 << 0, + WOLFCLU_IO_WRITABLE_STREAM = 1 << 1, + WOLFCLU_IO_READABLE_FILE = 1 << 2, + WOLFCLU_IO_WRITABLE_FILE = 1 << 3, + + /* type groups */ + WOLFCLU_IO_RW_STREAM = + WOLFCLU_IO_READABLE_STREAM | + WOLFCLU_IO_WRITABLE_STREAM, + + WOLFCLU_IO_RW_FILE = + WOLFCLU_IO_READABLE_FILE | + WOLFCLU_IO_WRITABLE_FILE, + + WOLFCLU_IO_READABLE = + WOLFCLU_IO_READABLE_STREAM | + WOLFCLU_IO_READABLE_FILE, + + WOLFCLU_IO_WRITABLE = + WOLFCLU_IO_WRITABLE_STREAM | + WOLFCLU_IO_WRITABLE_FILE, }; -/* - * @brief read contence of fp to buf paramter. You must free buffer - * yourself. +/** + * @brief read all of fp into a new buffer + * @param ioType a readable stream or file type. Streams are read until EOF, + * files are sized with seek first and read as a stream if that fails + * @param fp file pointer to read from + * @param buf pointer to store the buffer, NULL if empty. The caller frees it, + * wiping it first with wolfCLU_ForceZero if it holds secrets + * @param len pointer to store the number of bytes read + * @return WOLFCLU_SUCCESS on success, negative on error */ -int wolfCLU_readInIo(enum WOLFCLU_IO_TYPE ioType, XFILE fp, char** buf, +int wolfCLU_ReadIo(enum WOLFCLU_IO_TYPE ioType, XFILE fp, byte** buf, word32* len); -/* - * @brief write the contense of buf parameter to fp +/** + * @brief write len bytes of buf to fp and flush it + * @param ioType a writable stream or file type + * @param fp file pointer to write to + * @param buf buffer to write, may be NULL when len is 0 + * @param len number of bytes to write + * @return WOLFCLU_SUCCESS on success, negative on error */ -int wolfCLU_writeOutIo(enum WOLFCLU_IO_TYPE ioType, XFILE fp, - char* buf, word32 len); +int wolfCLU_WriteIo(enum WOLFCLU_IO_TYPE ioType, XFILE fp, const byte* buf, + word32 len); + + + #ifdef __cplusplus } #endif From eae4ffefacd71bb60059dba5b6d722c8cbfb5065 Mon Sep 17 00:00:00 2001 From: Aidan Keefe Date: Mon, 14 Sep 2026 16:09:35 -0600 Subject: [PATCH 4/4] fully fleshed out the io system --- src/tools/clu_base64.c | 57 +++--- src/tools/clu_io.c | 335 ++++++++++++++++++++++++++++-------- tests/base64/base64-test.py | 198 +++++++++++++++++++++ wolfCLU.vcxproj | 1 + wolfCLU.vcxproj.filters | 3 + wolfclu/clu_header_main.h | 54 ------ wolfclu/clu_io.h | 131 ++++++++++++++ wolfclu/include.am | 1 + 8 files changed, 630 insertions(+), 150 deletions(-) create mode 100644 wolfclu/clu_io.h diff --git a/src/tools/clu_base64.c b/src/tools/clu_base64.c index 1cab67f1..13430e9e 100644 --- a/src/tools/clu_base64.c +++ b/src/tools/clu_base64.c @@ -23,6 +23,7 @@ #include #include #include +#include static const struct option base64_options[] = { {"-in", required_argument, 0, WOLFCLU_INFILE }, @@ -117,21 +118,23 @@ int wolfCLU_Base64Setup(int argc, char** argv) } if (ret == WOLFCLU_SUCCESS) { + WOLFCLU_IO ioIn = {0}; if (inFile == NULL) { - ret = wolfCLU_ReadIo(WOLFCLU_IO_READABLE_STREAM, stdin, &input, - &inputSz); + ioIn = wolfCLU_OpenIo_fp(stdin, WOLFCLU_IO_READABLE_STREAM | + WOLFCLU_IO_NOCLOSE); } else { - XFILE fp = XFOPEN(inFile, "rb"); - if (fp == XBADFILE) { - wolfCLU_LogError("Could not open file %s", inFile); - ret = WOLFCLU_FATAL_ERROR; - } - else { - ret = wolfCLU_ReadIo(WOLFCLU_IO_READABLE_FILE, fp, - &input, &inputSz); - XFCLOSE(fp); - } + ioIn = wolfCLU_OpenIo_file(inFile, WOLFCLU_IO_READABLE_FILE); + } + + if (ioIn.type <= 0 || + wolfCLU_ReadIo(&ioIn, &input, &inputSz, 0) != + WOLFCLU_SUCCESS) { + ret = WOLFCLU_FATAL_ERROR; + } + if (ioIn.type > 0 && wolfCLU_CloseIo(&ioIn) != WOLFCLU_SUCCESS) { + wolfCLU_LogError("Could not close io"); + ret = WOLFCLU_FATAL_ERROR; } } @@ -248,32 +251,32 @@ int wolfCLU_Base64Setup(int argc, char** argv) } if (ret == WOLFCLU_SUCCESS) { + WOLFCLU_IO ioOut = {0}; if (outFile == NULL) { - ret = wolfCLU_WriteIo(WOLFCLU_IO_WRITABLE_STREAM, stdout, output, - outputSz); + ioOut = wolfCLU_OpenIo_fp(stdout, WOLFCLU_IO_WRITABLE_STREAM | + WOLFCLU_IO_NOCLOSE); } else { - XFILE fp = XFOPEN(outFile, "wb"); - if (fp == XBADFILE) { - wolfCLU_LogError("Could not open file %s", outFile); - ret = WOLFCLU_FATAL_ERROR; - } - else { - ret = wolfCLU_WriteIo(WOLFCLU_IO_WRITABLE_FILE, fp, - output, outputSz); - if (XFCLOSE(fp) != 0 && ret == WOLFCLU_SUCCESS) { - wolfCLU_LogError("Could not write file %s", outFile); - ret = WOLFCLU_FATAL_ERROR; - } - } + ioOut = wolfCLU_OpenIo_file(outFile, WOLFCLU_IO_WRITABLE_FILE); + } + + if (ioOut.type <= 0 || + wolfCLU_WriteIo(&ioOut, output, outputSz) != WOLFCLU_SUCCESS) { + ret = WOLFCLU_FATAL_ERROR; + } + if (ioOut.type > 0 && wolfCLU_CloseIo(&ioOut) != WOLFCLU_SUCCESS) { + wolfCLU_LogError("Could not close io"); + ret = WOLFCLU_FATAL_ERROR; } } /* Clean up */ if (input != NULL) { + wolfCLU_ForceZero(input, inputSz); XFREE(input, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); } if (output != NULL) { + wolfCLU_ForceZero(output, outputSz); XFREE(output, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); } #ifdef WOLFSSL_PEM_TO_DER diff --git a/src/tools/clu_io.c b/src/tools/clu_io.c index 295eed74..421fa7fa 100644 --- a/src/tools/clu_io.c +++ b/src/tools/clu_io.c @@ -1,6 +1,6 @@ /* clu_io.c * - * Copyright (C) 2006-2025 wolfSSL Inc. + * Copyright (C) 2006-2026 wolfSSL Inc. * * This file is part of wolfSSL. * @@ -21,6 +21,7 @@ #include #include +#include #include /* Windows opens stdout and stdin in text mode, translating 0x0A <-> 0x0D 0x0A. @@ -30,15 +31,33 @@ #include #endif + +enum WOLFCLU_IO_TYPE_SETS { + WOLFCLU_IO_READABLE = + WOLFCLU_IO_READABLE_STREAM | + WOLFCLU_IO_READABLE_FILE, + + WOLFCLU_IO_WRITABLE = + WOLFCLU_IO_WRITABLE_STREAM | + WOLFCLU_IO_WRITABLE_FILE, + + WOLFCLU_IO_RW = + WOLFCLU_IO_READABLE_STREAM | + WOLFCLU_IO_READABLE_FILE | + WOLFCLU_IO_WRITABLE_STREAM | + WOLFCLU_IO_WRITABLE_FILE, +}; + typedef struct WOLFCLU_IO_BUFFER { byte* outBuf; - int len; - int cap; + word32 len; + word32 cap; } WOLFCLU_IO_BUFFER; +#ifndef WOLFCLU_NO_FILESYSTEM /* move the data to a new allocation of newCap bytes, wiping the old one so no * stale copies are left on the heap */ -static int ResizeBuffer(WOLFCLU_IO_BUFFER* buffer, int newCap) +static int ResizeBuffer(WOLFCLU_IO_BUFFER* buffer, word32 newCap) { byte* tmp = (byte*)XMALLOC(newCap, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); if (tmp == NULL) { @@ -55,41 +74,54 @@ static int ResizeBuffer(WOLFCLU_IO_BUFFER* buffer, int newCap) return WOLFCLU_SUCCESS; } -/* read a stream of unknown length, growing the buffer as we go */ -static int StreamRead(XFILE fp, WOLFCLU_IO_BUFFER* buffer) +/* read a stream of unknown length, growing the buffer as we go. A stream with + * no end, like /dev/urandom, is read until WOLFCLU_IO_MAX_READ_SZ is passed */ +static int StreamRead(XFILE fp, WOLFCLU_IO_BUFFER* buffer, word32 limit) { - sword32 bytesRead = 0; + word32 bytesRead = 0; int newCap; int ret = WOLFCLU_SUCCESS; while (1) { if (buffer->cap == buffer->len) { - if (buffer->cap == INT_MAX) { + if (buffer->cap == limit) { /* try to read one more byte to make sure that if the * one more byte is EOF we break out with a full buffer */ char lookOneMore; - if (XFREAD(&lookOneMore, 1, 1, fp) == 0 && !XFERROR(fp)) + if (XFREAD(&lookOneMore, 1, 1, fp) != 0) { + wolfCLU_LogError("input too big needs to be %u " + "bytes or less", limit); + ret = WOLFCLU_FATAL_ERROR; + break; + } + else if (XFERROR(fp) != 0) { + wolfCLU_LogError("Error occured while reading the file"); + ret = WOLFCLU_FATAL_ERROR; + break; + } + else { + /* we have read exactly to our limit! */ break; + } + } - wolfCLU_LogError("input too big needs to be %d " - "bytes or less", INT_MAX); - ret = WOLFCLU_FATAL_ERROR; - break; + if (buffer->cap == 0) { + newCap = limit > 1024 ? 1024 : limit; } - if (buffer->cap > ((INT_MAX - 1024) / 2)) { - newCap = INT_MAX; + else if (buffer->cap > (limit / 3)) { + newCap = limit; } else { - newCap = buffer->cap * 2 + 1024; + newCap = buffer->cap * 2; } ret = ResizeBuffer(buffer, newCap); if (ret != WOLFCLU_SUCCESS) { break; } } - bytesRead = (sword32)XFREAD(buffer->outBuf + buffer->len, + bytesRead = (word32)XFREAD(buffer->outBuf + buffer->len, sizeof(*buffer->outBuf), buffer->cap - buffer->len, fp); /* check for errors first, ports without ferror define XFERROR as 0 */ - if (bytesRead < 0 || XFERROR(fp)) { + if (XFERROR(fp)) { wolfCLU_LogError("Error while reading input stream."); ret = WOLFCLU_FATAL_ERROR; break; @@ -98,7 +130,7 @@ static int StreamRead(XFILE fp, WOLFCLU_IO_BUFFER* buffer) break; /* EOF */ } - buffer->len += bytesRead; + buffer->len += (word32)bytesRead; } return ret; @@ -106,20 +138,21 @@ static int StreamRead(XFILE fp, WOLFCLU_IO_BUFFER* buffer) /* seek to get the length, then read it in one allocation. Inputs that can't * be sized this way, like pipes and devices, are read as a stream */ -static int FileRead(XFILE fp, WOLFCLU_IO_BUFFER* buffer) +static int FileRead(XFILE fp, WOLFCLU_IO_BUFFER* buffer, word32 limit) { - sword32 bytesRead = 0; + word32 bytesRead = 0; long fileSize = 0; if (XFSEEK(fp, 0, XSEEK_END) != 0) { /* not seekable, e.g. a pipe */ - return StreamRead(fp, buffer); + return StreamRead(fp, buffer, limit); } if ((fileSize = XFTELL(fp)) < 0) { wolfCLU_LogError("Could not get length of file"); return WOLFCLU_FATAL_ERROR; } - else if (fileSize > INT_MAX) { - wolfCLU_LogError("File is too large max is %d bytes", INT_MAX); + else if (fileSize > limit) { + wolfCLU_LogError("File is too large max is %d bytes", + limit); return WOLFCLU_FATAL_ERROR; } if (XFSEEK(fp, 0, XSEEK_SET) != 0) { @@ -127,118 +160,282 @@ static int FileRead(XFILE fp, WOLFCLU_IO_BUFFER* buffer) return WOLFCLU_FATAL_ERROR; } if (fileSize == 0) { - /* devices report a size of 0, an empty file just reads nothing */ - return StreamRead(fp, buffer); + /* devices report a size of 0, an empty file just reads nothing. An + * endless device, like /dev/zero, reads until the max read size */ + return StreamRead(fp, buffer, limit); } - buffer->len = (int)fileSize; + buffer->len = (word32)fileSize; buffer->outBuf = (byte*)XMALLOC(buffer->len, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); if (buffer->outBuf == NULL) { - wolfCLU_LogError("Could not allocate space for io " - "read."); + wolfCLU_LogError("Could not allocate space for io read."); return WOLFCLU_FATAL_ERROR; } buffer->cap = buffer->len; - bytesRead = (sword32)XFREAD(buffer->outBuf, sizeof(*buffer->outBuf), + bytesRead = (word32)XFREAD(buffer->outBuf, sizeof(*buffer->outBuf), buffer->len, fp); - if (bytesRead != buffer->len) { + /* check for errors first, ports without ferror define XFERROR as 0 */ + if (XFERROR(fp)) { wolfCLU_LogError("Could not read all of the file data"); return WOLFCLU_FATAL_ERROR; } + /* a short read is EOF, e.g. sysfs files report a size larger than their + * data */ + buffer->len = (int)bytesRead; return WOLFCLU_SUCCESS; } +#endif -int wolfCLU_ReadIo(enum WOLFCLU_IO_TYPE ioType, XFILE fp, byte** buf, - word32* len) +int wolfCLU_ReadIo(WOLFCLU_IO* io, byte** buf, word32* len, word32 limit) { int ret = WOLFCLU_SUCCESS; +#ifndef WOLFCLU_NO_FILESYSTEM WOLFCLU_IO_BUFFER buffer = {0}; - if (fp == XBADFILE || buf == NULL || len == NULL) { + if (io == NULL || io->type <= 0 || buf == NULL || len == NULL) { wolfCLU_LogError("Bad arg passed to wolfCLU_ReadIo"); return BAD_FUNC_ARG; } + if (limit == 0) { + limit = UINT_MAX; + } + *buf = NULL; *len = 0; - if (!(ioType & WOLFCLU_IO_READABLE)) { + if (!(io->type & WOLFCLU_IO_READABLE)) { wolfCLU_LogError("Invalid ioType passed to wolfCLU_ReadIo"); return BAD_FUNC_ARG; } -#ifdef WOLFCLU_NO_FILESYSTEM - if (ioType & WOLFCLU_IO_RW_FILE) { - wolfCLU_LogError("Cannot read files when compiled with " - "WOLFCLU_NO_FILESYSTEM"); - return NOT_COMPILED_IN; - } -#endif -#ifdef _WIN32 - /* binary mode, see note at top of file */ - (void)_setmode(_fileno(fp), _O_BINARY); -#endif - if (ioType & WOLFCLU_IO_READABLE_FILE) { - ret = FileRead(fp, &buffer); + if (io->type & WOLFCLU_IO_READABLE_FILE) { + ret = FileRead(io->fp, &buffer, limit); } else { - ret = StreamRead(fp, &buffer); + ret = StreamRead(io->fp, &buffer, limit); } if (ret != WOLFCLU_SUCCESS) { if (buffer.outBuf != NULL) { - wolfCLU_ForceZero(buffer.outBuf, buffer.len); + wolfCLU_ForceZero(buffer.outBuf, buffer.cap); XFREE(buffer.outBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + buffer.outBuf = NULL; } - return ret; } else { if (buffer.len == 0 && buffer.outBuf != NULL) { + wolfCLU_ForceZero(buffer.outBuf, buffer.cap); XFREE(buffer.outBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); buffer.outBuf = NULL; } *len = buffer.len; *buf = buffer.outBuf; - return ret; } + return ret; +#else + (void)io; + (void)buf; + (void)len; + ret = NOT_COMPILED_IN; + return ret; +#endif } -int wolfCLU_WriteIo(enum WOLFCLU_IO_TYPE ioType, XFILE fp, - const byte* buf, word32 len) +int wolfCLU_WriteIo(WOLFCLU_IO* io, const byte* buf, word32 len) { int ret = WOLFCLU_SUCCESS; - if (fp == XBADFILE || (buf == NULL && len > 0) || len > INT_MAX) { +#ifndef WOLFCLU_NO_FILESYSTEM + if (io == NULL || io->type <= 0 || (buf == NULL && len > 0)) { wolfCLU_LogError("Bad arg passed to wolfCLU_WriteIo"); return BAD_FUNC_ARG; } - if (!(ioType & WOLFCLU_IO_WRITABLE)) { + if (!(io->type & WOLFCLU_IO_WRITABLE)) { wolfCLU_LogError("Invalid ioType passed to wolfCLU_WriteIo"); return BAD_FUNC_ARG; } -#ifdef WOLFCLU_NO_FILESYSTEM - if (ioType & WOLFCLU_IO_RW_FILE) { - wolfCLU_LogError("Cannot write out to a file when compiled with " - "WOLFCLU_NO_FILESYSTEM"); - return NOT_COMPILED_IN; - } -#endif -#ifdef _WIN32 - /* binary mode, see note at top of file */ - (void)_setmode(_fileno(fp), _O_BINARY); -#endif - if (len > 0 && XFWRITE(buf, sizeof(*buf), (int)len, fp) != len) { + if (len > 0 && XFWRITE(buf, sizeof(*buf), len, io->fp) != len) { wolfCLU_LogError("Could not write buffer out to target"); ret = WOLFCLU_FATAL_ERROR; } #ifdef XFFLUSH /* the write may only be buffered, flush so errors like a full disk are * caught here */ - if (ret == WOLFCLU_SUCCESS && XFFLUSH(fp) != 0) { + if (ret == WOLFCLU_SUCCESS && XFFLUSH(io->fp) != 0) { wolfCLU_LogError("Could not flush buffer out to target"); ret = WOLFCLU_FATAL_ERROR; } #endif return ret; + +#else + (void)io; + (void)buf; + (void)len; + wolfCLU_LogError("There is no filesystem in this config: " + "cannot do file operations"); + ret = NOT_COMPILED_IN; + return ret; +#endif } +WOLFCLU_IO wolfCLU_OpenIo_fd(int fd, enum WOLFCLU_IO_TYPE type) +{ + WOLFCLU_IO io = {WOLFCLU_IO_NOT_OPEN, XBADFILE}; +#if !defined(WOLFCLU_NO_FILESYSTEM) && defined(XFDOPEN) + XFILE fp = XBADFILE; + if (fd < 0 || type <= 0) { + wolfCLU_LogError("Bad arg passed to wolfCLU_OpenIo_fd"); + io.type = WOLFCLU_IO_ERROR; + return io; + } + + if ((type & WOLFCLU_IO_READABLE) && (type & WOLFCLU_IO_WRITABLE)) { + fp = XFDOPEN(fd, "rb+"); + } + else if (type & WOLFCLU_IO_READABLE) { + fp = XFDOPEN(fd, "rb"); + } + else if (type & WOLFCLU_IO_WRITABLE) { + fp = XFDOPEN(fd, "wb"); + } + else { + wolfCLU_LogError("Bad type passed to wolfCLU_OpenIo_fd"); + io.type = WOLFCLU_IO_ERROR; + } + if (fp == XBADFILE) { + wolfCLU_LogError("Could not open file pointer from file descriptor"); + io.type = WOLFCLU_IO_ERROR; + } + + if (io.type == WOLFCLU_IO_NOT_OPEN) { + io.fp = fp; + io.type = type; +#ifdef _WIN32 + /* binary mode */ + (void)_setmode(_fileno(io.fp), _O_BINARY); +#endif + } + return io; +#else + (void)type; + (void)fd; +#if defined(WOLFCLU_NO_FILESYSTEM) + wolfCLU_LogError("There is no filesystem in this config: " + "cannot do file operations"); +#endif +#if !defined(XFDOPEN) + wolfCLU_LogError("Cannot open file pointer from file descriptor"); +#endif + io.type = WOLFCLU_IO_NOT_COMPILED_IN; + return io; +#endif +} + +WOLFCLU_IO wolfCLU_OpenIo_fp(XFILE fp, enum WOLFCLU_IO_TYPE type) +{ + WOLFCLU_IO io = {WOLFCLU_IO_NOT_OPEN, XBADFILE}; +#ifndef WOLFCLU_NO_FILESYSTEM + if (fp == XBADFILE) { + wolfCLU_LogError("Bad file pointer passed to wolfCLU_OpenIo_fp"); + io.type = WOLFCLU_IO_ERROR; + } + + /* check that at least one valid type bit is set */ + if (!(type & WOLFCLU_IO_RW) || type <= 0) { + wolfCLU_LogError("Bad type passed to wolfCLU_OpenIo_fp"); + io.type = WOLFCLU_IO_ERROR; + } + + if (io.type == WOLFCLU_IO_NOT_OPEN) { + io.fp = fp; + io.type = type; +#ifdef _WIN32 + /* binary mode */ + (void)_setmode(_fileno(io.fp), _O_BINARY); +#endif + } + return io; +#else + (void)fp; + (void)type; + wolfCLU_LogError("There is no filesystem in this config: " + "cannot do file operations"); + io.type = WOLFCLU_IO_NOT_COMPILED_IN; + return io; +#endif +} + +WOLFCLU_IO wolfCLU_OpenIo_file(const char* fileName, enum WOLFCLU_IO_TYPE type) +{ + WOLFCLU_IO io = {WOLFCLU_IO_NOT_OPEN, XBADFILE}; +#ifndef WOLFCLU_NO_FILESYSTEM + XFILE fp = XBADFILE; + if (fileName == NULL || type <= 0) { + wolfCLU_LogError("Bad arg passed to wolfCLU_OpenIo_file"); + io.type = WOLFCLU_IO_ERROR; + return io; + } + + if ((type & WOLFCLU_IO_READABLE) && (type & WOLFCLU_IO_WRITABLE)) { + fp = XFOPEN(fileName, "rb+"); + } + else if (type & WOLFCLU_IO_READABLE) { + fp = XFOPEN(fileName, "rb"); + } + else if (type & WOLFCLU_IO_WRITABLE) { + fp = XFOPEN(fileName, "wb"); + } + else { + wolfCLU_LogError("Bad type passed to wolfCLU_OpenIo_file"); + io.type = WOLFCLU_IO_ERROR; + } + if (fp == XBADFILE) { + wolfCLU_LogError("Could not open file pointer from file name: %s", + fileName); + io.type = WOLFCLU_IO_ERROR; + } + + if (io.type == WOLFCLU_IO_NOT_OPEN) { + io.fp = fp; + io.type = type; +#ifdef _WIN32 + /* binary mode */ + (void)_setmode(_fileno(io.fp), _O_BINARY); +#endif + } + return io; +#else + (void)type; + (void)fileName; + wolfCLU_LogError("There is no filesystem in this config: " + "cannot do file operations"); + io.type = WOLFCLU_IO_NOT_COMPILED_IN; + return io; +#endif +} + +int wolfCLU_CloseIo(WOLFCLU_IO* io) +{ +#ifndef WOLFCLU_NO_FILESYSTEM + int ret = WOLFCLU_SUCCESS; + if (io == NULL || io->type <= 0) + return BAD_FUNC_ARG; + + /* the stream is gone even when fclose fails */ + if (!(io->type & WOLFCLU_IO_NOCLOSE) && XFCLOSE(io->fp) != 0) { + ret = WOLFCLU_FATAL_ERROR; + } + + io->fp = XBADFILE; + io->type = WOLFCLU_IO_NOT_OPEN; + + return ret; +#else + (void)io; + wolfCLU_LogError("There is no filesystem in this config: " + "cannot do file operations"); + return NOT_COMPILED_IN; +#endif +} diff --git a/tests/base64/base64-test.py b/tests/base64/base64-test.py index 911ad601..33e5ae18 100644 --- a/tests/base64/base64-test.py +++ b/tests/base64/base64-test.py @@ -201,6 +201,8 @@ def test_missing_input_file(self): result = run_wolfssl("base64", "-in", "test-b64-does-not-exist.bin") self.assertNotEqual(result.returncode, 0, "missing input file should fail") + self.assertNotIn("Could not close io", result.stderr, + "a failed open should not be closed") def test_output_dir_missing(self): """An -out path in a missing directory gives a non-zero exit.""" @@ -210,6 +212,8 @@ def test_output_dir_missing(self): "out.b64")) self.assertNotEqual(result.returncode, 0, "output in a missing directory should fail") + self.assertNotIn("Could not close io", result.stderr, + "a failed open should not be closed") def test_failed_decode_keeps_output(self): """A failed -d does not create or truncate the -out file.""" @@ -268,6 +272,200 @@ def test_output_file_write_error(self): self.assertNotEqual(result.returncode, 0, "write to a full device should fail") + def test_stdin_buffer_boundaries(self): + """Stream reads at and around each buffer grow size (1024, 3072, 7168).""" + rng = random.Random(3) + for size in (1, 1023, 1024, 1025, 3071, 3072, 3073, 7168, 7169): + with self.subTest(size=size): + data = rng.randbytes(size) + result = subprocess.run( + [WOLFSSL_BIN, "base64"], + input=data, + capture_output=True, + timeout=60, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout.replace(b"\n", b""), + base64.b64encode(data), + "stdin encode does not match python base64") + + @unittest.skipUnless(os.path.exists("/dev/stdin"), "needs /dev/stdin") + def test_pipe_input_file_boundaries(self): + """-in on a pipe falls back to stream reads, including at grow sizes.""" + rng = random.Random(4) + for size in (1024, 3072, 7168): + with self.subTest(size=size): + data = rng.randbytes(size) + result = subprocess.run( + [WOLFSSL_BIN, "base64", "-in", "/dev/stdin"], + input=data, + capture_output=True, + timeout=60, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout.replace(b"\n", b""), + base64.b64encode(data), + "pipe input encode does not match python base64") + + def test_stdin_from_regular_file(self): + """stdin redirected from a seekable file is read in full as a stream.""" + in_file = "test-b64-stdin-file.bin" + self.addCleanup(lambda: os.remove(in_file) + if os.path.exists(in_file) else None) + + data = random.Random(5).randbytes(20000) + with open(in_file, "wb") as f: + f.write(data) + + with open(in_file, "rb") as f: + result = subprocess.run( + [WOLFSSL_BIN, "base64"], + stdin=f, + capture_output=True, + timeout=60, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout.replace(b"\n", b""), + base64.b64encode(data), + "redirected stdin encode does not match python base64") + + def test_binary_file_roundtrip(self): + """Every byte value, including NUL, CR and LF, survives -in/-out files.""" + in_file = "test-b64-binary.bin" + encoded_file = "test-b64-binary.b64" + decoded_file = "test-b64-binary.out" + for name in (in_file, encoded_file, decoded_file): + self.addCleanup(lambda n=name: os.remove(n) + if os.path.exists(n) else None) + + data = bytes(range(256)) * 40 + b"\r\n\r\n\x00\x1a" + with open(in_file, "wb") as f: + f.write(data) + + result = run_wolfssl("base64", "-in", in_file, "-out", encoded_file) + self.assertEqual(result.returncode, 0, result.stderr) + with open(encoded_file, "rb") as f: + self.assertEqual(f.read().replace(b"\n", b""), + base64.b64encode(data), + "file encode does not match python base64") + + result = run_wolfssl("base64", "-d", "-in", encoded_file, + "-out", decoded_file) + self.assertEqual(result.returncode, 0, result.stderr) + with open(decoded_file, "rb") as f: + self.assertEqual(f.read(), data, + "binary file round trip does not match") + + @unittest.skipUnless(os.path.exists("/dev/null"), "needs /dev/null") + def test_dev_null_input(self): + """-in on a device that reports a size of 0 gives empty output.""" + for args in (("base64",), ("base64", "-d")): + with self.subTest(args=args): + result = run_wolfssl(*args, "-in", "/dev/null") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout, "", + "empty device should give no output") + + @unittest.skipUnless(os.path.exists("/sys/class/net/lo/address"), + "needs /sys/class/net/lo/address") + def test_short_read_sysfs_file(self): + """-in on a sysfs file that reports a size larger than its data.""" + sysfs_file = "/sys/class/net/lo/address" + with open(sysfs_file, "rb") as f: + data = f.read() + + result = subprocess.run( + [WOLFSSL_BIN, "base64", "-in", sysfs_file], + capture_output=True, + timeout=60, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout.replace(b"\n", b""), + base64.b64encode(data), + "sysfs file encode does not match python base64") + + def test_output_file_truncated(self): + """An existing -out file longer than the output is fully replaced.""" + in_file = "test-b64-trunc.bin" + out_file = "test-b64-trunc.b64" + for name in (in_file, out_file): + self.addCleanup(lambda n=name: os.remove(n) + if os.path.exists(n) else None) + + data = b"short" + with open(in_file, "wb") as f: + f.write(data) + with open(out_file, "wb") as f: + f.write(b"x" * 10000) + + result = run_wolfssl("base64", "-in", in_file, "-out", out_file) + self.assertEqual(result.returncode, 0, result.stderr) + with open(out_file, "rb") as f: + self.assertEqual(f.read().replace(b"\n", b""), + base64.b64encode(data), + "old -out contents were not truncated") + + def test_missing_input_file_no_output(self): + """A missing -in file does not create the -out file.""" + out_file = "test-b64-missing-in.b64" + self.addCleanup(lambda: os.remove(out_file) + if os.path.exists(out_file) else None) + + result = run_wolfssl("base64", "-in", "test-b64-does-not-exist.bin", + "-out", out_file) + self.assertNotEqual(result.returncode, 0, + "missing input file should fail") + self.assertNotIn("Could not close io", result.stderr, + "a failed open should not be closed") + self.assertFalse(os.path.exists(out_file), + "failed read should not create -out") + + def test_input_is_directory(self): + """-in naming a directory gives a non-zero exit.""" + in_dir = "test-b64-in-dir" + os.makedirs(in_dir, exist_ok=True) + self.addCleanup(lambda: os.rmdir(in_dir) + if os.path.isdir(in_dir) else None) + + result = run_wolfssl("base64", "-in", in_dir) + self.assertNotEqual(result.returncode, 0, + "directory as input should fail") + self.assertNotIn("Could not close io", result.stderr, + "closing a dir after a failed read should succeed") + + def test_output_is_directory(self): + """-out naming a directory gives a non-zero exit.""" + out_dir = "test-b64-out-dir" + os.makedirs(out_dir, exist_ok=True) + self.addCleanup(lambda: os.rmdir(out_dir) + if os.path.isdir(out_dir) else None) + + result = run_wolfssl("base64", "-in", + os.path.join(CERTS_DIR, "server-key.der"), + "-out", out_dir) + self.assertNotEqual(result.returncode, 0, + "directory as output should fail") + + @unittest.skipIf(not hasattr(os, "geteuid") or os.geteuid() == 0, + "needs a non-root POSIX user") + def test_unreadable_input_file(self): + """An -in file without read permission gives a non-zero exit.""" + in_file = "test-b64-unreadable.bin" + self.addCleanup(lambda: os.remove(in_file) + if os.path.exists(in_file) else None) + + with open(in_file, "wb") as f: + f.write(b"secret") + os.chmod(in_file, 0o200) + + result = run_wolfssl("base64", "-in", in_file) + self.assertNotEqual(result.returncode, 0, + "unreadable input file should fail") + self.assertNotIn("Could not close io", result.stderr, + "a failed open should not be closed") + self.assertEqual(result.stdout, "", + "unreadable input should give no output") + def test_help(self): """ Test help flag """ result = subprocess.run( diff --git a/wolfCLU.vcxproj b/wolfCLU.vcxproj index b20e3335..306a81c3 100644 --- a/wolfCLU.vcxproj +++ b/wolfCLU.vcxproj @@ -196,6 +196,7 @@ + diff --git a/wolfCLU.vcxproj.filters b/wolfCLU.vcxproj.filters index 9d76d5bc..a6030785 100644 --- a/wolfCLU.vcxproj.filters +++ b/wolfCLU.vcxproj.filters @@ -167,6 +167,9 @@ Header Files + + Header Files + Header Files diff --git a/wolfclu/clu_header_main.h b/wolfclu/clu_header_main.h index 480343bf..b3fa9c93 100644 --- a/wolfclu/clu_header_main.h +++ b/wolfclu/clu_header_main.h @@ -851,60 +851,6 @@ int wolfCLU_ReadCertDer(const char* filename, byte** outDer); */ int wolfCLU_GetStdinPassword(byte* password, word32* passwordSz); -/** - * @brief IO types passed to the IO functions below - */ -enum WOLFCLU_IO_TYPE { - /* base types */ - WOLFCLU_IO_READABLE_STREAM = 1 << 0, - WOLFCLU_IO_WRITABLE_STREAM = 1 << 1, - WOLFCLU_IO_READABLE_FILE = 1 << 2, - WOLFCLU_IO_WRITABLE_FILE = 1 << 3, - - /* type groups */ - WOLFCLU_IO_RW_STREAM = - WOLFCLU_IO_READABLE_STREAM | - WOLFCLU_IO_WRITABLE_STREAM, - - WOLFCLU_IO_RW_FILE = - WOLFCLU_IO_READABLE_FILE | - WOLFCLU_IO_WRITABLE_FILE, - - WOLFCLU_IO_READABLE = - WOLFCLU_IO_READABLE_STREAM | - WOLFCLU_IO_READABLE_FILE, - - WOLFCLU_IO_WRITABLE = - WOLFCLU_IO_WRITABLE_STREAM | - WOLFCLU_IO_WRITABLE_FILE, -}; - -/** - * @brief read all of fp into a new buffer - * @param ioType a readable stream or file type. Streams are read until EOF, - * files are sized with seek first and read as a stream if that fails - * @param fp file pointer to read from - * @param buf pointer to store the buffer, NULL if empty. The caller frees it, - * wiping it first with wolfCLU_ForceZero if it holds secrets - * @param len pointer to store the number of bytes read - * @return WOLFCLU_SUCCESS on success, negative on error - */ -int wolfCLU_ReadIo(enum WOLFCLU_IO_TYPE ioType, XFILE fp, byte** buf, - word32* len); - -/** - * @brief write len bytes of buf to fp and flush it - * @param ioType a writable stream or file type - * @param fp file pointer to write to - * @param buf buffer to write, may be NULL when len is 0 - * @param len number of bytes to write - * @return WOLFCLU_SUCCESS on success, negative on error - */ -int wolfCLU_WriteIo(enum WOLFCLU_IO_TYPE ioType, XFILE fp, const byte* buf, - word32 len); - - - #ifdef __cplusplus } #endif diff --git a/wolfclu/clu_io.h b/wolfclu/clu_io.h new file mode 100644 index 00000000..ca6635eb --- /dev/null +++ b/wolfclu/clu_io.h @@ -0,0 +1,131 @@ +/* clu_io.h + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#ifndef _WOLFSSL_CLU_IO_HEADER_ +#define _WOLFSSL_CLU_IO_HEADER_ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +/** + * @brief IO types passed to the IO functions below + */ +enum WOLFCLU_IO_TYPE { + /*error codes*/ + WOLFCLU_IO_NOT_COMPILED_IN = NOT_COMPILED_IN, + WOLFCLU_IO_ERROR = WOLFCLU_FATAL_ERROR, + + /* base types */ + WOLFCLU_IO_NOT_OPEN = 0, + WOLFCLU_IO_READABLE_STREAM = 1 << 0, + WOLFCLU_IO_WRITABLE_STREAM = 1 << 1, + WOLFCLU_IO_READABLE_FILE = 1 << 2, + WOLFCLU_IO_WRITABLE_FILE = 1 << 3, + + /*behaviors*/ + WOLFCLU_IO_NOCLOSE = 1 << 10, + + /* type groups */ + WOLFCLU_IO_RW_STREAM = + WOLFCLU_IO_READABLE_STREAM | + WOLFCLU_IO_WRITABLE_STREAM, + + WOLFCLU_IO_RW_FILE = + WOLFCLU_IO_READABLE_FILE | + WOLFCLU_IO_WRITABLE_FILE, +}; + +#ifndef XFILE +#define XFILE void* +#endif + +#ifndef XBADFILE +#define XBADFILE NULL +#endif + + +typedef struct WOLFCLU_IO { + enum WOLFCLU_IO_TYPE type; + XFILE fp; +}WOLFCLU_IO; + +/** + * @brief Open a WOLFCLU_IO from a file descriptor, file pointer or file name. + * type sets the read/write mode and can include WOLFCLU_IO_NOCLOSE to + * leave the underlying file open on wolfCLU_CloseIo, e.g. for stdin + * @param fd file descriptor to open a file pointer on. After a successful open + * the fd is owned by the IO and closed by wolfCLU_CloseIo, on failure + * the caller still owns it + * @param fp file pointer to wrap, closed by wolfCLU_CloseIo unless + * WOLFCLU_IO_NOCLOSE is set + * @param fileName path of the file to open, writable files are truncated, + * read/write files are not truncated. + * @param type WOLFCLU_IO_TYPE flags for the IO + * @return an IO with type > 0 on success. On failure type is <= 0 + * (WOLFCLU_IO_ERROR or WOLFCLU_IO_NOT_COMPILED_IN) and the IO must not + * be passed to wolfCLU_CloseIo + */ +WOLFCLU_IO wolfCLU_OpenIo_fd(int fd, enum WOLFCLU_IO_TYPE type); +WOLFCLU_IO wolfCLU_OpenIo_fp(XFILE fp, enum WOLFCLU_IO_TYPE type); +WOLFCLU_IO wolfCLU_OpenIo_file(const char* fileName, enum WOLFCLU_IO_TYPE type); + +/** + * @brief close an open IO. The file is closed unless WOLFCLU_IO_NOCLOSE is + * set. The IO is reset to WOLFCLU_IO_NOT_OPEN even if the close fails + * @param io IO from a successful wolfCLU_OpenIo_* call + * @return WOLFCLU_SUCCESS on success, BAD_FUNC_ARG if io is not open, + * WOLFCLU_FATAL_ERROR if closing the file failed + */ +int wolfCLU_CloseIo(WOLFCLU_IO* io); + +/** + * @brief read file/stream data in to an allocated buffer. If limit is + * 0 it will default to UINT_MAX otherwise it will be the max number of + * bytes read from the file/stream. + * @param io open IO with a readable type + * @param buf pointer to store the buffer, NULL if empty. The caller frees it, + * wiping it first with wolfCLU_ForceZero if it holds secrets + * @param len pointer to store the number of bytes read + * @param limit maximum number of bytes that we want to read from a + * file or stream; 0 defaults to UINT_MAX bound + * @return WOLFCLU_SUCCESS on success, negative on error + */ +int wolfCLU_ReadIo(WOLFCLU_IO* io, byte** buf, word32* len, word32 limit); + +/** + * @brief write len bytes of buf to io and flush it + * @param io open IO with a writable type + * @param buf buffer to write, may be NULL when len is 0 + * @param len number of bytes to write + * @return WOLFCLU_SUCCESS on success, negative on error + */ +int wolfCLU_WriteIo(WOLFCLU_IO* io, const byte* buf, word32 len); + +#ifdef __cplusplus +} +#endif + +#endif /*_WOLFSSL_CLU_IO_HEADER_*/ diff --git a/wolfclu/include.am b/wolfclu/include.am index ffd25213..bc9c4559 100644 --- a/wolfclu/include.am +++ b/wolfclu/include.am @@ -10,6 +10,7 @@ nobase_include_HEADERS+=wolfclu/version.h \ wolfclu/clu_optargs.h \ wolfclu/clu_log.h \ wolfclu/clu_error_codes.h \ + wolfclu/clu_io.h \ wolfclu/x509/clu_cert.h \ wolfclu/x509/clu_parse.h \ wolfclu/x509/clu_request.h \