diff --git a/.gitignore b/.gitignore index 3902801f..24dfbfed 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,9 @@ wolfssl test-suite.log tests/*/*.log tests/*/*.trs +# check_PROGRAMS binaries left in the tree by an in-tree "make check" +tests/tools/tools_unit_test +tests/tools/tools_unit_test.exe ecckey src/config.h src/config.h.in diff --git a/Makefile.am b/Makefile.am index 6ef17b86..6e8e0a53 100644 --- a/Makefile.am +++ b/Makefile.am @@ -89,6 +89,7 @@ endif include src/include.am include wolfclu/include.am +include tests/tools/include.am if HAVE_PYTHON include tests/dh/include.am include tests/dsa/include.am @@ -117,10 +118,11 @@ TESTS += $(check_PROGRAMS) check_SCRIPTS+= $(dist_noinst_SCRIPTS) TESTS += $(check_SCRIPTS) -# Automake's test driver writes .log/.trs files next to each test script. -# When tests live in the source tree (no VPATH), those files land in tests/, -# where EXTRA_DIST+=tests would otherwise sweep them into the tarball and -# break `make distcheck` via stale VPATH lookups. +# Automake's test driver writes .log/.trs files next to each test script, and +# an in-tree build leaves the compiled check_PROGRAMS binaries and their .o +# files there too. When tests live in the source tree (no VPATH), all of that +# lands in tests/, where EXTRA_DIST+=tests would otherwise sweep it into the +# tarball and break `make distcheck` via stale VPATH lookups. # Generate the compressed manpages into the tarball from their .1 sources, # so the .gz copies are never hand-maintained in git. These ship in the release # tarball for downstream packaging; they are intentionally not installed @@ -130,6 +132,12 @@ TESTS += $(check_SCRIPTS) dist-hook: find $(distdir)/tests -name '*.log' -delete find $(distdir)/tests -name '*.trs' -delete + find $(distdir)/tests -name '*.o' -delete + find $(distdir)/tests -name '.dirstamp' -delete + find $(distdir)/tests \( -name '.deps' -o -name '.libs' \) -type d -prune -exec rm -rf {} + + for p in $(check_PROGRAMS); do \ + rm -f "$(distdir)/$$p"; \ + done # Always strip stale .1.gz from the tarball (local manpages-gz output or a # prior dist may have left them in manpages/). Regenerate only when enabled. chmod u+w $(distdir)/manpages 2>/dev/null || true diff --git a/src/crypto/clu_crypto_setup.c b/src/crypto/clu_crypto_setup.c index 5ed6f67e..e3dc173b 100644 --- a/src/crypto/clu_crypto_setup.c +++ b/src/crypto/clu_crypto_setup.c @@ -211,6 +211,16 @@ static const struct option crypt_options[] = { #endif /* returns WOLFCLU_SUCCESS on success */ +/* Zero sensitive data before freeing. */ +static void wolfCLU_zeroAndFreeCryptoBins(byte* pwdKey, byte* iv, byte* key, + char* mode, int keySize, int block) +{ + wolfCLU_ForceZero(key, keySize); + wolfCLU_ForceZero(pwdKey, keySize + block); + wolfCLU_ForceZero(iv, block); + wolfCLU_freeBins(pwdKey, iv, key, (byte*)mode, NULL); +} + int wolfCLU_setup(int argc, char** argv, char action) { #ifndef WOLFCLU_NO_FILESYSTEM @@ -311,7 +321,7 @@ int wolfCLU_setup(int argc, char** argv, char action) switch (option) { case ARG_FOUND_TWICE: wolfCLU_LogError("Found duplicate argument"); - wolfCLU_freeBins(pwdKey, iv, key, (byte*)mode, NULL); + wolfCLU_zeroAndFreeCryptoBins(pwdKey, iv, key, mode, keySize, block); return WOLFCLU_FATAL_ERROR; case WOLFCLU_PASSWORD_SOURCE: @@ -320,7 +330,7 @@ int wolfCLU_setup(int argc, char** argv, char action) /* On an unsupported source wolfCLU_GetPassword zeroes the buffer * and fails. Bail out so we do not encrypt under an empty key. */ if (ret != WOLFCLU_SUCCESS) { - wolfCLU_freeBins(pwdKey, iv, key, (byte*)mode, NULL); + wolfCLU_zeroAndFreeCryptoBins(pwdKey, iv, key, mode, keySize, block); return ret; } pwdKeyChk = 1; @@ -329,7 +339,7 @@ int wolfCLU_setup(int argc, char** argv, char action) case WOLFCLU_PASSWORD: if (optarg == NULL) { - wolfCLU_freeBins(pwdKey, iv, key, (byte*)mode, NULL); + wolfCLU_zeroAndFreeCryptoBins(pwdKey, iv, key, mode, keySize, block); return WOLFCLU_FATAL_ERROR; } else { @@ -354,14 +364,14 @@ int wolfCLU_setup(int argc, char** argv, char action) case WOLFCLU_KEY: /* hex key string from the command line */ if (optarg == NULL) { wolfCLU_LogError("no key passed in.."); - wolfCLU_freeBins(pwdKey, iv, key, (byte*)mode, NULL); + wolfCLU_zeroAndFreeCryptoBins(pwdKey, iv, key, mode, keySize, block); return WOLFCLU_FATAL_ERROR; } ret = wolfCLU_loadHexKeyInto(key, (keySize + 7) / 8, optarg, (word32)XSTRLEN(optarg)); if (ret != WOLFCLU_SUCCESS) { - wolfCLU_freeBins(pwdKey, iv, key, (byte*)mode, NULL); + wolfCLU_zeroAndFreeCryptoBins(pwdKey, iv, key, mode, keySize, block); return ret; } keyCheck = 1; @@ -374,13 +384,13 @@ int wolfCLU_setup(int argc, char** argv, char action) byte* ivTmp = NULL; word32 ivTmpSz = 0; if (optarg == NULL) { - wolfCLU_freeBins(pwdKey, iv, key, (byte*)mode, NULL); + wolfCLU_zeroAndFreeCryptoBins(pwdKey, iv, key, mode, keySize, block); return WOLFCLU_FATAL_ERROR; } ivString = (char*)XMALLOC(XSTRLEN(optarg) + 1, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); if (ivString == NULL) { - wolfCLU_freeBins(pwdKey, iv, key, (byte*)mode, NULL); + wolfCLU_zeroAndFreeCryptoBins(pwdKey, iv, key, mode, keySize, block); return MEMORY_E; } XSTRLCPY(ivString, optarg, XSTRLEN(optarg) + 1); @@ -396,7 +406,7 @@ int wolfCLU_setup(int argc, char** argv, char action) if (ret != WOLFCLU_SUCCESS) { WOLFCLU_LOG(WOLFCLU_E0, "failed during conversion of IV, ret = %d", ret); - wolfCLU_freeBins(pwdKey, iv, key, (byte*)mode, NULL); + wolfCLU_zeroAndFreeCryptoBins(pwdKey, iv, key, mode, keySize, block); return WOLFCLU_FATAL_ERROR; } if ((int)ivTmpSz != block) { @@ -405,7 +415,7 @@ int wolfCLU_setup(int argc, char** argv, char action) block, (unsigned int)ivTmpSz); wolfCLU_ForceZero(ivTmp, ivTmpSz); XFREE(ivTmp, NULL, DYNAMIC_TYPE_TMP_BUFFER); - wolfCLU_freeBins(pwdKey, iv, key, (byte*)mode, NULL); + wolfCLU_zeroAndFreeCryptoBins(pwdKey, iv, key, mode, keySize, block); return WOLFCLU_FATAL_ERROR; } XMEMCPY(iv, ivTmp, ivTmpSz); @@ -461,7 +471,7 @@ int wolfCLU_setup(int argc, char** argv, char action) if (optarg == NULL) { wolfCLU_LogError("no key file passed in.."); - wolfCLU_freeBins(pwdKey, iv, key, (byte*)mode, NULL); + wolfCLU_zeroAndFreeCryptoBins(pwdKey, iv, key, mode, keySize, block); return WOLFCLU_FATAL_ERROR; } @@ -471,7 +481,7 @@ int wolfCLU_setup(int argc, char** argv, char action) keyBio = wolfSSL_BIO_new_file(optarg, "rb"); if (keyBio == NULL) { wolfCLU_LogError("could not open key file '%s'", optarg); - wolfCLU_freeBins(pwdKey, iv, key, (byte*)mode, NULL); + wolfCLU_zeroAndFreeCryptoBins(pwdKey, iv, key, mode, keySize, block); return WOLFCLU_FATAL_ERROR; } @@ -480,7 +490,7 @@ int wolfCLU_setup(int argc, char** argv, char action) wolfCLU_LogError("key file '%s' is empty or unreadable", optarg); wolfSSL_BIO_free(keyBio); - wolfCLU_freeBins(pwdKey, iv, key, (byte*)mode, NULL); + wolfCLU_zeroAndFreeCryptoBins(pwdKey, iv, key, mode, keySize, block); return WOLFCLU_FATAL_ERROR; } @@ -488,7 +498,7 @@ int wolfCLU_setup(int argc, char** argv, char action) DYNAMIC_TYPE_TMP_BUFFER); if (fileBuf == NULL) { wolfSSL_BIO_free(keyBio); - wolfCLU_freeBins(pwdKey, iv, key, (byte*)mode, NULL); + wolfCLU_zeroAndFreeCryptoBins(pwdKey, iv, key, mode, keySize, block); return MEMORY_E; } @@ -497,7 +507,7 @@ int wolfCLU_setup(int argc, char** argv, char action) wolfCLU_ForceZero(fileBuf, fileLen); XFREE(fileBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); wolfSSL_BIO_free(keyBio); - wolfCLU_freeBins(pwdKey, iv, key, (byte*)mode, NULL); + wolfCLU_zeroAndFreeCryptoBins(pwdKey, iv, key, mode, keySize, block); return WOLFCLU_FATAL_ERROR; } wolfSSL_BIO_free(keyBio); @@ -528,7 +538,7 @@ int wolfCLU_setup(int argc, char** argv, char action) if (keyString == NULL) { wolfCLU_ForceZero(fileBuf, fileLen); XFREE(fileBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - wolfCLU_freeBins(pwdKey, iv, key, (byte*)mode, NULL); + wolfCLU_zeroAndFreeCryptoBins(pwdKey, iv, key, mode, keySize, block); return MEMORY_E; } /* Copy out hex characters, skipping any embedded @@ -549,7 +559,7 @@ int wolfCLU_setup(int argc, char** argv, char action) wolfCLU_ForceZero(fileBuf, fileLen); XFREE(fileBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); if (ret != WOLFCLU_SUCCESS) { - wolfCLU_freeBins(pwdKey, iv, key, (byte*)mode, NULL); + wolfCLU_zeroAndFreeCryptoBins(pwdKey, iv, key, mode, keySize, block); return ret; } } @@ -566,7 +576,7 @@ int wolfCLU_setup(int argc, char** argv, char action) "Invalid Key. Must match algorithm key size."); wolfCLU_ForceZero(fileBuf, fileLen); XFREE(fileBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - wolfCLU_freeBins(pwdKey, iv, key, (byte*)mode, NULL); + wolfCLU_zeroAndFreeCryptoBins(pwdKey, iv, key, mode, keySize, block); return WOLFCLU_FATAL_ERROR; } XMEMCPY(key, fileBuf, fileLen); @@ -595,7 +605,7 @@ int wolfCLU_setup(int argc, char** argv, char action) hashType = wolfSSL_EVP_get_digestbyname(optarg); if (hashType == NULL) { wolfCLU_LogError("Invalid digest name"); - wolfCLU_freeBins(pwdKey, iv, key, (byte*)mode, NULL); + wolfCLU_zeroAndFreeCryptoBins(pwdKey, iv, key, mode, keySize, block); return WOLFCLU_FATAL_ERROR; } break; @@ -617,7 +627,7 @@ int wolfCLU_setup(int argc, char** argv, char action) WOLFCLU_LOG(WOLFCLU_L0, "Please type \"wolfssl -decrypt -help\" for decryption" " usage \n"); - wolfCLU_freeBins(pwdKey, iv, key, (byte*)mode, NULL); + wolfCLU_zeroAndFreeCryptoBins(pwdKey, iv, key, mode, keySize, block); return WOLFCLU_FATAL_ERROR; } /* if no pwdKey is provided */ @@ -640,7 +650,7 @@ int wolfCLU_setup(int argc, char** argv, char action) "-in flag was not set, please enter a string or" " file name to be encrypted: "); if (ret != WOLFCLU_SUCCESS) { - wolfCLU_freeBins(pwdKey, iv, key, (byte*)mode, NULL); + wolfCLU_zeroAndFreeCryptoBins(pwdKey, iv, key, mode, keySize, block); return WOLFCLU_FATAL_ERROR; } WOLFCLU_LOG(WOLFCLU_L0, "Encrypting :\"%s\"", inName); @@ -650,13 +660,13 @@ int wolfCLU_setup(int argc, char** argv, char action) if (encCheck == 1 && decCheck == 1) { WOLFCLU_LOG(WOLFCLU_E0, "Encrypt and decrypt simultaneously is invalid"); - wolfCLU_freeBins(pwdKey, iv, key, (byte*)mode, NULL); + wolfCLU_zeroAndFreeCryptoBins(pwdKey, iv, key, mode, keySize, block); return WOLFCLU_FATAL_ERROR; } if (inCheck == 0 && decCheck == 1) { wolfCLU_LogError("File/string to decrypt needed"); - wolfCLU_freeBins(pwdKey, iv, key, (byte*)mode, NULL); + wolfCLU_zeroAndFreeCryptoBins(pwdKey, iv, key, mode, keySize, block); return WOLFCLU_FATAL_ERROR; } @@ -666,7 +676,7 @@ int wolfCLU_setup(int argc, char** argv, char action) "-iv was explicitly set, but no -key or -inkey was" " provided. A non-password based key must be supplied" " when setting the -iv flag."); - wolfCLU_freeBins(pwdKey, iv, key, (byte*)mode, NULL); + wolfCLU_zeroAndFreeCryptoBins(pwdKey, iv, key, mode, keySize, block); return WOLFCLU_FATAL_ERROR; } } @@ -679,7 +689,7 @@ int wolfCLU_setup(int argc, char** argv, char action) WOLFCLU_LOG(WOLFCLU_E0, "-key/-inkey requires -iv to be set: an IV must be" " supplied alongside an explicit key."); - wolfCLU_freeBins(pwdKey, iv, key, (byte*)mode, NULL); + wolfCLU_zeroAndFreeCryptoBins(pwdKey, iv, key, mode, keySize, block); return WOLFCLU_FATAL_ERROR; } @@ -701,7 +711,7 @@ int wolfCLU_setup(int argc, char** argv, char action) ret = wolfCLU_readFilename(outNameEnc, sizeof(outNameEnc), "Please enter a name for the output file: "); if (ret != WOLFCLU_SUCCESS) { - wolfCLU_freeBins(pwdKey, iv, key, (byte*)mode, NULL); + wolfCLU_zeroAndFreeCryptoBins(pwdKey, iv, key, mode, keySize, block); return WOLFCLU_FATAL_ERROR; } out = outNameEnc; @@ -723,7 +733,7 @@ int wolfCLU_setup(int argc, char** argv, char action) ret = wolfCLU_readFilename(outNameDec, sizeof(outNameDec), "Please enter a name for the output file: "); if (ret != WOLFCLU_SUCCESS) { - wolfCLU_freeBins(pwdKey, iv, key, (byte*)mode, NULL); + wolfCLU_zeroAndFreeCryptoBins(pwdKey, iv, key, mode, keySize, block); return WOLFCLU_FATAL_ERROR; } out = outNameDec; @@ -743,7 +753,7 @@ int wolfCLU_setup(int argc, char** argv, char action) wolfCLU_ForceZero(key, keySize); wolfCLU_ForceZero(pwdKey, keySize + block); wolfCLU_ForceZero(iv, block); - wolfCLU_freeBins(pwdKey, iv, key, (byte*)mode, NULL); + wolfCLU_zeroAndFreeCryptoBins(pwdKey, iv, key, mode, keySize, block); return ret; #else diff --git a/src/crypto/clu_decrypt.c b/src/crypto/clu_decrypt.c index 8d8faf42..376e45ec 100644 --- a/src/crypto/clu_decrypt.c +++ b/src/crypto/clu_decrypt.c @@ -66,10 +66,11 @@ int wolfCLU_decrypt(int alg, char* mode, byte* pwdKey, byte* key, int size, wolfCLU_LogError("Input file does not exist."); return DECRYPT_ERROR; } - /* opens output file */ - if ((outFile = XFOPEN(out, "wb")) == NULL) { - wolfCLU_LogError("Error creating output file."); + /* opens output file; guarded against -in and -out naming the same + * file, since opening it truncates it and would destroy the + * ciphertext mid-read. */ + if ((outFile = wolfCLU_OpenPairedOutFile(in, out, inFile)) == NULL) { XFCLOSE(inFile); return DECRYPT_ERROR; } @@ -236,7 +237,11 @@ int wolfCLU_decrypt(int alg, char* mode, byte* pwdKey, byte* key, int size, /* Use the wolfssl wc_FreeRng to free rng */ wc_FreeRng(&rng); XFCLOSE(inFile); - XFCLOSE(outFile); + if (wolfCLU_CloseOutFile(outFile, out) != WOLFCLU_SUCCESS && ret == 0) { + /* Only when nothing else already failed, so a more specific earlier + * error is not overwritten. */ + ret = DECRYPT_ERROR; + } (void)mode; (void)alg; diff --git a/src/crypto/clu_encrypt.c b/src/crypto/clu_encrypt.c index 750a6ca8..2d286c5e 100644 --- a/src/crypto/clu_encrypt.c +++ b/src/crypto/clu_encrypt.c @@ -60,6 +60,11 @@ int wolfCLU_encrypt(int alg, char* mode, byte* pwdKey, byte* key, int size, char* userInputBuffer = NULL; /* buffer when input is not a file */ + /* wolfCLU_OpenPairedOutFile() below rejects -in == -out before its + * truncating open, which is what protects the scratch copy the branch + * below writes to -in when it does not already exist -- see + * clu_decrypt.c, which relies on the same single check. */ + if (access (in, F_OK) == -1) { WOLFCLU_LOG(WOLFCLU_L0, "file did not exist, encrypting string following \"-i\"" "instead."); @@ -74,10 +79,14 @@ int wolfCLU_encrypt(int alg, char* mode, byte* pwdKey, byte* key, int size, /* writes the entered text to the input buffer */ XMEMCPY(userInputBuffer, in, inputLength); - /* open the file to write */ - tempInFile = XFOPEN(in, "wb"); + /* open the file to write; use the owner-only, no-follow-symlink + * secure open rather than wolfCLU_OpenOutFile(), since this scratch + * copy holds the plaintext the user is about to encrypt. Without + * this, a dangling symlink at -in would make wolfCLU_OpenOutFile() + * follow it and write the plaintext to the symlink's target + * instead of refusing. */ + tempInFile = wolfCLU_OpenKeyFile(in); if (tempInFile == NULL) { - wolfCLU_LogError("unable to open file %s", in); XFREE(userInputBuffer, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); return BAD_FUNC_ARG; } @@ -129,6 +138,7 @@ int wolfCLU_encrypt(int alg, char* mode, byte* pwdKey, byte* key, int size, if (ret != 0) { XFCLOSE(inFile); + wc_FreeRng(&rng); return ret; } @@ -137,6 +147,7 @@ int wolfCLU_encrypt(int alg, char* mode, byte* pwdKey, byte* key, int size, if (ret != WOLFCLU_SUCCESS) { wolfCLU_LogError("failed to set pwdKey."); XFCLOSE(inFile); + wc_FreeRng(&rng); return ret; } /* move the generated pwdKey to "key" for encrypting */ @@ -145,27 +156,32 @@ int wolfCLU_encrypt(int alg, char* mode, byte* pwdKey, byte* key, int size, } } - /* open the outFile in write mode */ - outFile = XFOPEN(out, "wb"); + /* open the outFile in write mode; re-checks -in/-out and proves outFile + * is not inFile before truncating, so a path swapped in after the + * check above cannot destroy the plaintext mid-read. */ + outFile = wolfCLU_OpenPairedOutFile(in, out, inFile); if (outFile == NULL) { - wolfCLU_LogError("unable to open output file %s", out); XFCLOSE(inFile); + wc_FreeRng(&rng); return WOLFCLU_FATAL_ERROR; } XFWRITE(salt, 1, SALT_SIZE, outFile); XFWRITE(iv, 1, block, outFile); - XFCLOSE(outFile); /* MALLOC 1kB buffers */ input = (byte*) XMALLOC(MAX_LEN, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); if (input == NULL) { XFCLOSE(inFile); + XFCLOSE(outFile); + wc_FreeRng(&rng); return MEMORY_E; } output = (byte*) XMALLOC(MAX_LEN, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); if (output == NULL) { XFCLOSE(inFile); + XFCLOSE(outFile); wolfCLU_freeBins(input, NULL, NULL, NULL, NULL); + wc_FreeRng(&rng); return MEMORY_E; } @@ -196,7 +212,13 @@ int wolfCLU_encrypt(int alg, char* mode, byte* pwdKey, byte* key, int size, if (hexRet != WOLFCLU_SUCCESS) { wolfCLU_LogError("failed during conversion of input," " ret = %d", hexRet); + /* wolfCLU_hexToBin() already freed and NULLed its + * own allocation, so this is really here to free + * 'output' on the way out. */ + wolfCLU_freeBins(input, output, NULL, NULL, NULL); XFCLOSE(inFile); + XFCLOSE(outFile); + wc_FreeRng(&rng); return hexRet; } }/* end hex or ascii */ @@ -211,6 +233,8 @@ int wolfCLU_encrypt(int alg, char* mode, byte* pwdKey, byte* key, int size, else { /* otherwise we got a file read error */ wolfCLU_freeBins(input, output, NULL, NULL, NULL); XFCLOSE(inFile); + XFCLOSE(outFile); + wc_FreeRng(&rng); return FREAD_ERROR; }/* End feof check */ }/* End fread check */ @@ -221,8 +245,10 @@ int wolfCLU_encrypt(int alg, char* mode, byte* pwdKey, byte* key, int size, ret = wc_CamelliaSetKey(&camellia, key, size / 8, iv); if (ret != 0) { XFCLOSE(inFile); + XFCLOSE(outFile); wolfCLU_LogError("CamelliaSetKey failed."); wolfCLU_freeBins(input, output, NULL, NULL, NULL); + wc_FreeRng(&rng); return ret; } if (XSTRNCMP(mode, "cbc", 3) == 0) { @@ -230,8 +256,10 @@ int wolfCLU_encrypt(int alg, char* mode, byte* pwdKey, byte* key, int size, } else { XFCLOSE(inFile); + XFCLOSE(outFile); wolfCLU_LogError("Incompatible mode while using Camellia."); wolfCLU_freeBins(input, output, NULL, NULL, NULL); + wc_FreeRng(&rng); return FATAL_ERROR; } } @@ -253,15 +281,7 @@ int wolfCLU_encrypt(int alg, char* mode, byte* pwdKey, byte* key, int size, WOLFCLU_LOG(WOLFCLU_L0, " ]\n"); } /* end visual confirmation */ - /* Open the outFile in append mode */ - outFile = XFOPEN(out, "ab"); - if (outFile == NULL) { - XFCLOSE(inFile); - wolfCLU_LogError("failed to open file."); - wolfCLU_freeBins(input, output, NULL, NULL, NULL); - return FWRITE_ERROR; - } - + /* write this chunk to the already-open outFile */ ret = (int)XFWRITE(output, 1, tempMax, outFile); if (ferror(outFile)) { @@ -269,6 +289,7 @@ int wolfCLU_encrypt(int alg, char* mode, byte* pwdKey, byte* key, int size, XFCLOSE(inFile); wolfCLU_LogError("failed to write to file."); wolfCLU_freeBins(input, output, NULL, NULL, NULL); + wc_FreeRng(&rng); return FWRITE_ERROR; } if (ret > MAX_LEN) { @@ -276,17 +297,27 @@ int wolfCLU_encrypt(int alg, char* mode, byte* pwdKey, byte* key, int size, XFCLOSE(inFile); wolfCLU_LogError("Wrote too much to file."); wolfCLU_freeBins(input, output, NULL, NULL, NULL); + wc_FreeRng(&rng); return FWRITE_ERROR; } - /* close the outFile */ - XFCLOSE(outFile); length -= tempMax; if (length < 0) WOLFCLU_LOG(WOLFCLU_L0, "length went past zero."); } - /* closes the opened files and frees the memory */ + /* closes the opened files and frees the memory. ret currently holds the + * byte count from the last XFWRITE() above, not a status code, so the + * close result is captured separately rather than folded into it. */ + { + int closeRet = wolfCLU_CloseOutFile(outFile, out); + if (closeRet != WOLFCLU_SUCCESS) { + ret = closeRet; + } + else { + ret = WOLFCLU_SUCCESS; + } + } XFCLOSE(inFile); XMEMSET(key, 0, size); XMEMSET(iv, 0 , block); @@ -297,6 +328,6 @@ int wolfCLU_encrypt(int alg, char* mode, byte* pwdKey, byte* key, int size, (void)mode; (void)alg; - return WOLFCLU_SUCCESS; + return (ret == WOLFCLU_SUCCESS) ? WOLFCLU_SUCCESS : WOLFCLU_FATAL_ERROR; } #endif diff --git a/src/crypto/clu_evp_crypto.c b/src/crypto/clu_evp_crypto.c index 4b7c3443..56fa3159 100644 --- a/src/crypto/clu_evp_crypto.c +++ b/src/crypto/clu_evp_crypto.c @@ -77,6 +77,11 @@ int wolfCLU_evp_crypto(const WOLFSSL_EVP_CIPHER* cphr, char* mode, byte* pwdKey, return BAD_FUNC_ARG; } + /* Opening the output truncates it, destroying the input mid-read. */ + if (wolfCLU_RejectSamePath(fileIn, fileOut) != WOLFCLU_SUCCESS) { + return WOLFCLU_FATAL_ERROR; + } + /* Start up the random number generator */ if (wc_InitRng(&rng) != 0) { wolfCLU_LogError("Random Number Generator failed to start."); @@ -236,7 +241,21 @@ int wolfCLU_evp_crypto(const WOLFSSL_EVP_CIPHER* cphr, char* mode, byte* pwdKey, /* open the outFile in write mode */ if (ret == WOLFCLU_SUCCESS) { if (fileOut != NULL) { - out = wolfSSL_BIO_new_file(fileOut, "wb"); + /* Guard against identical in/out paths before truncating output. */ + XFILE inBioFile = NULL; + FILE* outFile; + + if (fileIn != NULL && in != NULL) { + (void)wolfSSL_BIO_get_fp(in, &inBioFile); + } + outFile = wolfCLU_OpenOutFileDistinctFrom(fileOut, inBioFile); + if (outFile != NULL) { + out = wolfSSL_BIO_new_fp(outFile, BIO_CLOSE); + if (out == NULL) { + XFCLOSE(outFile); + wolfCLU_LogError("unable to open output file %s", fileOut); + } + } } else { /* write to stdout if no file provided */ @@ -246,10 +265,7 @@ int wolfCLU_evp_crypto(const WOLFSSL_EVP_CIPHER* cphr, char* mode, byte* pwdKey, } } if (out == NULL) { - if (fileOut != NULL) { - wolfCLU_LogError("unable to open output file %s", fileOut); - } - else { + if (fileOut == NULL) { wolfCLU_LogError("unable to open stdout for output"); } ret = WOLFCLU_FATAL_ERROR; @@ -390,7 +406,13 @@ int wolfCLU_evp_crypto(const WOLFSSL_EVP_CIPHER* cphr, char* mode, byte* pwdKey, } if (ret == WOLFCLU_SUCCESS) { - wolfSSL_BIO_write(out, output, outputSz); + /* Checked like the in-loop write above: this is the padded final + * block, so losing it silently truncates the output. */ + if (wolfSSL_BIO_write(out, output, outputSz) < 0) { + wolfCLU_LogError("Failed to write output file %s", + fileOut != NULL ? fileOut : "stdout"); + ret = WOLFCLU_FATAL_ERROR; + } } /* write out stored up output in base64 encrypt case */ @@ -411,12 +433,34 @@ int wolfCLU_evp_crypto(const WOLFSSL_EVP_CIPHER* cphr, char* mode, byte* pwdKey, ret = WOLFCLU_FATAL_ERROR; } else { - wolfSSL_BIO_write(base64Bio, mem->data, (int)mem->length); + /* The write can fail outright (ENOSPC/EIO) for a large + * payload, before anything is left buffered for the flush + * below to catch, so its result has to be checked here. */ + if (wolfSSL_BIO_write(base64Bio, mem->data, + (int)mem->length) != (int)mem->length) { + wolfCLU_LogError("Failed to write output file %s", + fileOut != NULL ? fileOut : "stdout"); + ret = WOLFCLU_FATAL_ERROR; + } wolfSSL_BIO_free(base64Bio); } } } + /* Flush output stream to catch write errors before closing. */ + { + WOLFSSL_BIO* fileBio = (enc && isBase64) ? tmp : out; + + if (fileBio != NULL && + wolfSSL_BIO_flush(fileBio) != WOLFSSL_SUCCESS) { + wolfCLU_LogError("Failed to write output file %s", + fileOut != NULL ? fileOut : "stdout"); + if (ret == WOLFCLU_SUCCESS) { + ret = WOLFCLU_FATAL_ERROR; + } + } + } + /* closes the opened files and frees the memory */ wolfSSL_BIO_free(out); wolfSSL_BIO_free(in); diff --git a/src/dh/clu_dh.c b/src/dh/clu_dh.c index 373ccb1d..1b5995c5 100644 --- a/src/dh/clu_dh.c +++ b/src/dh/clu_dh.c @@ -36,57 +36,6 @@ static const byte keyDhOid[] = {42, 134, 72, 134, 247, 13, 1, 3, 1}; -static word32 BytePrecisionCopy(word32 value) -{ - word32 i; - for (i = (word32)sizeof(value) - 1; i; --i) - if (value >> ((i - 1) * WOLFSSL_BIT_SIZE)) - break; - - return i; -} - -static word32 SetLengthCopy(word32 length, byte* output) -{ - /* Start encoding at start of buffer. */ - word32 i = 0; - - if (length < ASN_LONG_LENGTH) { - /* Only one byte needed to encode. */ - if (output) { - /* Write out length value. */ - output[i] = (byte)length; - } - /* Skip over length. */ - i++; - } - else { - /* Calculate the number of bytes required to encode value. */ - byte j = (byte)BytePrecisionCopy(length); - - if (output) { - /* Encode count byte. */ - output[i] = j | ASN_LONG_LENGTH; - } - /* Skip over count byte. */ - i++; - - /* Encode value as a big-endian byte array. */ - for (; j > 0; --j) { - if (output) { - /* Encode next most-significant byte. */ - output[i] = (byte)(length >> ((j - 1) * WOLFSSL_BIT_SIZE)); - } - /* Skip over byte. */ - i++; - } - } - - /* Return number of bytes in encoded length. */ - return i; -} - - static int SetMyVersionCopy(word32 version, byte* output, int header) { int i = 0; @@ -117,7 +66,7 @@ static int SetObjectIdCopy(int len, byte* output) /* Skip tag. */ idx += ASN_TAG_SZ; /* Encode length - passing NULL for output will not encode. */ - idx += SetLengthCopy(len, output ? output + idx : NULL); + idx += wolfCLU_DerSetLength(len, output ? output + idx : NULL); /* Return index after header. */ return idx; @@ -130,7 +79,8 @@ static word32 SetSequenceCopy(word32 len, byte* output) output[0] = ASN_SEQUENCE | ASN_CONSTRUCTED; } - return SetLengthCopy(len, output ? output + ASN_TAG_SZ : NULL) + ASN_TAG_SZ; + return wolfCLU_DerSetLength(len, (output != NULL) ? output + ASN_TAG_SZ : + NULL) + ASN_TAG_SZ; } @@ -140,7 +90,8 @@ static word32 SetOctetStringCopy(word32 len, byte* output) output[0] = ASN_OCTET_STRING; } - return SetLengthCopy(len, output ? output + ASN_TAG_SZ : NULL) + ASN_TAG_SZ; + return wolfCLU_DerSetLength(len, (output != NULL) ? output + ASN_TAG_SZ : + NULL) + ASN_TAG_SZ; } @@ -161,7 +112,7 @@ static int SetASNIntCopy(int len, byte firstByte, byte* output) len++; } /* Encode length - passing NULL for output will not encode. */ - idx += SetLengthCopy(len, output ? output + idx : NULL); + idx += wolfCLU_DerSetLength(len, output ? output + idx : NULL); /* Put out pre-pended 0 as well. */ if (firstByte & 0x80) { if (output) { @@ -265,7 +216,8 @@ int wc_DhPrivKeyToDer(DhKey* key, byte* prv, word32 prvSz, byte* output, /* determine size */ /* octect string: priv */ privSz = SetASNIntMPCopy(&mpPriv, -1, NULL); - idx = 1 + SetLengthCopy(privSz, NULL) + privSz; /* +1 for ASN_OCTET_STRING */ + /* +1 for ASN_OCTET_STRING */ + idx = 1 + wolfCLU_DerSetLength(privSz, NULL) + privSz; keySz = idx; /* DH Parameters sequence with P and G */ @@ -384,6 +336,7 @@ int wolfCLU_DhParamSetup(int argc, char** argv) byte noOut = 0; byte rngInited = 0; byte dhInited = 0; + byte haveIn = 0; WOLFSSL_BIO *bioIn = NULL; WOLFSSL_BIO *bioOut = NULL; @@ -446,7 +399,9 @@ int wolfCLU_DhParamSetup(int argc, char** argv) * option found in the arguments passed in */ if (ret == WOLFCLU_SUCCESS) { - int i = 2; // start at 2 because wolfssl & dhparam will be in first and second + /* start at 2 because wolfssl & dhparam will be in the first and + * second positions */ + int i = 2; int found = 0; while (i + 1 <= argc && !found) { /* confirm arg is a non '-' option that does not correspond @@ -481,6 +436,7 @@ int wolfCLU_DhParamSetup(int argc, char** argv) } /* read in parameters */ + haveIn = (bioIn != NULL); if (ret == WOLFCLU_SUCCESS && bioIn != NULL) { DerBuffer* pDer = NULL; byte* in = NULL; @@ -488,7 +444,12 @@ int wolfCLU_DhParamSetup(int argc, char** argv) word32 idx = 0; inSz = wolfSSL_BIO_get_len(bioIn); - if (inSz > 0) { + if (inSz <= 0) { + wolfCLU_LogError("Failed to get length of input DH params or " + "empty file"); + ret = WOLFCLU_FATAL_ERROR; + } + else { in = (byte*)XMALLOC(inSz, NULL, DYNAMIC_TYPE_TMP_BUFFER); if (in == NULL) { ret = WOLFCLU_FATAL_ERROR; @@ -526,6 +487,18 @@ int wolfCLU_DhParamSetup(int argc, char** argv) if (pDer != NULL) wc_FreeDer(&pDer); } + + /* -in is fully consumed above, so close it here rather than at + * function exit: -in and -out may name the same path (e.g. + * "dhparam -genkey -in params.pem -out params.pem" is only safe + * because -in has already been read and closed before -out + * truncates it), and freeing bioIn now turns any future change + * that tries to read -in lazily, after this point, into an + * immediate use-after-free instead of a silent truncate-before- + * read data loss. haveIn (captured above) keeps the "-in given" + * check below working with bioIn gone. */ + wolfSSL_BIO_free(bioIn); + bioIn = NULL; } if (ret == WOLFCLU_SUCCESS) { @@ -534,10 +507,11 @@ int wolfCLU_DhParamSetup(int argc, char** argv) WOLFCLU_LOG(WOLFCLU_E0, "No filesystem support. Unable to open output file"); ret = WOLFCLU_FATAL_ERROR; #else - bioOut = wolfSSL_BIO_new_file(out, "wb"); + /* lock down perms only when -genkey also writes a private + * key here */ + bioOut = wolfCLU_OpenOutOrKeyFileBio(out, + genKey ? WOLFCLU_OUT_SECRET : WOLFCLU_OUT_PUBLIC); if (bioOut == NULL) { - wolfCLU_LogError("Unable to open output file %s", - optarg); ret = WOLFCLU_FATAL_ERROR; } #endif @@ -558,7 +532,7 @@ int wolfCLU_DhParamSetup(int argc, char** argv) } /* generate the dh parameters */ - if (ret == WOLFCLU_SUCCESS && bioIn == NULL) { + if (ret == WOLFCLU_SUCCESS && !haveIn) { #if defined(HAVE_FFDHE_4096) #if defined(HAVE_FIPS) && FIPS_VERSION_LE(2,0) if (modSz == 4096) { @@ -728,7 +702,8 @@ int wolfCLU_DhParamSetup(int argc, char** argv) byte* outBuf = NULL; byte* pem = NULL; word32 outBufSz = 0; - word32 pemSz = 0; + word32 pemSz = 0; /* size of the pem allocation */ + int pemRet = 0; /* signed wc_DerToPem return */ if (wc_DhGenerateKeyPair(&dh, &rng, priv, &privSz, pub, &pubSz) != 0) { wolfCLU_LogError("Error making DH key"); @@ -772,8 +747,10 @@ int wolfCLU_DhParamSetup(int argc, char** argv) } if (ret == WOLFCLU_SUCCESS) { - pemSz = wc_DerToPem(outBuf, outBufSz, NULL, 0, DH_PRIVATEKEY_TYPE); - if (pemSz > 0) { + pemRet = wc_DerToPem(outBuf, outBufSz, NULL, 0, + DH_PRIVATEKEY_TYPE); + if (pemRet > 0) { + pemSz = (word32)pemRet; pem = (byte*)XMALLOC(pemSz, NULL, DYNAMIC_TYPE_TMP_BUFFER); if (pem == NULL) { ret = WOLFCLU_FATAL_ERROR; @@ -785,22 +762,29 @@ int wolfCLU_DhParamSetup(int argc, char** argv) } if (ret == WOLFCLU_SUCCESS) { - pemSz = wc_DerToPem(outBuf, outBufSz, pem, pemSz, + pemRet = wc_DerToPem(outBuf, outBufSz, pem, pemSz, DH_PRIVATEKEY_TYPE); - if (pemSz <= 0) { + if (pemRet <= 0) { ret = WOLFCLU_FATAL_ERROR; } } if (ret == WOLFCLU_SUCCESS && - wolfSSL_BIO_write(bioOut, pem, pemSz) <= 0) { + wolfSSL_BIO_write(bioOut, pem, pemRet) <= 0) { ret = WOLFCLU_FATAL_ERROR; } - if (pem != NULL) + /* priv, and the DER/PEM encodings built from it, all hold the DH + * private key. */ + wolfCLU_ForceZero(priv, sizeof(priv)); + if (pem != NULL) { + wolfCLU_ForceZero(pem, pemSz); XFREE(pem, NULL, DYNAMIC_TYPE_TMP_BUFFER); - if (outBuf != NULL) + } + if (outBuf != NULL) { + wolfCLU_ForceZero(outBuf, outBufSz); XFREE(outBuf, NULL, DYNAMIC_TYPE_TMP_BUFFER); + } } wolfSSL_BIO_free(bioIn); diff --git a/src/dsa/clu_dsa.c b/src/dsa/clu_dsa.c index be6d9eb2..0a457e72 100644 --- a/src/dsa/clu_dsa.c +++ b/src/dsa/clu_dsa.c @@ -147,11 +147,16 @@ int wolfCLU_DsaParamSetup(int argc, char** argv) if (ret == WOLFCLU_SUCCESS && bioIn != NULL) { DerBuffer* pDer = NULL; byte* in = NULL; - word32 inSz = 0; + int inSz = 0; word32 idx = 0; inSz = wolfSSL_BIO_get_len(bioIn); - if (inSz > 0) { + if (inSz <= 0) { + wolfCLU_LogError("Failed to get length of input DSA params or " + "empty file"); + ret = WOLFCLU_FATAL_ERROR; + } + else { in = (byte*)XMALLOC(inSz, NULL, DYNAMIC_TYPE_TMP_BUFFER); if (in == NULL) { ret = WOLFCLU_FATAL_ERROR; @@ -169,7 +174,7 @@ int wolfCLU_DsaParamSetup(int argc, char** argv) } /* der should always be smaller then pem but check just in case */ - if (ret == WOLFCLU_SUCCESS && inSz < pDer->length) { + if (ret == WOLFCLU_SUCCESS && (word32)inSz < pDer->length) { ret = WOLFCLU_FATAL_ERROR; } @@ -179,7 +184,7 @@ int wolfCLU_DsaParamSetup(int argc, char** argv) } if (ret == WOLFCLU_SUCCESS && - wc_DsaParamsDecode(in, &idx, &dsa, inSz) != 0) { + wc_DsaParamsDecode(in, &idx, &dsa, (word32)inSz) != 0) { wolfCLU_LogError("Unable to decode input params"); ret = WOLFCLU_FATAL_ERROR; } @@ -197,10 +202,11 @@ int wolfCLU_DsaParamSetup(int argc, char** argv) WOLFCLU_LOG(WOLFCLU_E0, "No filesystem support. Unable to open input file"); ret = WOLFCLU_FATAL_ERROR; #else - bioOut = wolfSSL_BIO_new_file(out, "wb"); + /* lock down perms only when -genkey also writes a private + * key here */ + bioOut = wolfCLU_OpenOutOrKeyFileBio(out, + genKey ? WOLFCLU_OUT_SECRET : WOLFCLU_OUT_PUBLIC); if (bioOut == NULL) { - wolfCLU_LogError("Unable to open output file %s", - optarg); ret = WOLFCLU_FATAL_ERROR; } #endif @@ -288,7 +294,8 @@ int wolfCLU_DsaParamSetup(int argc, char** argv) byte* outBuf = NULL; byte* pem = NULL; word32 outBufSz = 0; - word32 pemSz = 0; + word32 pemSz = 0; /* size of the pem allocation */ + int pemRet = 0; /* signed wc_DerToPem return */ if (wc_MakeDsaKey(&rng, &dsa) != 0) { wolfCLU_LogError("Error making DSA key"); @@ -325,8 +332,10 @@ int wolfCLU_DsaParamSetup(int argc, char** argv) } if (ret == WOLFCLU_SUCCESS) { - pemSz = wc_DerToPem(outBuf, outBufSz, NULL, 0, DSA_PRIVATEKEY_TYPE); - if (pemSz > 0) { + pemRet = wc_DerToPem(outBuf, outBufSz, NULL, 0, + DSA_PRIVATEKEY_TYPE); + if (pemRet > 0) { + pemSz = (word32)pemRet; pem = (byte*)XMALLOC(pemSz, NULL, DYNAMIC_TYPE_TMP_BUFFER); if (pem == NULL) { ret = WOLFCLU_FATAL_ERROR; @@ -338,22 +347,27 @@ int wolfCLU_DsaParamSetup(int argc, char** argv) } if (ret == WOLFCLU_SUCCESS) { - pemSz = wc_DerToPem(outBuf, outBufSz, pem, pemSz, + pemRet = wc_DerToPem(outBuf, outBufSz, pem, pemSz, DSA_PRIVATEKEY_TYPE); - if (pemSz <= 0) { + if (pemRet <= 0) { ret = WOLFCLU_FATAL_ERROR; } } if (ret == WOLFCLU_SUCCESS && - wolfSSL_BIO_write(bioOut, pem, pemSz) <= 0) { + wolfSSL_BIO_write(bioOut, pem, pemRet) <= 0) { ret = WOLFCLU_FATAL_ERROR; } - if (pem != NULL) + /* Both encodings hold the DSA private key. */ + if (pem != NULL) { + wolfCLU_ForceZero(pem, pemSz); XFREE(pem, NULL, DYNAMIC_TYPE_TMP_BUFFER); - if (outBuf != NULL) + } + if (outBuf != NULL) { + wolfCLU_ForceZero(outBuf, outBufSz); XFREE(outBuf, NULL, DYNAMIC_TYPE_TMP_BUFFER); + } } wolfSSL_BIO_free(bioIn); diff --git a/src/ecparam/clu_ecparam.c b/src/ecparam/clu_ecparam.c index 387210a6..2bcef3f2 100644 --- a/src/ecparam/clu_ecparam.c +++ b/src/ecparam/clu_ecparam.c @@ -201,6 +201,16 @@ int wolfCLU_ecparam(int argc, char** argv) key = wolfSSL_EVP_PKEY_get1_EC_KEY(pkey); } wolfSSL_EVP_PKEY_free(pkey); + + /* -in is fully consumed above, so close it here rather than at + * function exit: -in and -out may name the same path (e.g. + * "ecparam -genkey -in key.pem -out key.pem" is only safe because + * -in has already been read and closed before -out truncates it), + * and freeing it now turns any future change that tries to read + * -in lazily, after this point, into an immediate use-after-free + * instead of a silent truncate-before-read data loss. */ + wolfSSL_BIO_free(in); + in = NULL; } if (ret == WOLFCLU_SUCCESS && genKey) { @@ -221,7 +231,10 @@ int wolfCLU_ecparam(int argc, char** argv) WOLFCLU_LOG(WOLFCLU_E0, "No filesystem support. Unable to open input file"); ret = WOLFCLU_FATAL_ERROR; #else - bioOut = wolfSSL_BIO_new_file(out, "wb"); + /* -genkey sends an EC private key here, so it gets the same + * owner-only treatment as dhparam/dsaparam -genkey. */ + bioOut = wolfCLU_OpenOutOrKeyFileBio(out, + genKey ? WOLFCLU_OUT_SECRET : WOLFCLU_OUT_PUBLIC); if (bioOut == NULL) { ret = WOLFCLU_FATAL_ERROR; } @@ -282,6 +295,11 @@ int wolfCLU_ecparam(int argc, char** argv) } if (der != NULL) { + /* der holds the EC private key, so scrub it before + * releasing it back to the allocator. */ + if (derSz > 0) { + wolfCLU_ForceZero(der, (unsigned int)derSz); + } /* der was created by wolfSSL library so we assume * that XMALLOC was used and call XFREE here */ XFREE(der, NULL, DYNAMIC_TYPE_TMP_BUFFER); diff --git a/src/genkey/clu_genkey.c b/src/genkey/clu_genkey.c index b9c512e8..d9c04a9c 100644 --- a/src/genkey/clu_genkey.c +++ b/src/genkey/clu_genkey.c @@ -26,13 +26,26 @@ #if defined(WOLFSSL_KEY_GEN) && !defined(NO_ASN) +/* Each key-generation routine below writes its result out through the wolfCLU + * secure file helpers, which are only declared and compiled when a stdio + * filesystem is available, so each is additionally conditioned on + * !WOLFCLU_NO_FILESYSTEM and falls back to its NOT_COMPILED_IN branch -- + * except wolfCLU_genKey_ED25519() and the XMSS read/write callbacks, whose + * entire definitions (including the NOT_COMPILED_IN stub) are already + * conditioned on their key type being enabled, so they gain no separate + * !WOLFCLU_NO_FILESYSTEM stub of their own; their only callers are already + * inside a !WOLFCLU_NO_FILESYSTEM guard. + * The BIO-based helpers (wolfCLU_GenKeyECC, wolfCLU_EcparamPrintOID, + * wolfCLU_KeyDerToPem) open no files and stay available: ecparam still + * generates keys to stdout without a filesystem. */ #include #include #include #include #include /* PER_FORM/DER_FORM */ -#ifdef HAVE_ED25519 +/* Writes the key to a file, so it needs the secure file helpers. */ +#if defined(HAVE_ED25519) && !defined(WOLFCLU_NO_FILESYSTEM) /* return WOLFCLU_SUCCESS on success */ int wolfCLU_genKey_ED25519(WC_RNG* rng, char* fOutNm, int directive, int format) { @@ -51,8 +64,8 @@ int wolfCLU_genKey_ED25519(WC_RNG* rng, char* fOutNm, int directive, int format) XFILE file = NULL; /* file stream */ byte* derBuf = NULL; /* buffer for DER format */ byte* pemBuf = NULL; /* buffer for PEM format */ - int derSz; /* size of DER buffer */ - int pemSz; /* size of PEM buffer */ + int derSz = 0; /* size of DER buffer */ + int pemSz = 0; /* size of PEM buffer */ /* initialize ed25519 key */ ret = wc_ed25519_init(&edKeyOut); @@ -121,7 +134,7 @@ int wolfCLU_genKey_ED25519(WC_RNG* rng, char* fOutNm, int directive, int format) /* open the file for writing the private key */ if (ret == 0) { - file = XFOPEN(finalOutFNm, "wb"); + file = wolfCLU_OpenKeyFile(finalOutFNm); if (!file) { ret = OUTPUT_FILE_ERROR; } @@ -201,12 +214,23 @@ int wolfCLU_genKey_ED25519(WC_RNG* rng, char* fOutNm, int directive, int format) break; } /* else fall through to PUB_ONLY_FILE if flagOutputPub == 1 */ - XFCLOSE(file); + if (wolfCLU_CloseOutFile(file, finalOutFNm) != WOLFCLU_SUCCESS) { + ret = OUTPUT_FILE_ERROR; + } file = NULL; + if (derBuf != NULL) { + wolfCLU_ForceZero(derBuf, derSz); + } XFREE(derBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); derBuf = NULL; + if (pemBuf != NULL) { + wolfCLU_ForceZero(pemBuf, pemSz); + } XFREE(pemBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); pemBuf = NULL; + if (ret != 0) { + break; + } FALL_THROUGH; case PUB_ONLY_FILE: @@ -216,7 +240,7 @@ int wolfCLU_genKey_ED25519(WC_RNG* rng, char* fOutNm, int directive, int format) /* open the file for writing the public key */ if (ret == 0) { - file = XFOPEN(finalOutFNm, "wb"); + file = wolfCLU_OpenOutFile(finalOutFNm); if (!file) { ret = OUTPUT_FILE_ERROR; } @@ -301,27 +325,40 @@ int wolfCLU_genKey_ED25519(WC_RNG* rng, char* fOutNm, int directive, int format) } /* cleanup allocated resources */ + if (file != NULL) { + if (wolfCLU_CloseOutFile(file, finalOutFNm) != WOLFCLU_SUCCESS && + ret == 0) { + ret = OUTPUT_FILE_ERROR; + } + file = NULL; + } if (finalOutFNm != NULL) { XFREE(finalOutFNm, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); finalOutFNm = NULL; } if (derBuf != NULL) { + wolfCLU_ForceZero(derBuf, derSz); XFREE(derBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); } if (pemBuf != NULL) { + wolfCLU_ForceZero(pemBuf, pemSz); XFREE(pemBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); } - if (file != NULL) { - XFCLOSE(file); - file = NULL; - } + /* privKeyBuf holds the raw private key when format == RAW_FORM; wipe it + * here regardless of format so a caller-supplied but unused buffer is + * never left holding key material past this call. */ + wolfCLU_ForceZero(privKeyBuf, sizeof(privKeyBuf)); /* expected ret == WOLFCLU_SUCCESS */ return (ret >= 0) ? WOLFCLU_SUCCESS : ret; } -#endif /* HAVE_ED25519 */ +#endif /* HAVE_ED25519 && !WOLFCLU_NO_FILESYSTEM */ #ifdef HAVE_ECC + +/* Only wolfCLU_GenAndOutput_ECC() uses these two, so they follow it in being + * compiled out without a filesystem. */ +#ifndef WOLFCLU_NO_FILESYSTEM /* returns WOLFCLU_SUCCESS on successfully writing out public DER key */ static int wolfCLU_ECC_write_pub_der(WOLFSSL_BIO* out, WOLFSSL_EC_KEY* key) { @@ -403,6 +440,7 @@ static int wolfCLU_ECC_write_priv_der(WOLFSSL_BIO* out, WOLFSSL_EC_KEY* key) return ret; } +#endif /* !WOLFCLU_NO_FILESYSTEM */ void wolfCLU_EcparamPrintOID(WOLFSSL_BIO* out, WOLFSSL_EC_KEY* key, int fmt) @@ -613,7 +651,7 @@ WOLFSSL_EC_KEY* wolfCLU_GenKeyECC(char* name) int wolfCLU_GenAndOutput_ECC(WC_RNG* rng, char* fName, int directive, int fmt, char* name) { -#ifdef HAVE_ECC +#if defined(HAVE_ECC) && !defined(WOLFCLU_NO_FILESYSTEM) int fNameSz; int fExtSz = 6; char fExtPriv[6] = ".priv"; @@ -663,11 +701,9 @@ int wolfCLU_GenAndOutput_ECC(WC_RNG* rng, char* fName, int directive, fOutNameBuf[fNameSz + fExtSz] = '\0'; WOLFCLU_LOG(WOLFCLU_L0, "Private key file = %s", fOutNameBuf); - bioPri = wolfSSL_BIO_new_file(fOutNameBuf, "wb"); + bioPri = wolfCLU_OpenKeyFileBio(fOutNameBuf); if (bioPri == NULL) { - wolfCLU_LogError("unable to read outfile %s", - fOutNameBuf); - ret = MEMORY_E; + ret = OUTPUT_FILE_ERROR; } } @@ -697,11 +733,9 @@ int wolfCLU_GenAndOutput_ECC(WC_RNG* rng, char* fName, int directive, fOutNameBuf[fNameSz + fExtSz] = '\0'; WOLFCLU_LOG(WOLFCLU_L0, "Public key file = %s", fOutNameBuf); - bioPub = wolfSSL_BIO_new_file(fOutNameBuf, "wb"); + bioPub = wolfCLU_OpenOutFileBio(fOutNameBuf); if (bioPub == NULL) { - wolfCLU_LogError("unable to read outfile %s", - fOutNameBuf); - ret = MEMORY_E; + ret = OUTPUT_FILE_ERROR; } } @@ -743,8 +777,10 @@ int wolfCLU_GenAndOutput_ECC(WC_RNG* rng, char* fName, int directive, (void)directive; (void)fmt; + (void)name; + return NOT_COMPILED_IN; -#endif /* HAVE_ECC */ +#endif /* HAVE_ECC && !WOLFCLU_NO_FILESYSTEM */ } @@ -789,7 +825,7 @@ int wolfCLU_KeyDerToPem(const byte* der, int derSz, byte** out, int pemType, int wolfCLU_genKey_RSA(WC_RNG* rng, char* fName, int directive, int fmt, int keySz, long exp) { -#ifndef NO_RSA +#if !defined(NO_RSA) && !defined(WOLFCLU_NO_FILESYSTEM) RsaKey key; /* the RSA key structure */ XFILE file = NULL; /* file stream */ int ret = WOLFCLU_SUCCESS; /* return value */ @@ -849,7 +885,7 @@ int wolfCLU_genKey_RSA(WC_RNG* rng, char* fName, int directive, int fmt, int /* open the file for writing the private key */ if (ret == WOLFCLU_SUCCESS) { - file = XFOPEN(fOutNameBuf, "wb"); + file = wolfCLU_OpenKeyFile(fOutNameBuf); if (!file) { ret = OUTPUT_FILE_ERROR; } @@ -919,7 +955,9 @@ int wolfCLU_genKey_RSA(WC_RNG* rng, char* fName, int directive, int fmt, int break; } /* else fall through to PUB_ONLY_FILE if flagOutputPub == 1 */ - XFCLOSE(file); + if (wolfCLU_CloseOutFile(file, fOutNameBuf) != WOLFCLU_SUCCESS) { + ret = OUTPUT_FILE_ERROR; + } file = NULL; if (derBuf != NULL) { if (derBufSz > 0) { @@ -935,6 +973,9 @@ int wolfCLU_genKey_RSA(WC_RNG* rng, char* fName, int directive, int fmt, int XFREE(pemBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); pemBuf = NULL; } + if (ret != WOLFCLU_SUCCESS) { + break; + } FALL_THROUGH; case PUB_ONLY_FILE: @@ -944,7 +985,7 @@ int wolfCLU_genKey_RSA(WC_RNG* rng, char* fName, int directive, int fmt, int /* open the file for writing the public key */ if (ret == WOLFCLU_SUCCESS) { - file = XFOPEN(fOutNameBuf, "wb"); + file = wolfCLU_OpenOutFile(fOutNameBuf); if (!file) { ret = OUTPUT_FILE_ERROR; } @@ -1019,6 +1060,13 @@ int wolfCLU_genKey_RSA(WC_RNG* rng, char* fName, int directive, int fmt, int } /* cleanup allocated resources */ + if (file != NULL) { + if (wolfCLU_CloseOutFile(file, fOutNameBuf) != WOLFCLU_SUCCESS && + ret == WOLFCLU_SUCCESS) { + ret = OUTPUT_FILE_ERROR; + } + file = NULL; + } if (fOutNameBuf != NULL) { XFREE(fOutNameBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); fOutNameBuf = NULL; @@ -1037,10 +1085,6 @@ int wolfCLU_genKey_RSA(WC_RNG* rng, char* fName, int directive, int fmt, int XFREE(pemBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); pemBuf = NULL; } - if (file != NULL) { - XFCLOSE(file); - file = NULL; - } wc_FreeRsaKey(&key); @@ -1061,7 +1105,7 @@ int wolfCLU_genKey_RSA(WC_RNG* rng, char* fName, int directive, int fmt, int int wolfCLU_genKey_Dilithium(WC_RNG* rng, char* fName, int directive, int fmt, int keySz, int level, int withAlg) { -#ifdef HAVE_DILITHIUM +#if defined(HAVE_DILITHIUM) && !defined(WOLFCLU_NO_FILESYSTEM) int ret = WOLFCLU_SUCCESS; XFILE file = NULL; @@ -1180,10 +1224,8 @@ int wolfCLU_genKey_Dilithium(WC_RNG* rng, char* fName, int directive, int fmt, /* open file and write Private key */ if (ret == WOLFCLU_SUCCESS) { - file = XFOPEN(fOutNameBuf, "wb"); - if (file == XBADFILE) { - wolfCLU_LogError("unable to open file %s", - fOutNameBuf); + file = wolfCLU_OpenKeyFile(fOutNameBuf); + if (file == NULL) { ret = OUTPUT_FILE_ERROR; } } @@ -1198,7 +1240,9 @@ int wolfCLU_genKey_Dilithium(WC_RNG* rng, char* fName, int directive, int fmt, break; } - XFCLOSE(file); + if (wolfCLU_CloseOutFile(file, fOutNameBuf) != WOLFCLU_SUCCESS) { + ret = OUTPUT_FILE_ERROR; + } file = NULL; wolfCLU_ForceZero(derBuf, keySz); XFREE(derBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); @@ -1208,6 +1252,9 @@ int wolfCLU_genKey_Dilithium(WC_RNG* rng, char* fName, int directive, int fmt, XFREE(pemBuf, HEAP_HINT, DYNAMIC_TYPE_PRIVATE_KEY); pemBuf = NULL; } + if (ret != WOLFCLU_SUCCESS) { + break; + } FALL_THROUGH; case PUB_ONLY_FILE: @@ -1247,10 +1294,8 @@ int wolfCLU_genKey_Dilithium(WC_RNG* rng, char* fName, int directive, int fmt, /* open file and write Public key */ if (ret == WOLFCLU_SUCCESS) { - file = XFOPEN(fOutNameBuf, "wb"); - if (file == XBADFILE) { - wolfCLU_LogError("unable to open file %s", - fOutNameBuf); + file = wolfCLU_OpenOutFile(fOutNameBuf); + if (file == NULL) { ret = OUTPUT_FILE_ERROR; } } @@ -1268,8 +1313,13 @@ int wolfCLU_genKey_Dilithium(WC_RNG* rng, char* fName, int directive, int fmt, } } - if (file != NULL) - XFCLOSE(file); + if (file != NULL) { + if (wolfCLU_CloseOutFile(file, fOutNameBuf) != WOLFCLU_SUCCESS && + ret == WOLFCLU_SUCCESS) { + ret = OUTPUT_FILE_ERROR; + } + file = NULL; + } if (derBuf != NULL) { wolfCLU_ForceZero(derBuf, keySz); @@ -1301,13 +1351,13 @@ int wolfCLU_genKey_Dilithium(WC_RNG* rng, char* fName, int directive, int fmt, (void)withAlg; return NOT_COMPILED_IN; -#endif /* HAVE_DILITHIUM */ +#endif /* HAVE_DILITHIUM && !WOLFCLU_NO_FILESYSTEM */ } int wolfCLU_genKey_ML_DSA(WC_RNG* rng, char* fName, int directive, int fmt, int keySz, int level, int withAlg) { -#ifdef HAVE_DILITHIUM +#if defined(HAVE_DILITHIUM) && !defined(WOLFCLU_NO_FILESYSTEM) int ret = WOLFCLU_SUCCESS; XFILE file = NULL; @@ -1428,10 +1478,8 @@ int wolfCLU_genKey_ML_DSA(WC_RNG* rng, char* fName, int directive, int fmt, /* open file and write Private key */ if (ret == WOLFCLU_SUCCESS) { - file = XFOPEN(fOutNameBuf, "wb"); - if (file == XBADFILE) { - wolfCLU_LogError("unable to open file %s", - fOutNameBuf); + file = wolfCLU_OpenKeyFile(fOutNameBuf); + if (file == NULL) { ret = OUTPUT_FILE_ERROR; } } @@ -1446,7 +1494,9 @@ int wolfCLU_genKey_ML_DSA(WC_RNG* rng, char* fName, int directive, int fmt, break; } - XFCLOSE(file); + if (wolfCLU_CloseOutFile(file, fOutNameBuf) != WOLFCLU_SUCCESS) { + ret = OUTPUT_FILE_ERROR; + } file = NULL; wolfCLU_ForceZero(derBuf, keySz); XFREE(derBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); @@ -1456,6 +1506,9 @@ int wolfCLU_genKey_ML_DSA(WC_RNG* rng, char* fName, int directive, int fmt, XFREE(pemBuf, HEAP_HINT, DYNAMIC_TYPE_PRIVATE_KEY); pemBuf = NULL; } + if (ret != WOLFCLU_SUCCESS) { + break; + } FALL_THROUGH; case PUB_ONLY_FILE: @@ -1500,10 +1553,8 @@ int wolfCLU_genKey_ML_DSA(WC_RNG* rng, char* fName, int directive, int fmt, /* open file and write Public key */ if (ret == WOLFCLU_SUCCESS) { - file = XFOPEN(fOutNameBuf, "wb"); - if (file == XBADFILE) { - wolfCLU_LogError("unable to open file %s", - fOutNameBuf); + file = wolfCLU_OpenOutFile(fOutNameBuf); + if (file == NULL) { ret = OUTPUT_FILE_ERROR; } } @@ -1522,7 +1573,11 @@ int wolfCLU_genKey_ML_DSA(WC_RNG* rng, char* fName, int directive, int fmt, } if (file != NULL) { - XFCLOSE(file); + if (wolfCLU_CloseOutFile(file, fOutNameBuf) != WOLFCLU_SUCCESS && + ret == WOLFCLU_SUCCESS) { + ret = OUTPUT_FILE_ERROR; + } + file = NULL; } if (derBuf != NULL) { @@ -1555,11 +1610,13 @@ int wolfCLU_genKey_ML_DSA(WC_RNG* rng, char* fName, int directive, int fmt, (void)withAlg; return NOT_COMPILED_IN; -#endif /* HAVE_DILITHIUM */ +#endif /* HAVE_DILITHIUM && !WOLFCLU_NO_FILESYSTEM */ } /* The call back function of the writing xmss key */ -#ifdef WOLFSSL_HAVE_XMSS +/* The read/write callbacks below go through the secure file helpers; + * clu_sign.c only registers them when a filesystem is available. */ +#if defined(WOLFSSL_HAVE_XMSS) && !defined(WOLFCLU_NO_FILESYSTEM) enum wc_XmssRc wolfCLU_XmssKey_WriteCb(const byte * priv, word32 privSz, void * context) { @@ -1578,15 +1635,12 @@ enum wc_XmssRc wolfCLU_XmssKey_WriteCb(const byte * priv, filename = context; - /* Open file for read and write. */ - file = fopen(filename, "rb+"); + /* This is the XMSS private key, including the signing state that is + * rewritten after every signature, so it gets the same owner-only, + * no-symlink treatment as every other private key wolfCLU writes. */ + file = wolfCLU_OpenSecureFileForUpdate(filename); if (!file) { - /* Create the file if it didn't exist. */ - file = fopen(filename, "wb+"); - if (!file) { - fprintf(stderr, "error: fopen(%s, \"w+\") failed.\n", filename); - return WC_XMSS_RC_WRITE_FAIL; - } + return WC_XMSS_RC_WRITE_FAIL; } n_write = fwrite(priv, 1, privSz, file); @@ -1606,9 +1660,12 @@ enum wc_XmssRc wolfCLU_XmssKey_WriteCb(const byte * priv, /* Verify private key data has actually been written to persistent * storage correctly. */ - file = fopen(filename, "rb+"); + /* ownerOnly is clear: this only reads back what was just written, and + * the mode was already forced during the write above. Passing 1 here + * would make a read path fchmod the file. */ + file = wolfCLU_OpenExistingSecureFile(filename, "rb", 0); if (!file) { - fprintf(stderr, "error: fopen(%s, \"r+\") failed.\n", filename); + fprintf(stderr, "error: could not reopen %s to verify.\n", filename); return WC_XMSS_RC_WRITE_FAIL; } @@ -1626,12 +1683,15 @@ enum wc_XmssRc wolfCLU_XmssKey_WriteCb(const byte * priv, if (n_read != n_write) { fprintf(stderr, "error: read %zu, expected %zu: %d\n", n_read, n_write, ferror(file)); + wolfCLU_ForceZero(buff, (unsigned int)privSz); free(buff); fclose(file); return WC_XMSS_RC_WRITE_FAIL; } n_cmp = XMEMCMP(buff, priv, n_write); + /* buff holds a copy of the private key read back from disk. */ + wolfCLU_ForceZero(buff, (unsigned int)privSz); free(buff); buff = NULL; @@ -1664,9 +1724,12 @@ enum wc_XmssRc wolfCLU_XmssKey_ReadCb(byte * priv, filename = context; - file = fopen(filename, "rb"); + /* Read-only path, so it follows symlinks like any other read; the + * no-symlink rule is applied where this key is written, in the write + * callback above. */ + file = XFOPEN(filename, "rb"); if (!file) { - fprintf(stderr, "error: fopen(%s, \"rb\") failed\n", filename); + fprintf(stderr, "error: could not open %s for reading\n", filename); return WC_XMSS_RC_READ_FAIL; } @@ -1683,12 +1746,12 @@ enum wc_XmssRc wolfCLU_XmssKey_ReadCb(byte * priv, return WC_XMSS_RC_READ_TO_MEMORY; } -#endif /* WOLFSSL_HAVE_XMSS */ +#endif /* WOLFSSL_HAVE_XMSS && !WOLFCLU_NO_FILESYSTEM */ int wolfCLU_genKey_XMSS(WC_RNG* rng, char* fName, int directive, const char* paramStr) { -#ifdef WOLFSSL_HAVE_XMSS +#if defined(WOLFSSL_HAVE_XMSS) && !defined(WOLFCLU_NO_FILESYSTEM) int ret = 0; int fNameSz = 0; /* file name without append */ int fExtSz = 6; /* size of ".priv\0" and ".pub\0\0" */ @@ -1794,10 +1857,9 @@ int wolfCLU_genKey_XMSS(WC_RNG* rng, char* fName, WOLFCLU_LOG(WOLFCLU_L0, "Public key file = %s", fOutNameBuf); /* open the file for writing the public key */ - file = XFOPEN(fOutNameBuf, "wb"); + file = wolfCLU_OpenOutFile(fOutNameBuf); if (file == NULL) { ret = OUTPUT_FILE_ERROR; - wolfCLU_LogError("unable to open file %s\nRET: %d", fOutNameBuf, ret); } /* get the public key length */ @@ -1839,7 +1901,11 @@ int wolfCLU_genKey_XMSS(WC_RNG* rng, char* fName, } if (file != NULL) { - XFCLOSE(file); + if (wolfCLU_CloseOutFile(file, fOutNameBuf) != WOLFCLU_SUCCESS && + ret == 0) { + ret = OUTPUT_FILE_ERROR; + } + file = NULL; } } @@ -1864,7 +1930,7 @@ int wolfCLU_genKey_XMSS(WC_RNG* rng, char* fName, (void)paramStr; return NOT_COMPILED_IN; -#endif /* HAVE_XMSS */ +#endif /* WOLFSSL_HAVE_XMSS && !WOLFCLU_NO_FILESYSTEM */ } #endif /* WOLFSSL_KEY_GEN && !NO_ASN*/ diff --git a/src/hash/clu_hash_setup.c b/src/hash/clu_hash_setup.c index bc10564f..ad3b44e5 100644 --- a/src/hash/clu_hash_setup.c +++ b/src/hash/clu_hash_setup.c @@ -109,6 +109,7 @@ int wolfCLU_hashSetup(int argc, char** argv) WOLFSSL_BIO *bioOut = NULL; char* inFile = NULL; char* outFile = NULL; + int outSeen = 0; opterr = 0; /* do not display unrecognized options */ @@ -125,6 +126,7 @@ int wolfCLU_hashSetup(int argc, char** argv) break; case WOLFCLU_OUTFILE: + outSeen = 1; outFile = optarg; break; @@ -293,12 +295,44 @@ int wolfCLU_hashSetup(int argc, char** argv) } } + /* -out as the last argv token binds a NULL optarg; without this the + * digest silently goes to stdout with a success exit code. */ + if (ret == WOLFCLU_SUCCESS && outSeen && outFile == NULL) { + wolfCLU_LogError("-out requires a file name"); + ret = USER_INPUT_ERROR; + } + if (ret == WOLFCLU_SUCCESS && outFile != NULL) { - bioOut = wolfSSL_BIO_new_file(outFile, "wb"); - if (bioOut == NULL) { - wolfCLU_LogError("unable to open output file %s", outFile); + /* Opening -out truncates it, and the input is streamed afterwards, + * so naming one file for both would hash whatever survives the + * truncation and overwrite the user's data with the digest. */ + if (wolfCLU_RejectSamePath(inFile, outFile) != WOLFCLU_SUCCESS) { ret = USER_INPUT_ERROR; } + else { + /* A digest is not secret, so this keeps fopen() semantics: + * symlinks and /dev/stdout stay valid -out targets. bioIn is + * already open, so the fd-level check also closes the window + * between the comparison above and this open. */ + XFILE inBioFile = NULL; + FILE* outFp; + + if (bioIn != NULL) { + (void)wolfSSL_BIO_get_fp(bioIn, &inBioFile); + } + outFp = wolfCLU_OpenOutFileDistinctFrom(outFile, inBioFile); + if (outFp == NULL) { + ret = USER_INPUT_ERROR; + } + else { + bioOut = wolfSSL_BIO_new_fp(outFp, BIO_CLOSE); + if (bioOut == NULL) { + XFCLOSE(outFp); + wolfCLU_LogError("unable to open output file %s", outFile); + ret = USER_INPUT_ERROR; + } + } + } } if (ret == WOLFCLU_SUCCESS) { diff --git a/src/pkcs/clu_pkcs12.c b/src/pkcs/clu_pkcs12.c index 29b08219..38fef863 100644 --- a/src/pkcs/clu_pkcs12.c +++ b/src/pkcs/clu_pkcs12.c @@ -74,6 +74,9 @@ int wolfCLU_PKCS12(int argc, char** argv) WOLF_STACK_OF(WOLFSSL_X509) *extra = NULL; WOLFSSL_BIO *bioIn = NULL; WOLFSSL_BIO *bioOut = NULL; + char *inPath = NULL; + char *outPath = NULL; + int outSeen = 0; opterr = 0; /* do not display unrecognized options */ optind = 0; /* start at indent 0 */ @@ -106,6 +109,7 @@ int wolfCLU_PKCS12(int argc, char** argv) break; case WOLFCLU_INFILE: + inPath = optarg; bioIn = wolfSSL_BIO_new_file(optarg, "rb"); if (bioIn == NULL) { wolfCLU_LogError("Unable to open pkcs12 file %s", @@ -115,12 +119,11 @@ int wolfCLU_PKCS12(int argc, char** argv) break; case WOLFCLU_OUTFILE: - bioOut = wolfSSL_BIO_new_file(optarg, "wb"); - if (bioOut == NULL) { - wolfCLU_LogError("Unable to open output file %s", - optarg); - ret = WOLFCLU_FATAL_ERROR; - } + /* Deferred: -out can carry an unencrypted or DES-encrypted + * private key alongside the cert unless -nokeys is given, + * which may appear later on the command line. */ + outSeen = 1; + outPath = optarg; break; case WOLFCLU_HELP: @@ -140,6 +143,14 @@ int wolfCLU_PKCS12(int argc, char** argv) } } + /* -out as the last argv token binds a NULL optarg; without this the + * deferred open below is skipped and the output silently goes to + * stdout with a success exit code. */ + if (ret == WOLFCLU_SUCCESS && outSeen && outPath == NULL) { + wolfCLU_LogError("-out requires a file name"); + ret = USER_INPUT_ERROR; + } + /* with currently only supporting PKCS12 parsing, an input file is expected */ if (ret == WOLFCLU_SUCCESS && bioIn == NULL) { wolfCLU_LogError("No input file set"); @@ -187,6 +198,21 @@ int wolfCLU_PKCS12(int argc, char** argv) } } + if (ret == WOLFCLU_SUCCESS && outPath != NULL) { + /* Use owner-only permissions if writing a private key. */ + if (wolfCLU_RejectSamePath(inPath, outPath) != WOLFCLU_SUCCESS) { + ret = WOLFCLU_FATAL_ERROR; + } + else { + bioOut = wolfCLU_OpenOutOrKeyFileBio(outPath, + (printKeys && pkey != NULL) ? WOLFCLU_OUT_SECRET : + WOLFCLU_OUT_PUBLIC); + if (bioOut == NULL) { + ret = WOLFCLU_FATAL_ERROR; + } + } + } + /* setup output bio to stdout if not already set */ if (ret == WOLFCLU_SUCCESS && bioOut == NULL) { bioOut = wolfSSL_BIO_new(wolfSSL_BIO_s_file()); diff --git a/src/pkcs/clu_pkcs7.c b/src/pkcs/clu_pkcs7.c index 41b642a0..c60e6eb6 100644 --- a/src/pkcs/clu_pkcs7.c +++ b/src/pkcs/clu_pkcs7.c @@ -64,6 +64,9 @@ int wolfCLU_PKCS7(int argc, char** argv) PKCS7 pkcs7; WOLFSSL_BIO *bioIn = NULL; WOLFSSL_BIO *bioOut = NULL; + char *inPath = NULL; + char *outPath = NULL; + int outSeen = 0; DerBuffer* derObj = NULL; byte* buf = NULL; byte* derContent = NULL; @@ -88,6 +91,7 @@ int wolfCLU_PKCS7(int argc, char** argv) break; case WOLFCLU_INFILE: + inPath = optarg; bioIn = wolfSSL_BIO_new_file(optarg, "rb"); if (bioIn == NULL) { wolfCLU_LogError("Unable to open pkcs7 file %s", @@ -97,12 +101,11 @@ int wolfCLU_PKCS7(int argc, char** argv) break; case WOLFCLU_OUTFILE: - bioOut = wolfSSL_BIO_new_file(optarg, "wb"); - if (bioOut == NULL) { - wolfCLU_LogError("Unable to open output file %s", - optarg); - ret = WOLFCLU_FATAL_ERROR; - } + /* Opening now would truncate -in before it's read below if + * the two name the same file; deferred until -in is known + * to be a different path. */ + outSeen = 1; + outPath = optarg; break; case WOLFCLU_INFORM: @@ -130,6 +133,14 @@ int wolfCLU_PKCS7(int argc, char** argv) } } + /* -out as the last argv token binds a NULL optarg; without this the + * deferred open below is skipped and the output silently goes to + * stdout with a success exit code. */ + if (ret == WOLFCLU_SUCCESS && outSeen && outPath == NULL) { + wolfCLU_LogError("-out requires a file name"); + ret = USER_INPUT_ERROR; + } + /* currently only supporting PKCS7 parsing, input is expected */ if (ret == WOLFCLU_SUCCESS && bioIn == NULL) { bioIn = wolfSSL_BIO_new(wolfSSL_BIO_s_file()); @@ -188,6 +199,23 @@ int wolfCLU_PKCS7(int argc, char** argv) } + /* Open -out now that -in has been fully read: doing this any earlier + * risks truncating -in before it's read, if the two name the same + * file. */ + if (ret == WOLFCLU_SUCCESS && outPath != NULL) { + if (wolfCLU_RejectSamePath(inPath, outPath) != WOLFCLU_SUCCESS) { + ret = WOLFCLU_FATAL_ERROR; + } + else { + /* This tool only ever prints certificates, never key material, + * so default (non-owner-only) permissions apply. */ + bioOut = wolfCLU_OpenOutFileBio(outPath); + if (bioOut == NULL) { + ret = WOLFCLU_FATAL_ERROR; + } + } + } + /* setup output bio to stdout if not already set */ if (ret == WOLFCLU_SUCCESS && bioOut == NULL) { bioOut = wolfSSL_BIO_new(wolfSSL_BIO_s_file()); diff --git a/src/pkcs/clu_pkcs8.c b/src/pkcs/clu_pkcs8.c index 9c4c7049..31434b87 100644 --- a/src/pkcs/clu_pkcs8.c +++ b/src/pkcs/clu_pkcs8.c @@ -84,6 +84,9 @@ int wolfCLU_PKCS8(int argc, char** argv) WOLFSSL_EVP_PKEY *pkey = NULL; WOLFSSL_BIO *bioIn = NULL; WOLFSSL_BIO *bioOut = NULL; + char *inPath = NULL; + char *outPath = NULL; + int outSeen = 0; char password[MAX_PASSWORD_SIZE]; int passwordSz = MAX_PASSWORD_SIZE; byte* pass = NULL; @@ -100,21 +103,20 @@ int wolfCLU_PKCS8(int argc, char** argv) break; case WOLFCLU_INFILE: + inPath = optarg; bioIn = wolfSSL_BIO_new_file(optarg, "rb"); if (bioIn == NULL) { - wolfCLU_LogError("Unable to open pkcs8 file %s", - optarg); + wolfCLU_LogError("Unable to open pkcs8 file %s", optarg); ret = WOLFCLU_FATAL_ERROR; } break; case WOLFCLU_OUTFILE: - bioOut = wolfSSL_BIO_new_file(optarg, "wb"); - if (bioOut == NULL) { - wolfCLU_LogError("Unable to open output file %s", - optarg); - ret = WOLFCLU_FATAL_ERROR; - } + /* Opening now would truncate -in before it's read below if + * the two name the same file; deferred until -in is known + * to be a different path. */ + outSeen = 1; + outPath = optarg; break; case WOLFCLU_INFORM: @@ -159,6 +161,23 @@ int wolfCLU_PKCS8(int argc, char** argv) } } + /* -out as the last argv token binds a NULL optarg; without this the + * deferred open below is skipped and the private key is silently + * written to stdout with a success exit code. */ + if (ret == WOLFCLU_SUCCESS && outSeen && outPath == NULL) { + wolfCLU_LogError("-out requires a file name"); + ret = USER_INPUT_ERROR; + } + + /* Refuse an in-place overwrite before stdin is drained or a passphrase + * is prompted for: comparing the two paths has no side effects, so it + * can fail fast. Only the truncating open has to wait for -in. */ + if (ret == WOLFCLU_SUCCESS && outPath != NULL) { + if (wolfCLU_RejectSamePath(inPath, outPath) != WOLFCLU_SUCCESS) { + ret = WOLFCLU_FATAL_ERROR; + } + } + /* currently only supporting PKCS8 parsing, input is expected */ if (ret == WOLFCLU_SUCCESS && bioIn == NULL) { word32 keyLen = 0; @@ -234,6 +253,26 @@ int wolfCLU_PKCS8(int argc, char** argv) ret = WOLFCLU_FATAL_ERROR; } + /* Reject before -out is opened: this combination can never succeed, and + * opening -out would truncate and force-chmod an existing key on a path + * that is already doomed. */ + if (ret == WOLFCLU_SUCCESS && toPkcs8 == 1 && noCrypt == 0) { + WOLFCLU_LOG(WOLFCLU_E0, "Encrypting PKCS8 keys not yet supported"); + ret = WOLFCLU_FATAL_ERROR; + } + + /* -in has been fully read by now, so truncating -out can no longer + * destroy it. The in-place refusal itself was already decided above, + * before anything was read or prompted for. + * Always a private key (PKCS#1 or PKCS#8, encrypted or not), so lock it + * down owner-only. */ + if (ret == WOLFCLU_SUCCESS && outPath != NULL) { + bioOut = wolfCLU_OpenKeyFileBio(outPath); + if (bioOut == NULL) { + ret = WOLFCLU_FATAL_ERROR; + } + } + /* setup output bio to stdout if not already set */ if (ret == WOLFCLU_SUCCESS && bioOut == NULL) { bioOut = wolfSSL_BIO_new(wolfSSL_BIO_s_file()); @@ -248,11 +287,6 @@ int wolfCLU_PKCS8(int argc, char** argv) } } - if (ret == WOLFCLU_SUCCESS && toPkcs8 == 1 && noCrypt == 0) { - WOLFCLU_LOG(WOLFCLU_E0, "Encrypting PKCS8 keys not yet supported"); - ret = WOLFCLU_FATAL_ERROR; - } - if (ret == WOLFCLU_SUCCESS && pkey != NULL) { if (outForm == DER_FORM) { unsigned char *der = NULL; diff --git a/src/pkey/clu_pkey.c b/src/pkey/clu_pkey.c index f171d05c..7410bbad 100644 --- a/src/pkey/clu_pkey.c +++ b/src/pkey/clu_pkey.c @@ -424,6 +424,9 @@ int wolfCLU_pKeySetup(int argc, char** argv) WOLFSSL_EVP_PKEY *pkey = NULL; WOLFSSL_BIO *bioIn = NULL; WOLFSSL_BIO *bioOut = NULL; + char *inPath = NULL; + char *outPath = NULL; + int outSeen = 0; optind = 0; /* start at indent 0 */ while ((option = wolfCLU_GetOpt(argc, argv, "", pkey_options, @@ -444,21 +447,20 @@ int wolfCLU_pKeySetup(int argc, char** argv) break; case WOLFCLU_INFILE: + inPath = optarg; bioIn = wolfSSL_BIO_new_file(optarg, "rb"); if (bioIn == NULL) { - wolfCLU_LogError("Unable to open public key file %s", - optarg); + wolfCLU_LogError("Unable to open public key file %s", optarg); ret = WOLFCLU_FATAL_ERROR; } break; case WOLFCLU_OUTFILE: - bioOut = wolfSSL_BIO_new_file(optarg, "wb"); - if (bioOut == NULL) { - wolfCLU_LogError("Unable to open output file %s", - optarg); - ret = WOLFCLU_FATAL_ERROR; - } + /* Deferred: whether this is a private key (owner-only) or a + * public key (default perms) depends on -pubout, which may + * appear later on the command line. */ + outSeen = 1; + outPath = optarg; break; case WOLFCLU_INFORM: @@ -486,6 +488,14 @@ int wolfCLU_pKeySetup(int argc, char** argv) } + /* -out as the last argv token binds a NULL optarg; without this the + * deferred open below is skipped and the output silently goes to + * stdout with a success exit code. */ + if (ret == WOLFCLU_SUCCESS && outSeen && outPath == NULL) { + wolfCLU_LogError("-out requires a file name"); + ret = USER_INPUT_ERROR; + } + if (ret == WOLFCLU_SUCCESS && bioIn != NULL) { if (inForm == PEM_FORM) { if (pubIn) { @@ -509,6 +519,22 @@ int wolfCLU_pKeySetup(int argc, char** argv) } } + if (ret == WOLFCLU_SUCCESS && outPath != NULL) { + /* -pubout is fully parsed by now, so the secret-ness of -out is + * known. -in has been fully read, so this open cannot destroy it; + * still refuse an in-place overwrite. */ + if (wolfCLU_RejectSamePath(inPath, outPath) != WOLFCLU_SUCCESS) { + ret = WOLFCLU_FATAL_ERROR; + } + else { + bioOut = wolfCLU_OpenOutOrKeyFileBio(outPath, + pubOut ? WOLFCLU_OUT_PUBLIC : WOLFCLU_OUT_SECRET); + if (bioOut == NULL) { + ret = WOLFCLU_FATAL_ERROR; + } + } + } + if (ret == WOLFCLU_SUCCESS && bioOut == NULL) { bioOut = wolfSSL_BIO_new(wolfSSL_BIO_s_file()); if (bioOut == NULL) { diff --git a/src/pkey/clu_rsa.c b/src/pkey/clu_rsa.c index fa748e41..a538921e 100644 --- a/src/pkey/clu_rsa.c +++ b/src/pkey/clu_rsa.c @@ -80,6 +80,9 @@ int wolfCLU_RSA(int argc, char** argv) WOLFSSL_BIO *bioIn = NULL; WOLFSSL_BIO *bioOut = NULL; WOLFSSL_RSA *rsa = NULL; + char *inPath = NULL; + char *outPath = NULL; + int outSeen = 0; opterr = 0; /* do not display unrecognized options */ optind = 0; /* start at indent 0 */ @@ -92,21 +95,20 @@ int wolfCLU_RSA(int argc, char** argv) break; case WOLFCLU_INFILE: + inPath = optarg; bioIn = wolfSSL_BIO_new_file(optarg, "rb"); if (bioIn == NULL) { - wolfCLU_LogError("unable to open key file %s", - optarg); + wolfCLU_LogError("unable to open key file %s", optarg); ret = WOLFCLU_FATAL_ERROR; } break; case WOLFCLU_OUTFILE: - bioOut = wolfSSL_BIO_new_file(optarg, "wb"); - if (bioOut == NULL) { - wolfCLU_LogError("unable to open out file %s", - optarg); - ret = WOLFCLU_FATAL_ERROR; - } + /* Deferred: whether this is a private key (owner-only) or a + * public key/modulus (default perms) depends on -pubout, + * which may appear later on the command line. */ + outSeen = 1; + outPath = optarg; break; case WOLFCLU_INFORM: @@ -155,6 +157,14 @@ int wolfCLU_RSA(int argc, char** argv) } } + /* -out as the last argv token binds a NULL optarg; without this the + * deferred open below is skipped and the output silently goes to + * stdout with a success exit code. */ + if (ret == WOLFCLU_SUCCESS && outSeen && outPath == NULL) { + wolfCLU_LogError("-out requires a file name"); + ret = USER_INPUT_ERROR; + } + /* read in the RSA key */ if (ret == WOLFCLU_SUCCESS && bioIn != NULL) { if (inForm == PEM_FORM) { @@ -201,6 +211,21 @@ int wolfCLU_RSA(int argc, char** argv) } } + if (ret == WOLFCLU_SUCCESS && outPath != NULL) { + /* Use owner-only permissions for private key output. */ + if (wolfCLU_RejectSamePath(inPath, outPath) != WOLFCLU_SUCCESS) { + ret = WOLFCLU_FATAL_ERROR; + } + else { + bioOut = wolfCLU_OpenOutOrKeyFileBio(outPath, + (pubOut || noOut) ? WOLFCLU_OUT_PUBLIC : + WOLFCLU_OUT_SECRET); + if (bioOut == NULL) { + ret = WOLFCLU_FATAL_ERROR; + } + } + } + /* print to stdout if no -out was used */ if (ret == WOLFCLU_SUCCESS && bioOut == NULL) { bioOut = wolfSSL_BIO_new(wolfSSL_BIO_s_file()); diff --git a/src/server/clu_server_setup.c b/src/server/clu_server_setup.c index 467ceb23..d387e60e 100644 --- a/src/server/clu_server_setup.c +++ b/src/server/clu_server_setup.c @@ -23,9 +23,9 @@ #include #include #include -#include #ifndef WOLFCLU_NO_FILESYSTEM +#include static const struct option server_options[] = { {"-port", required_argument, 0, WOLFCLU_PORT }, @@ -98,6 +98,7 @@ static int _addServerArg(const char** args, const char* in, int* idx) int wolfCLU_Server(int argc, char** argv) { +#ifndef WOLFCLU_NO_FILESYSTEM func_args args; int ret = WOLFCLU_SUCCESS; int longIndex = 1; @@ -206,4 +207,10 @@ int wolfCLU_Server(int argc, char** argv) FreeTcpReady(&ready); return ret; +#else + (void)argc; + (void)argv; + WOLFCLU_LOG(WOLFCLU_E0, "No filesystem support"); + return WOLFCLU_FATAL_ERROR; +#endif } diff --git a/src/server/server.c b/src/server/server.c index 5b9c70dd..7aa199ec 100644 --- a/src/server/server.c +++ b/src/server/server.c @@ -23,6 +23,11 @@ * https://github.com/wolfSSL/wolfssl-examples/tree/master/tls */ +/* wolfclu/server.h only declares server_test() with a filesystem, and the + * example itself is built around loading certs and keys from files. Compile + * the whole thing out otherwise, exactly as src/client/client.c does. */ +#ifndef WOLFCLU_NO_FILESYSTEM + #ifdef HAVE_CONFIG_H #include #endif @@ -3927,3 +3932,5 @@ THREAD_RETURN WOLFSSL_THREAD server_test(void* args) char* myoptarg = NULL; #endif /* NO_MAIN_DRIVER */ + +#endif /* !WOLFCLU_NO_FILESYSTEM */ diff --git a/src/sign-verify/clu_crl_verify.c b/src/sign-verify/clu_crl_verify.c index 4d00e117..105e04c9 100644 --- a/src/sign-verify/clu_crl_verify.c +++ b/src/sign-verify/clu_crl_verify.c @@ -199,10 +199,8 @@ int wolfCLU_CRLVerify(int argc, char** argv) if (ret == WOLFCLU_SUCCESS && output != 0 && out != NULL) { - bioOut = wolfSSL_BIO_new_file(out, "wb"); + bioOut = wolfCLU_OpenOutFileBio(out); if (bioOut == NULL) { - wolfCLU_LogError("unable to open output file %s", - optarg); ret = WOLFCLU_FATAL_ERROR; } } diff --git a/src/sign-verify/clu_dgst_setup.c b/src/sign-verify/clu_dgst_setup.c index 9ed448bf..b619bd04 100644 --- a/src/sign-verify/clu_dgst_setup.c +++ b/src/sign-verify/clu_dgst_setup.c @@ -269,7 +269,7 @@ static int wolfCLU_dgstHmac(WOLFSSL_BIO* dataBio, char* hmacKey, WOLFSSL_BIO* outBio = NULL; if (outFile != NULL) { - outBio = wolfSSL_BIO_new_file(outFile, "wb"); + outBio = wolfCLU_OpenOutFileBio(outFile); } else { outBio = wolfSSL_BIO_new_fp(stdout, WOLFSSL_BIO_NOCLOSE); @@ -511,10 +511,8 @@ static int wolfCLU_dgstSignVerify(WOLFSSL_BIO* dataBio, WOLFSSL_BIO* pubKeyBio, /* write out the signature */ if (ret == WOLFCLU_SUCCESS) { - sigBio = wolfSSL_BIO_new_file(outFile, "wb"); + sigBio = wolfCLU_OpenOutFileBio(outFile); if (sigBio == NULL) { - wolfCLU_LogError("Unable to create signature file %s", - outFile); ret = WOLFCLU_FATAL_ERROR; } } @@ -663,8 +661,7 @@ int wolfCLU_dgst_setup(int argc, char** argv) case WOLFCLU_VERIFY: pubKeyBio = wolfSSL_BIO_new_file(optarg, "rb"); if (pubKeyBio == NULL) { - wolfCLU_LogError("Unable to open key file %s", - optarg); + wolfCLU_LogError("Unable to open key file %s", optarg); ret = WOLFCLU_FATAL_ERROR; } break; diff --git a/src/sign-verify/clu_sign.c b/src/sign-verify/clu_sign.c index 4be2dbcf..7d78b693 100644 --- a/src/sign-verify/clu_sign.c +++ b/src/sign-verify/clu_sign.c @@ -32,7 +32,11 @@ #define WOLFCLU_MAX_KEY_PEM_DER_SZ 65536 #endif /* WOLFCLU_MAX_KEY_PEM_DER_SZ */ -#ifndef WOLFCLU_NO_FILESYSTEM +/* Matches WOLFCLU_MAX_FILE_SIZE in clu_verify.c: the message being signed + * has the same upper bound as the message being verified. */ +#ifndef WOLFCLU_MAX_FILE_SIZE +#define WOLFCLU_MAX_FILE_SIZE 0xFFFFFFF +#endif /* WOLFCLU_MAX_FILE_SIZE */ int wolfCLU_KeyPemToDer(unsigned char** pkeyBuf, int pkeySz, int pubIn) { int ret = 0; @@ -118,41 +122,14 @@ int wolfCLU_sign_data(char* in, char* out, char* privKey, int keyType, { int ret; int fSz; - long fTell; - XFILE f; byte *data = NULL; - f = XFOPEN(in, "rb"); - if (f == NULL) { - wolfCLU_LogError("unable to open file %s", in); - return BAD_FUNC_ARG; - } - if (XFSEEK(f, 0, SEEK_END) != 0) { - wolfCLU_LogError("Failed to seek to end of file."); - XFCLOSE(f); - return WOLFCLU_FATAL_ERROR; - } - fTell = XFTELL(f); - if (fTell <= 0 || fTell > INT_MAX) { - wolfCLU_LogError("Incorrect input file size: %ld", fTell); - XFCLOSE(f); - return WOLFCLU_FATAL_ERROR; - } - fSz = (int)fTell; - - data = (byte*)XMALLOC((size_t)fSz, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - if (data == NULL) { - XFCLOSE(f); - return MEMORY_E; - } - - if (XFSEEK(f, 0, SEEK_SET) != 0 || - XFREAD(data, 1, (size_t)fSz, f) != (size_t)fSz) { - XFREE(data, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - XFCLOSE(f); - return WOLFCLU_FATAL_ERROR; + /* Read file to buffer; accepts 0-length files. Logs on failure. */ + ret = wolfCLU_ReadMessageFileToBuffer(in, WOLFCLU_MAX_FILE_SIZE, &data, + &fSz); + if (ret != WOLFCLU_SUCCESS) { + return ret; } - XFCLOSE(f); switch(keyType) { @@ -322,9 +299,8 @@ int wolfCLU_sign_data_rsa(byte* data, char* out, word32 dataSz, char* privKey, ret = wc_RsaSSL_Sign(data, dataSz, outBuf, (word32)outBufSz, &key, &rng); if (ret >= 0) { XFILE s; - s = XFOPEN(out, "wb"); + s = wolfCLU_OpenOutFile(out); if (s == NULL) { - wolfCLU_LogError("Failed to open output file"); ret = BAD_FUNC_ARG; } else { @@ -506,9 +482,8 @@ int wolfCLU_sign_data_ecc(byte* data, char* out, word32 fSz, char* privKey, } if (ret >= 0) { XFILE s; - s = XFOPEN(out, "wb"); + s = wolfCLU_OpenOutFile(out); if (s == NULL) { - wolfCLU_LogError("Failed to open file"); ret = BAD_FUNC_ARG; } else { @@ -678,9 +653,8 @@ int wolfCLU_sign_data_ed25519 (byte* data, char* out, word32 fSz, char* privKey, ret = wc_ed25519_sign_msg(data, fSz, outBuf, &outLen, &key); if (ret >= 0) { XFILE s; - s = XFOPEN(out, "wb"); + s = wolfCLU_OpenOutFile(out); if (s == NULL) { - wolfCLU_LogError("Failed to open file"); ret = BAD_FUNC_ARG; } else { @@ -856,10 +830,9 @@ int wolfCLU_sign_data_dilithium (byte* data, char* out, word32 dataSz, char* pri if (ret == 0) { XFILE outFile; - outFile = XFOPEN(out, "wb"); + outFile = wolfCLU_OpenOutFile(out); if (outFile == NULL) { - wolfCLU_LogError("Failed to open output file %s", out); ret = BAD_FUNC_ARG; } else { if ((int)XFWRITE(outBuf, 1, outBufSz, outFile) <= 0) { @@ -1034,10 +1007,9 @@ int wolfCLU_sign_data_xmss(byte* data, char* out, int fSz, char* privKey) /* output signature */ if (ret == 0) { - outFile = XFOPEN(out, "wb"); + outFile = wolfCLU_OpenOutFile(out); if (outFile == NULL) { ret = OUTPUT_FILE_ERROR; - wolfCLU_LogError("Failed to open file %s.\nRET: %d", out, ret); } else if (ret == 0) { /* write to file */ @@ -1228,10 +1200,9 @@ int wolfCLU_sign_data_xmssmt(byte* data, char* out, int fSz, char* privKey) /* output signature */ if (ret == 0) { - outFile = XFOPEN(out, "wb"); + outFile = wolfCLU_OpenOutFile(out); if (outFile == NULL) { ret = OUTPUT_FILE_ERROR; - wolfCLU_LogError("Failed to open file %s.\nRET: %d", out, ret); } else if (ret == 0) { /* write to file */ @@ -1270,4 +1241,4 @@ int wolfCLU_sign_data_xmssmt(byte* data, char* out, int fSz, char* privKey) #endif /* WOLFSSL_HAVE_XMSS */ } -#endif /* !WOLFCLU_NO_FILESYSTEM */ + diff --git a/src/sign-verify/clu_verify.c b/src/sign-verify/clu_verify.c index b3d56d34..c5c47412 100644 --- a/src/sign-verify/clu_verify.c +++ b/src/sign-verify/clu_verify.c @@ -32,56 +32,73 @@ * allocated. */ #define WOLFCLU_MAX_FILE_SIZE 0xFFFFFFF +/* Reads the message/digest file used by every wolfCLU_verify_signature() + * case except RSA (which verifies the signature directly). On success + * stores the buffer in *hash and its length in *hSzOut and returns + * WOLFCLU_SUCCESS; on failure returns the error wolfCLU_ReadMessageFileToBuffer() + * reported, already logged, for the caller to propagate as-is. */ +static int wolfCLU_ReadVerifyHash(char* hashFile, byte** hash, long* hSzOut) +{ + int hSzInt = 0; + int hRet = wolfCLU_ReadMessageFileToBuffer(hashFile, WOLFCLU_MAX_FILE_SIZE, + hash, &hSzInt); + if (hRet == WOLFCLU_SUCCESS) { + *hSzOut = hSzInt; + } + return hRet; +} + +#ifdef WOLFSSL_HAVE_XMSS +/* Reads an XMSS/XMSS-MT public key file, shared by + * wolfCLU_verify_signature_xmss() and wolfCLU_verify_signature_xmssmt(): + * both need the same "read it in, then confirm it is at least large enough + * to hold the OID" check before they can look at the parameter set. On + * success stores the buffer in *keyBuf and its length in *keyFileSzOut and + * returns WOLFCLU_SUCCESS; on failure returns the error, already logged. */ +static int wolfCLU_ReadXmssPubKey(char* pubKey, byte** keyBuf, + long* keyFileSzOut) +{ + int keyFileSzInt = 0; + int ret = wolfCLU_ReadFileToBuffer(pubKey, WOLFCLU_MAX_FILE_SIZE, keyBuf, + &keyFileSzInt); + + if (ret == WOLFCLU_SUCCESS) { + if (keyFileSzInt < (int)XMSS_OID_LEN) { + ret = WOLFCLU_FATAL_ERROR; + wolfCLU_LogError("File: %s is too small to hold a valid " + "XMSS public key.", pubKey); + } + else { + *keyFileSzOut = keyFileSzInt; + } + } + return ret; +} +#endif /* WOLFSSL_HAVE_XMSS */ + int wolfCLU_verify_signature(char* sig, char* hashFile, char* out, char* keyPath, int keyType, int pubIn, int inForm) { long hSz = 0; long fSz; + int dataSz = 0; int ret = WOLFCLU_FATAL_ERROR; byte* hash = NULL; byte* data = NULL; - XFILE h; - XFILE f; - if (sig == NULL) { - return BAD_FUNC_ARG; - } - - f = XFOPEN(sig, "rb"); - if (f == NULL) { - wolfCLU_LogError("unable to open file %s", sig); - return BAD_FUNC_ARG; - } - XFSEEK(f, 0, SEEK_END); - fSz = XFTELL(f); - - if (fSz < 0) { - wolfCLU_LogError("Invalid Sig File %s.", sig); - XFCLOSE(f); + if (sig == NULL) { return BAD_FUNC_ARG; } - if (fSz > WOLFCLU_MAX_FILE_SIZE) { - wolfCLU_LogError("File: %s exceeds max size of 0x%X " - "bytes.", sig, (unsigned)WOLFCLU_MAX_FILE_SIZE); - XFCLOSE(f); - return WOLFCLU_FATAL_ERROR; - } - - data = (byte*)XMALLOC(fSz, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - if (data == NULL) { - XFCLOSE(f); - return MEMORY_E; - } - if (XFSEEK(f, 0, SEEK_SET) != 0 || (long)XFREAD(data, 1, fSz, f) != fSz) { - XFCLOSE(f); - XFREE(data, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - return WOLFCLU_FATAL_ERROR; + ret = wolfCLU_ReadFileToBuffer(sig, WOLFCLU_MAX_FILE_SIZE, &data, &dataSz); + if (ret != WOLFCLU_SUCCESS) { + return ret; } - XFCLOSE(f); + fSz = (long)dataSz; + ret = WOLFCLU_FATAL_ERROR; switch(keyType) { case RSA_SIG_VER: @@ -90,93 +107,20 @@ int wolfCLU_verify_signature(char* sig, char* hashFile, char* out, break; case ECC_SIG_VER: - h = XFOPEN(hashFile,"rb"); - if (h == NULL) { - wolfCLU_LogError("unable to open file %s", hashFile); - ret = BAD_FUNC_ARG; - break; - } - - XFSEEK(h, 0, SEEK_END); - hSz = XFTELL(h); - - if (hSz < 0) { - wolfCLU_LogError("Unable to Get Size of Hash File %s.", - hashFile); - ret = BAD_FUNC_ARG; - XFCLOSE(h); - break; - } - - if (hSz > WOLFCLU_MAX_FILE_SIZE) { - wolfCLU_LogError("File: %s exceeds max size of 0x%X " - "bytes.", hashFile, (unsigned)WOLFCLU_MAX_FILE_SIZE); - ret = WOLFCLU_FATAL_ERROR; - XFCLOSE(h); + ret = wolfCLU_ReadVerifyHash(hashFile, &hash, &hSz); + if (ret != WOLFCLU_SUCCESS) { break; } - - hash = (byte*)XMALLOC(hSz, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - if (hash == NULL) { - ret = MEMORY_E; - XFCLOSE(h); - break; - } - - if (XFSEEK(h, 0, SEEK_SET) != 0 || (int)XFREAD(hash, 1, hSz, h) != hSz) { - XFCLOSE(h); - XFREE(hash, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - XFREE(data, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - return WOLFCLU_FATAL_ERROR; - } - XFCLOSE(h); ret = wolfCLU_verify_signature_ecc(data, (int)fSz, hash, (int)hSz, keyPath, pubIn, inForm); break; case ED25519_SIG_VER: #ifdef HAVE_ED25519 - h = XFOPEN(hashFile, "rb"); - if (h == NULL) { - wolfCLU_LogError("unable to open file %s", hashFile); - ret = BAD_FUNC_ARG; - break; - } - - XFSEEK(h, 0, SEEK_END); - hSz = XFTELL(h); - - - if (hSz < 0) { - wolfCLU_LogError("Unable to Get Size of Hash File %s.", - hashFile); - ret = BAD_FUNC_ARG; - XFCLOSE(h); + ret = wolfCLU_ReadVerifyHash(hashFile, &hash, &hSz); + if (ret != WOLFCLU_SUCCESS) { break; } - - if (hSz > WOLFCLU_MAX_FILE_SIZE) { - wolfCLU_LogError("File: %s exceeds max size of 0x%X " - "bytes.", hashFile, (unsigned)WOLFCLU_MAX_FILE_SIZE); - ret = WOLFCLU_FATAL_ERROR; - XFCLOSE(h); - break; - } - - hash = (byte*)XMALLOC(hSz, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - if (hash == NULL) { - ret = MEMORY_E; - XFCLOSE(h); - break; - } - - if (XFSEEK(h, 0, SEEK_SET) != 0 || (int)XFREAD(hash, 1, hSz, h) != hSz) { - XFCLOSE(h); - XFREE(hash, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - XFREE(data, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - return WOLFCLU_FATAL_ERROR; - } - XFCLOSE(h); ret = wolfCLU_verify_signature_ed25519(data, (int)fSz, hash, (int)hSz, keyPath, pubIn, inForm); #endif @@ -184,50 +128,10 @@ int wolfCLU_verify_signature(char* sig, char* hashFile, char* out, #ifdef HAVE_DILITHIUM case DILITHIUM_SIG_VER: - /* hashFIle means msgFile */ - h = XFOPEN(hashFile, "rb"); - if (h == NULL) { - wolfCLU_LogError("unable to open file %s", hashFile); - ret = BAD_FUNC_ARG; + ret = wolfCLU_ReadVerifyHash(hashFile, &hash, &hSz); + if (ret != WOLFCLU_SUCCESS) { break; } - - /* hSz means msgLen */ - XFSEEK(h, 0, SEEK_END); - hSz = XFTELL(h); - - if (hSz < 0) { - wolfCLU_LogError("Unable to Get Size of Hash File %s.", - hashFile); - ret = BAD_FUNC_ARG; - XFCLOSE(h); - break; - } - - if (hSz > WOLFCLU_MAX_FILE_SIZE) { - wolfCLU_LogError("File: %s exceeds max size of 0x%X " - "bytes.", hashFile, (unsigned)WOLFCLU_MAX_FILE_SIZE); - ret = WOLFCLU_FATAL_ERROR; - XFCLOSE(h); - break; - } - - /* hash means msg */ - hash = (byte*)XMALLOC(hSz, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - if (hash == NULL) { - ret = MEMORY_E; - XFCLOSE(h); - break; - } - - if (XFSEEK(h, 0, SEEK_SET) != 0 || (int)XFREAD(hash, 1, hSz, h) != hSz) { - XFCLOSE(h); - XFREE(hash, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - XFREE(data, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - return WOLFCLU_FATAL_ERROR; - } - XFCLOSE(h); - ret = wolfCLU_verify_signature_dilithium(data, (int)fSz, hash, (int)hSz, keyPath, inForm); break; @@ -235,100 +139,19 @@ int wolfCLU_verify_signature(char* sig, char* hashFile, char* out, #ifdef WOLFSSL_HAVE_XMSS case XMSS_SIG_VER: - /* hashFIle means msgFile */ - h = XFOPEN(hashFile, "rb"); - if (h == NULL) { - wolfCLU_LogError("unable to open file %s", hashFile); - ret = BAD_FUNC_ARG; - break; - } - - /* hSz means msgLen */ - XFSEEK(h, 0, SEEK_END); - hSz = XFTELL(h); - - - if (hSz < 0) { - wolfCLU_LogError("Unable to Get Size of Hash File %s.", - hashFile); - ret = BAD_FUNC_ARG; - XFCLOSE(h); + ret = wolfCLU_ReadVerifyHash(hashFile, &hash, &hSz); + if (ret != WOLFCLU_SUCCESS) { break; } - - if (hSz > WOLFCLU_MAX_FILE_SIZE) { - wolfCLU_LogError("File: %s exceeds max size of 0x%X " - "bytes.", hashFile, (unsigned)WOLFCLU_MAX_FILE_SIZE); - ret = WOLFCLU_FATAL_ERROR; - XFCLOSE(h); - break; - } - - /* hash means msg */ - hash = (byte*)XMALLOC(hSz, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - if (hash == NULL) { - ret = MEMORY_E; - XFCLOSE(h); - break; - } - - if (XFSEEK(h, 0, SEEK_SET) != 0 || (int)XFREAD(hash, 1, hSz, h) != hSz) { - XFCLOSE(h); - XFREE(hash, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - XFREE(data, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - return WOLFCLU_FATAL_ERROR; - } - XFCLOSE(h); - ret = wolfCLU_verify_signature_xmss(data, (int)fSz, hash, (int)hSz, keyPath); break; case XMSSMT_SIG_VER: - /* hashFIle means msgFile */ - h = XFOPEN(hashFile, "rb"); - if (h == NULL) { - wolfCLU_LogError("unable to open file %s", hashFile); - ret = BAD_FUNC_ARG; - break; - } - - /* hSz means msgLen */ - XFSEEK(h, 0, SEEK_END); - hSz = XFTELL(h); - - if (hSz < 0) { - wolfCLU_LogError("Unable to Get Size of Hash File %s.", - hashFile); - ret = BAD_FUNC_ARG; - XFCLOSE(h); - break; - } - - if (hSz > WOLFCLU_MAX_FILE_SIZE) { - wolfCLU_LogError("File: %s exceeds max size of 0x%X " - "bytes.", hashFile, (unsigned)WOLFCLU_MAX_FILE_SIZE); - ret = WOLFCLU_FATAL_ERROR; - XFCLOSE(h); - break; - } - - /* hash means msg */ - hash = (byte*)XMALLOC(hSz, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - if (hash == NULL) { - ret = MEMORY_E; - XFCLOSE(h); + ret = wolfCLU_ReadVerifyHash(hashFile, &hash, &hSz); + if (ret != WOLFCLU_SUCCESS) { break; } - - if (XFSEEK(h, 0, SEEK_SET) != 0 || (int)XFREAD(hash, 1, hSz, h) != hSz) { - XFCLOSE(h); - XFREE(hash, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - XFREE(data, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - return WOLFCLU_FATAL_ERROR; - } - XFCLOSE(h); - ret = wolfCLU_verify_signature_xmssmt(data, (int)fSz, hash, (int)hSz, keyPath); break; @@ -353,9 +176,9 @@ int wolfCLU_verify_signature_rsa(byte* sig, char* out, int sigSz, char* keyPath, #ifndef NO_RSA int ret; + int keyFileSzInt = 0; long keyFileSz = 0; word32 index = 0; - XFILE keyPathFile = NULL; RsaKey key; byte* keyBuf = NULL; byte* outBuf = NULL; @@ -367,37 +190,15 @@ int wolfCLU_verify_signature_rsa(byte* sig, char* out, int sigSz, char* keyPath, wolfCLU_LogError("Failed to initialize RsaKey.\nRet: %d", ret); } - /* open, read, and store RSA key */ - if (ret == 0) { - keyPathFile = XFOPEN(keyPath, "rb"); - if (keyPathFile == NULL) { - wolfCLU_LogError("unable to open file %s", keyPath); - ret = BAD_FUNC_ARG; - } - } + /* open, read, and store RSA key */ if (ret == 0) { - XFSEEK(keyPathFile, 0, SEEK_END); - keyFileSz = XFTELL(keyPathFile); - if (keyFileSz < 0) { - wolfCLU_LogError("Unable to Get Size of Key File %s.", keyPath); - ret = BAD_FUNC_ARG; - } - else if (keyFileSz > WOLFCLU_MAX_FILE_SIZE) { - wolfCLU_LogError("File: %s exceeds max size of 0x%X " - "bytes.", keyPath, (unsigned)WOLFCLU_MAX_FILE_SIZE); - ret = WOLFCLU_FATAL_ERROR; - } - } - if (ret == 0) { - keyBuf = (byte*)XMALLOC(keyFileSz+1, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - if (keyBuf == NULL) { - ret = MEMORY_E; + ret = wolfCLU_ReadFileToBuffer(keyPath, WOLFCLU_MAX_FILE_SIZE, + &keyBuf, &keyFileSzInt); + if (ret == WOLFCLU_SUCCESS) { + keyFileSz = keyFileSzInt; + ret = 0; } - } - if (ret == 0) { - XMEMSET(keyBuf, 0, keyFileSz+1); - if (XFSEEK(keyPathFile, 0, SEEK_SET) != 0 || - (int)XFREAD(keyBuf, 1, keyFileSz, keyPathFile) != keyFileSz) { + else { ret = WOLFCLU_FATAL_ERROR; } } @@ -468,9 +269,8 @@ int wolfCLU_verify_signature_rsa(byte* sig, char* out, int sigSz, char* keyPath, /* write the output to the specified file */ if (ret > 0) { int writeSz = ret; - XFILE s = XFOPEN(out, "wb"); + XFILE s = wolfCLU_OpenOutFile(out); if (s == NULL) { - wolfCLU_LogError("Unable to open file %s", out); ret = BAD_FUNC_ARG; } else { @@ -483,14 +283,16 @@ int wolfCLU_verify_signature_rsa(byte* sig, char* out, int sigSz, char* keyPath, } /* Cleanup allocated resources */ - if (keyPathFile != NULL) { - XFCLOSE(keyPathFile); - } - if (outBuf != NULL) { XFREE(outBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); } if (keyBuf != NULL) { + /* With pubIn == 0 this holds a private key. Wipe using the size + * of the CURRENT allocation: wolfCLU_KeyPemToDer() swaps keyBuf + * for a smaller DER buffer and the tracking variable differs + * between these functions. + */ + wolfCLU_ForceZero(keyBuf, (unsigned int)keyFileSz); XFREE(keyBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); } @@ -509,11 +311,11 @@ int wolfCLU_verify_signature_ecc(byte* sig, int sigSz, byte* hash, int hashSz, #ifdef HAVE_ECC int ret; + int keyFileSzInt = 0; long keyFileSz = 0; int stat = 0; word32 index = 0; - XFILE keyPathFile = NULL; ecc_key key; byte* keyBuf = NULL; byte* outBuf = NULL; @@ -527,37 +329,14 @@ int wolfCLU_verify_signature_ecc(byte* sig, int sigSz, byte* hash, int hashSz, wolfCLU_LogError("Failed to initialize ecc key.\nRet: %d", ret); } - /* open, read, and store Ecc key */ - if (ret == 0) { - keyPathFile = XFOPEN(keyPath, "rb"); - if (keyPathFile == NULL) { - wolfCLU_LogError("unable to open file %s", keyPath); - ret = BAD_FUNC_ARG; - } - } - if (ret == 0) { - XFSEEK(keyPathFile, 0, SEEK_END); - keyFileSz = XFTELL(keyPathFile); - if (keyFileSz < 0) { - wolfCLU_LogError("Unable to Get Size of Key File %s.", keyPath); - ret = BAD_FUNC_ARG; - } - else if (keyFileSz > WOLFCLU_MAX_FILE_SIZE) { - wolfCLU_LogError("File: %s exceeds max size of 0x%X " - "bytes.", keyPath, (unsigned)WOLFCLU_MAX_FILE_SIZE); - ret = WOLFCLU_FATAL_ERROR; - } - } if (ret == 0) { - keyBuf = (byte*)XMALLOC(keyFileSz+1, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - if (keyBuf == NULL) { - ret = MEMORY_E; + ret = wolfCLU_ReadFileToBuffer(keyPath, WOLFCLU_MAX_FILE_SIZE, + &keyBuf, &keyFileSzInt); + if (ret == WOLFCLU_SUCCESS) { + keyFileSz = keyFileSzInt; + ret = 0; } - } - if (ret == 0) { - XMEMSET(keyBuf, 0, keyFileSz+1); - if (XFSEEK(keyPathFile, 0, SEEK_SET) != 0 || - (int)XFREAD(keyBuf, 1, keyFileSz, keyPathFile) != keyFileSz) { + else { ret = WOLFCLU_FATAL_ERROR; } } @@ -661,14 +440,16 @@ int wolfCLU_verify_signature_ecc(byte* sig, int sigSz, byte* hash, int hashSz, } /* cleanup allocated resources */ - if (keyPathFile != NULL) { - XFCLOSE(keyPathFile); - } - if (outBuf != NULL) { XFREE(outBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); } if (keyBuf != NULL) { + /* With pubIn == 0 this holds a private key. Wipe using the size + * of the CURRENT allocation: wolfCLU_KeyPemToDer() swaps keyBuf + * for a smaller DER buffer and the tracking variable differs + * between these functions. + */ + wolfCLU_ForceZero(keyBuf, (unsigned int)keyFileSz); XFREE(keyBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); } @@ -688,9 +469,9 @@ int wolfCLU_verify_signature_ed25519(byte* sig, int sigSz, int ret; int stat = 0; word32 index = 0; + int keyFileSzInt = 0; long keyFileSz = 0; - XFILE keyPathFile = NULL; ed25519_key key; byte* keyBuf = NULL; @@ -702,37 +483,14 @@ int wolfCLU_verify_signature_ed25519(byte* sig, int sigSz, wolfCLU_LogError("Failed to initialize ED25519 key.\nRet: %d", ret); } - /* open, read, and store ED25519 key */ - if (ret == 0) { - keyPathFile = XFOPEN(keyPath, "rb"); - if (keyPathFile == NULL) { - wolfCLU_LogError("unable to open file %s", keyPath); - ret = BAD_FUNC_ARG; - } - } - if (ret == 0) { - XFSEEK(keyPathFile, 0, SEEK_END); - keyFileSz = XFTELL(keyPathFile); - if (keyFileSz < 0) { - wolfCLU_LogError("Unable to Get Size of Key File %s.", keyPath); - ret = BAD_FUNC_ARG; - } - else if (keyFileSz > WOLFCLU_MAX_FILE_SIZE) { - wolfCLU_LogError("File: %s exceeds max size of 0x%X " - "bytes.", keyPath, (unsigned)WOLFCLU_MAX_FILE_SIZE); - ret = WOLFCLU_FATAL_ERROR; - } - } if (ret == 0) { - keyBuf = (byte*)XMALLOC(keyFileSz+1, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - if (keyBuf == NULL) { - ret = MEMORY_E; + ret = wolfCLU_ReadFileToBuffer(keyPath, WOLFCLU_MAX_FILE_SIZE, + &keyBuf, &keyFileSzInt); + if (ret == WOLFCLU_SUCCESS) { + keyFileSz = keyFileSzInt; + ret = 0; } - } - if (ret == 0) { - XMEMSET(keyBuf, 0, keyFileSz+1); - if (XFSEEK(keyPathFile, 0, SEEK_SET) != 0 || - (int)XFREAD(keyBuf, 1, keyFileSz, keyPathFile) != keyFileSz) { + else { ret = WOLFCLU_FATAL_ERROR; } } @@ -817,11 +575,13 @@ int wolfCLU_verify_signature_ed25519(byte* sig, int sigSz, } /* cleanup allocated resources */ - if (keyPathFile != NULL) { - XFCLOSE(keyPathFile); - } - if (keyBuf != NULL) { + /* With pubIn == 0 this holds a private key. Wipe using the size + * of the CURRENT allocation: wolfCLU_KeyPemToDer() swaps keyBuf + * for a smaller DER buffer and the tracking variable differs + * between these functions. + */ + wolfCLU_ForceZero(keyBuf, (unsigned int)keyFileSz); XFREE(keyBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); } @@ -840,7 +600,6 @@ int wolfCLU_verify_signature_dilithium(byte* sig, int sigSz, byte* msg, #ifdef HAVE_DILITHIUM int ret = 0; - XFILE keyFile = NULL; byte* keyBuf = NULL; long keyFileSz = 0; word32 keyBufSz = 0; @@ -872,62 +631,21 @@ int wolfCLU_verify_signature_dilithium(byte* sig, int sigSz, byte* msg, } /* open and read public key */ - keyFile = XFOPEN(keyPath, "rb"); - if (keyFile == NULL) { - wolfCLU_LogError("Failed to open public key FILE."); - wc_dilithium_free(key); - #ifdef WOLFSSL_SMALL_STACK - XFREE(key, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - #endif - return BAD_FUNC_ARG; - } - - XFSEEK(keyFile, 0, SEEK_END); - keyFileSz = XFTELL(keyFile); - if (keyFileSz <= 0) { - wolfCLU_LogError("Failed to get valid size of public key FILE."); - XFCLOSE(keyFile); - wc_dilithium_free(key); - #ifdef WOLFSSL_SMALL_STACK - XFREE(key, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - #endif - return BAD_FUNC_ARG; - } - if (keyFileSz > DILITHIUM_MAX_BOTH_KEY_PEM_SIZE) { - wolfCLU_LogError("Incorrect public key file size: %ld", keyFileSz); - XFCLOSE(keyFile); - wc_dilithium_free(key); - #ifdef WOLFSSL_SMALL_STACK - XFREE(key, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - #endif - return WOLFCLU_FATAL_ERROR; - } - - keyBuf = (byte*)XMALLOC(keyFileSz + 1, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - if (keyBuf == NULL) { - wolfCLU_LogError("Failed to malloc key buffer."); - XFCLOSE(keyFile); - wc_dilithium_free(key); - #ifdef WOLFSSL_SMALL_STACK - XFREE(key, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - #endif - return MEMORY_E; - } - XMEMSET(keyBuf, 0, keyFileSz + 1); - - if (XFSEEK(keyFile, 0, SEEK_SET) != 0 || - (int)XFREAD(keyBuf, 1, keyFileSz, keyFile) != keyFileSz) { - wolfCLU_LogError("Failed to read public key.\nRET: %d", ret); - XFCLOSE(keyFile); - XFREE(keyBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - wc_dilithium_free(key); - #ifdef WOLFSSL_SMALL_STACK - XFREE(key, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - #endif - return WOLFCLU_FATAL_ERROR; + { + int keyFileSzInt = 0; + int keyRet = wolfCLU_ReadFileToBuffer(keyPath, + DILITHIUM_MAX_BOTH_KEY_PEM_SIZE, &keyBuf, &keyFileSzInt); + if (keyRet != WOLFCLU_SUCCESS) { + /* wolfCLU_ReadFileToBuffer() already reported the reason. */ + wc_dilithium_free(key); + #ifdef WOLFSSL_SMALL_STACK + XFREE(key, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + #endif + return keyRet; + } + keyFileSz = keyFileSzInt; } keyBufSz = (word32)keyFileSz; - XFCLOSE(keyFile); /* convert PEM to DER if necessary */ if (inForm == PEM_FORM) { @@ -940,6 +658,10 @@ int wolfCLU_verify_signature_dilithium(byte* sig, int sigSz, byte* msg, } else { wolfCLU_LogError("Failed to convert PEM to DER.\nRET: %d", ret); + /* The conversion failed, so keyBuf still holds the original + * file contents and keyBufSz is still its allocation size. + * Wipe it here too, matching the success path below. */ + wolfCLU_ForceZero(keyBuf, keyBufSz); XFREE(keyBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); wc_dilithium_free(key); #ifdef WOLFSSL_SMALL_STACK @@ -955,6 +677,11 @@ int wolfCLU_verify_signature_dilithium(byte* sig, int sigSz, byte* msg, /* retrieving public key and storing in the dilithium key */ ret = wc_Dilithium_PublicKeyDecode(keyBuf, &index, key, keyBufSz); + /* keyBuf holds whatever file -inkey named, which may be a private key. + * keyBufSz (not keyFileSz) is the size of the CURRENT allocation: + * wolfCLU_KeyPemToDer() swaps keyBuf for a smaller DER buffer and this + * function tracks that size in keyBufSz. */ + wolfCLU_ForceZero(keyBuf, keyBufSz); XFREE(keyBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); if (ret != 0) { wolfCLU_LogError("Failed to decode public key.\nRET: %d", ret); @@ -1009,7 +736,6 @@ int wolfCLU_verify_signature_xmss(byte* sig, int sigSz, { #ifdef WOLFSSL_HAVE_XMSS int ret = 0; - XFILE keyFile = NULL; /* public key file */ byte* keyBuf = NULL; /* public key buffer */ long keyFileSz = 0; /* public key buffer size */ word32 oid = 0x0; /* OID of the XMSS parameter */ @@ -1035,48 +761,9 @@ int wolfCLU_verify_signature_xmss(byte* sig, int sigSz, /* open and read public key */ if (ret == 0) { - keyFile = XFOPEN(pubKey, "rb"); - if (keyFile == NULL) { - ret = OUTPUT_FILE_ERROR; - wolfCLU_LogError("Failed to open Public key FILE."); - } - } - - if (ret == 0) { - XFSEEK(keyFile, 0, SEEK_END); - keyFileSz = XFTELL(keyFile); - if (keyFileSz < 0) { - ret = WOLFCLU_FATAL_ERROR; - wolfCLU_LogError("Failed to get size of public key FILE."); - } - else if (keyFileSz > WOLFCLU_MAX_FILE_SIZE) { - ret = WOLFCLU_FATAL_ERROR; - wolfCLU_LogError("File: %s exceeds max size of 0x%X " - "bytes.", pubKey, (unsigned)WOLFCLU_MAX_FILE_SIZE); - } - } - - if (ret == 0) { - keyBuf = (byte*)XMALLOC(keyFileSz, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - if (keyBuf == NULL) { - ret = MEMORY_E; - wolfCLU_LogError("Failed to malloc key buffer.\nRET: %d", ret); - } - else { - XMEMSET(keyBuf, 0, keyFileSz); - } - } - - if (ret == 0) { - if (XFSEEK(keyFile, 0, SEEK_SET) != 0 || - (int)XFREAD(keyBuf, 1, keyFileSz, keyFile) != keyFileSz) { - ret = WOLFCLU_FATAL_ERROR; - wolfCLU_LogError("Failed to read public key." - "\nRET: %d", ret); - } - else { - XFCLOSE(keyFile); - keyFile = NULL; + ret = wolfCLU_ReadXmssPubKey(pubKey, &keyBuf, &keyFileSz); + if (ret == WOLFCLU_SUCCESS) { + ret = 0; } } @@ -1144,9 +831,6 @@ int wolfCLU_verify_signature_xmss(byte* sig, int sigSz, } /* cleanup allocated resources */ - if (keyFile != NULL) { - XFCLOSE(keyFile); - } if (keyBuf != NULL) { XFREE(keyBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); } @@ -1176,7 +860,6 @@ int wolfCLU_verify_signature_xmssmt(byte* sig, int sigSz, { #ifdef WOLFSSL_HAVE_XMSS int ret = 0; - XFILE keyFile = NULL; /* public key file */ byte* keyBuf = NULL; /* public key buffer */ long keyFileSz = 0; /* public key buffer size */ word32 oid = 0x0; /* OID of the XMSS parameter */ @@ -1202,47 +885,9 @@ int wolfCLU_verify_signature_xmssmt(byte* sig, int sigSz, /* open and read public key */ if (ret == 0) { - keyFile = XFOPEN(pubKey, "rb"); - if (keyFile == NULL) { - ret = OUTPUT_FILE_ERROR; - wolfCLU_LogError("Failed to open Public key FILE."); - } - } - - if (ret == 0) { - XFSEEK(keyFile, 0, SEEK_END); - keyFileSz = XFTELL(keyFile); - if (keyFileSz < 0) { - ret = WOLFCLU_FATAL_ERROR; - wolfCLU_LogError("Failed to get size of public key FILE."); - } - else if (keyFileSz > WOLFCLU_MAX_FILE_SIZE) { - ret = WOLFCLU_FATAL_ERROR; - wolfCLU_LogError("File: %s exceeds max size of 0x%X " - "bytes.", pubKey, (unsigned)WOLFCLU_MAX_FILE_SIZE); - } - } - - if (ret == 0) { - keyBuf = (byte*)XMALLOC(keyFileSz, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - if (keyBuf == NULL) { - ret = MEMORY_E; - wolfCLU_LogError("Failed to malloc key buffer.\nRET: %d", ret); - } - else { - XMEMSET(keyBuf, 0, keyFileSz); - } - } - - if (ret == 0) { - if (XFSEEK(keyFile, 0, SEEK_SET) != 0 || - (int)XFREAD(keyBuf, 1, keyFileSz, keyFile) != keyFileSz) { - ret = WOLFCLU_FATAL_ERROR; - wolfCLU_LogError("Failed to read public key.\nRET: %d", ret); - } - else { - XFCLOSE(keyFile); - keyFile = NULL; + ret = wolfCLU_ReadXmssPubKey(pubKey, &keyBuf, &keyFileSz); + if (ret == WOLFCLU_SUCCESS) { + ret = 0; } } @@ -1325,9 +970,6 @@ int wolfCLU_verify_signature_xmssmt(byte* sig, int sigSz, } /* cleanup allocated resources */ - if (keyFile != NULL) { - XFCLOSE(keyFile); - } if (keyBuf != NULL) { XFREE(keyBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); } diff --git a/src/sign-verify/clu_x509_verify.c b/src/sign-verify/clu_x509_verify.c index 98367096..81c1f8b1 100644 --- a/src/sign-verify/clu_x509_verify.c +++ b/src/sign-verify/clu_x509_verify.c @@ -51,7 +51,6 @@ static void wolfCLU_x509VerifyHelp(void) "1 cert as -untrusted"); } -#endif static X509* load_cert_from_file(const char* filename) { WOLFSSL_BIO* bio = NULL; @@ -75,6 +74,7 @@ static X509* load_cert_from_file(const char* filename) { return cert; } +#endif /* !WOLFCLU_NO_FILESYSTEM */ int wolfCLU_x509Verify(int argc, char** argv) { diff --git a/src/tools/clu_base64.c b/src/tools/clu_base64.c index 70c3ee6d..dedb3a30 100644 --- a/src/tools/clu_base64.c +++ b/src/tools/clu_base64.c @@ -24,6 +24,8 @@ #include #include +/* Only referenced by the full build of wolfCLU_Base64Setup() below. */ +#if !defined(WOLFCLU_NO_FILESYSTEM) && !defined(NO_CODING) static const struct option base64_options[] = { {"-in", required_argument, 0, WOLFCLU_INFILE }, {"-out", required_argument, 0, WOLFCLU_OUTFILE }, @@ -44,6 +46,7 @@ static void wolfCLU_Base64Help(void) WOLFCLU_LOG(WOLFCLU_L0, "\t-d Decode data"); WOLFCLU_LOG(WOLFCLU_L0, "\t-help Display this message"); } +#endif /* base64 setup function */ int wolfCLU_Base64Setup(int argc, char** argv) @@ -82,10 +85,8 @@ int wolfCLU_Base64Setup(int argc, char** argv) break; case WOLFCLU_OUTFILE: - bioOut = wolfSSL_BIO_new_file(optarg, "wb"); + bioOut = wolfCLU_OpenOutFileBio(optarg); if (bioOut == NULL) { - wolfCLU_LogError("unable to open output file %s", - optarg); if (bioIn != NULL) { wolfSSL_BIO_free(bioIn); } diff --git a/src/tools/clu_funcs.c b/src/tools/clu_funcs.c index 364bef3f..18413aca 100644 --- a/src/tools/clu_funcs.c +++ b/src/tools/clu_funcs.c @@ -34,6 +34,27 @@ #include #include +/* Platform headers for the file helpers further down. Kept here rather than + * beside those helpers so INT_MAX/PATH_MAX are in scope for the whole file. + * The filesystem-specific ones are guarded to match the helpers themselves: + * a --disable-filesystem build targets platforms where they do not exist. */ +#ifndef WOLFCLU_NO_FILESYSTEM +#ifdef _WIN32 +#include +#include +#include +#include +#else +#include +#include +#include +#endif +#include +#endif /* !WOLFCLU_NO_FILESYSTEM */ +#include +#include +#include + #define SALT_SIZE 8 #define DES3_BLOCK_SIZE 24 @@ -550,6 +571,13 @@ int wolfCLU_getAlgo(int argc, char** argv, int* alg, char** mode, int* size) int option; char name[80]; + /* #3985: guard argv[2] access. argc==2 means argv[2] is the POSIX NULL + * sentinel; XSTRLEN(NULL) would crash before the overflow check below. */ + if (argc < 3 || argv[2] == NULL) { + wolfCLU_LogError("ERROR: missing algorithm argument"); + return USER_INPUT_ERROR; + } + wolfCLU_oldAlgo(argc, argv); XMEMSET(name, 0, sizeof(name)); if (XSTRLEN(argv[2]) >= sizeof(name)) { @@ -693,7 +721,7 @@ void wolfCLU_stats(double start, int blockSize, int64_t blocks) WOLFCLU_LOG(WOLFCLU_L0, "took %6.3f seconds, blocks = %llu", time_total, (unsigned long long)blocks); - bytes = ((blocks * blockSize) / MEGABYTE) / time_total; + bytes = ((double)(blocks * blockSize) / MEGABYTE) / time_total; WOLFCLU_LOG(WOLFCLU_L0, "Average %s/s = %8.1f", unit, bytes); if (blockSize != MEGABYTE) { WOLFCLU_LOG(WOLFCLU_L0, "Block size of this algorithm is: %d.\n", blockSize); @@ -1096,11 +1124,50 @@ void wolfCLU_convertToLower(char* s, int sSz) { int i; for (i = 0; i < sSz; i++) { - s[i] = tolower(s[i]); + s[i] = (char)tolower((unsigned char)s[i]); } } +/* DER definite-length encoder. Returns encoded length byte count. + * + * Duplicates wolfssl/wolfcrypt/asn.h's internal SetLength() rather than + * calling it: that symbol is declared WOLFSSL_ASN_API, which only resolves + * to an exported WOLFSSL_API (as opposed to hidden WOLFSSL_LOCAL) when + * wolfSSL is built with WOLFSSL_TEST_CERT, OPENSSL_EXTRA, + * OPENSSL_EXTRA_X509_SMALL, or WOLFSSL_PUBLIC_ASN. wolfCLU has no control + * over how the wolfSSL it links against was configured, so taking a hard + * dependency on SetLength() would fail to link against a minimal build + * that defines none of those - this local copy keeps DER length encoding + * available regardless. */ +word32 wolfCLU_DerSetLength(word32 length, byte* output) +{ + word32 i; + word32 sz = 1; + + if (length < ASN_LONG_LENGTH) { + if (output != NULL) + output[0] = (byte)length; + } + else { + word32 len = length; + + while (len != 0) { + sz++; + len >>= 8; + } + if (output != NULL) { + output[0] = (byte)(ASN_LONG_LENGTH | (sz - 1)); + for (i = 1; i < sz; i++) { + output[sz - i] = (byte)(length & 0xFF); + length >>= 8; + } + } + } + + return sz; +} + void wolfCLU_ForceZero(void* mem, unsigned int len) { #ifndef WOLFSSL_NO_FORCE_ZERO @@ -1112,6 +1179,1373 @@ void wolfCLU_ForceZero(void* mem, unsigned int len) #endif } +/* Everything from here to the matching #endif needs a stdio filesystem. These + * helpers work in terms of FILE* and POSIX/Win32 file descriptors rather than + * wolfSSL's XFILE/XFOPEN porting macros, because the permission and symlink + * guarantees they exist to provide have no equivalent in that abstraction. */ +#ifndef WOLFCLU_NO_FILESYSTEM + +static int wolfCLU_ReadFileToBufferEx(const char* path, long maxSz, + byte** outBuf, int* outSz, int allowEmpty) +{ + int sz; + long fsz; + byte* buf = NULL; + XFILE f; + + if (path == NULL || outBuf == NULL || outSz == NULL || maxSz <= 0) { + return BAD_FUNC_ARG; + } + *outBuf = NULL; + *outSz = 0; + + /* Reads follow symlinks like any other tool. Refusing them here would + * reject the layouts keys normally live in (certbot's live/ symlinks, + * /etc/ssl/private farms, /dev/stdin, process substitution) while + * buying nothing: anyone able to plant a symlink at the key's path can + * equally replace the key file itself. The no-symlink rule is enforced + * where it does defend something -- the write paths. */ + f = XFOPEN(path, "rb"); + if (f == XBADFILE) { + /* A file that will not open is a runtime failure, not a caller + * mistake; BAD_FUNC_ARG is reserved for the argument check above. */ + wolfCLU_LogError("unable to open file %s", path); + return WOLFCLU_FATAL_ERROR; + } + + if (XFSEEK(f, 0, XSEEK_END) != 0) { + XFCLOSE(f); + return WOLFCLU_FATAL_ERROR; + } + fsz = XFTELL(f); + if (XFSEEK(f, 0, XSEEK_SET) != 0) { + XFCLOSE(f); + return WOLFCLU_FATAL_ERROR; + } + if (fsz < 0 || (fsz == 0 && !allowEmpty)) { + wolfCLU_LogError("%s: file is empty or unreadable", path); + XFCLOSE(f); + return WOLFCLU_FATAL_ERROR; + } + if (fsz > maxSz || fsz > (long)INT_MAX) { + wolfCLU_LogError("%s: size %ld exceeds %ld-byte file limit", + path, fsz, maxSz); + XFCLOSE(f); + return WOLFCLU_FATAL_ERROR; + } + sz = (int)fsz; + + /* +1/NUL-terminate: matches other PEM-buffer readers in this codebase. */ + buf = (byte*)XMALLOC((size_t)sz + 1, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + if (buf == NULL) { + XFCLOSE(f); + return MEMORY_E; + } + + /* short/long read here catches a file that changed size after XFTELL. */ + if (XFREAD(buf, 1, (size_t)sz, f) != (size_t)sz) { + XFCLOSE(f); + wolfCLU_ForceZero(buf, sz); + XFREE(buf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + return WOLFCLU_FATAL_ERROR; + } + buf[sz] = '\0'; + XFCLOSE(f); + + *outBuf = buf; + *outSz = sz; + return WOLFCLU_SUCCESS; +} + +int wolfCLU_ReadFileToBuffer(const char* path, long maxSz, byte** outBuf, + int* outSz) +{ + return wolfCLU_ReadFileToBufferEx(path, maxSz, outBuf, outSz, 0); +} + +/* Same as wolfCLU_ReadFileToBuffer(), but accepts a zero-length file: the + * message/digest being verified is legitimately empty for some algorithms + * (e.g. Ed25519 RFC 8032 test vector 1), unlike a key or signature file, + * which never is. */ +int wolfCLU_ReadMessageFileToBuffer(const char* path, long maxSz, + byte** outBuf, int* outSz) +{ + return wolfCLU_ReadFileToBufferEx(path, maxSz, outBuf, outSz, 1); +} + +/* Open path for writing. When ownerOnly is set the target is kept as an + * owner-only regular file and symlinks/reparse points are refused; otherwise + * this matches fopen(path, mode). */ +#ifdef _WIN32 +#ifndef ELOOP + #define ELOOP 41 +#endif +#ifndef EMLINK + #define EMLINK 31 +#endif +#pragma comment(lib, "advapi32.lib") + +/* Owner-only DACL: no inheritance, full access for the object owner alone. */ +#define WOLFCLU_OWNER_ONLY_SDDL "D:P(A;;FA;;;OW)" + +/* Translate a stdio mode string into the CreateFileA()/CRT arguments fopen() + * would use for it, so both platforms honour mode identically. wantTrunc is + * reported separately rather than folded into the disposition: key files are + * truncated only after the ownership checks below have passed, so that a + * refused target is left untouched. Returns 0 on success, -1 for a mode + * string fopen() would not accept. */ +static int wolfCLU_ModeToWin32(const char* mode, DWORD* accessOut, + DWORD* dispOut, int* crtFlagsOut, int* wantTruncOut) +{ + int update; + + if (mode == NULL || mode[0] == '\0') { + return -1; + } + /* 'b' and friends may appear in any order; only '+' changes direction. */ + update = (XSTRSTR(mode, "+") != NULL); + *accessOut = update ? (GENERIC_READ | GENERIC_WRITE) : 0; + *wantTruncOut = 0; + + switch (mode[0]) { + case 'r': + if (!update) *accessOut = GENERIC_READ; + *dispOut = OPEN_EXISTING; + *crtFlagsOut = update ? _O_RDWR : _O_RDONLY; + break; + case 'w': + if (!update) *accessOut = GENERIC_WRITE; + *dispOut = OPEN_ALWAYS; + *crtFlagsOut = (update ? _O_RDWR : _O_WRONLY) | _O_CREAT | + _O_TRUNC; + *wantTruncOut = 1; + break; + case 'a': + if (!update) *accessOut = GENERIC_WRITE; + *dispOut = OPEN_ALWAYS; + *crtFlagsOut = (update ? _O_RDWR : _O_WRONLY) | _O_CREAT | + _O_APPEND; + break; + default: + return -1; + } + return 0; +} + +/* Read the SID out of the process token for cls (TokenUser or TokenOwner). + * Caller LocalFree()s *bufOut, which owns the storage *sidOut points into. + * return 0 on success, -1 otherwise */ +static int wolfCLU_GetTokenSid(HANDLE hToken, TOKEN_INFORMATION_CLASS cls, + void** bufOut, PSID* sidOut) +{ + DWORD len = 0; + void* buf; + + *bufOut = NULL; + *sidOut = NULL; + + /* Sizing call; always fails. */ + (void)GetTokenInformation(hToken, cls, NULL, 0, &len); + if (len == 0) { + return -1; + } + buf = LocalAlloc(LPTR, len); + if (buf == NULL) { + return -1; + } + if (!GetTokenInformation(hToken, cls, buf, len, &len)) { + LocalFree(buf); + return -1; + } + *bufOut = buf; + *sidOut = (cls == TokenUser) ? ((TOKEN_USER*)buf)->User.Sid + : ((TOKEN_OWNER*)buf)->Owner; + return 0; +} + +/* Counterpart of the POSIX st_uid check. The owner-only DACL grants full + * access to the object *owner*, so applying it to a foreign-owned file would + * hand that user the key instead of locking them out. + * return 0 when we own it, -1 otherwise */ +static int wolfCLU_HandleOwnedBySelf(HANDLE hFile) +{ + PSECURITY_DESCRIPTOR pSD = NULL; + PSID pOwner = NULL; + HANDLE hToken = NULL; + void* buf; + PSID sid; + int ret = -1; + + if (GetSecurityInfo(hFile, SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION, + &pOwner, NULL, NULL, NULL, &pSD) != ERROR_SUCCESS) { + return -1; + } + if (pOwner == NULL || !IsValidSid(pOwner)) { + LocalFree(pSD); + return -1; + } + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken)) { + LocalFree(pSD); + return -1; + } + if (wolfCLU_GetTokenSid(hToken, TokenUser, &buf, &sid) == 0) { + if (EqualSid(pOwner, sid)) { + ret = 0; + } + LocalFree(buf); + } + /* TokenOwner too: an elevated process creates files owned by the + * Administrators group, and those are still ours. */ + if (ret != 0 && + wolfCLU_GetTokenSid(hToken, TokenOwner, &buf, &sid) == 0) { + if (EqualSid(pOwner, sid)) { + ret = 0; + } + LocalFree(buf); + } + CloseHandle(hToken); + LocalFree(pSD); + return ret; +} + +/* Validate a freshly opened key file handle and lock it down: refuse a + * reparse point, a multiply linked file or one we do not own, apply the + * owner-only DACL CreateFileA() only sets on files it creates, then truncate + * if asked. A file + * this call created is removed again when the checks refuse it. hFile is + * closed on failure. created says whether CreateFileA() made the file. + * return 0 on success, -1 with errno set otherwise */ +static int wolfCLU_FinishKeyHandle(HANDLE hFile, const char* path, + PSECURITY_DESCRIPTOR pSD, int created, int wantTrunc) +{ + BY_HANDLE_FILE_INFORMATION bhfi; + + if (!GetFileInformationByHandle(hFile, &bhfi)) { + CloseHandle(hFile); + errno = EACCES; + return -1; + } + if ((bhfi.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { + /* Reparse point here means path was swapped for a symlink/junction + * between GetFileAttributesA() and CreateFileA(). */ + CloseHandle(hFile); + if (created) { + (void)_unlink(path); + } + errno = ELOOP; + return -1; + } + if (bhfi.nNumberOfLinks > 1) { + /* A second hard link would keep the old contents readable, and the + * DACL below would be shared with whoever owns that link. */ + CloseHandle(hFile); + if (created) { + (void)_unlink(path); + } + errno = EMLINK; + return -1; + } + if (wolfCLU_HandleOwnedBySelf(hFile) != 0) { + /* Matches the POSIX st_uid refusal; a pre-existing path may belong + * to someone else. */ + CloseHandle(hFile); + if (created) { + (void)_unlink(path); + } + errno = EPERM; + return -1; + } + + /* The SECURITY_ATTRIBUTES passed to CreateFileA() only apply their DACL + * to a file it actually created; a pre-existing file keeps its old, + * possibly permissive, ACL. */ + if (!created && + !SetKernelObjectSecurity(hFile, DACL_SECURITY_INFORMATION, pSD)) { + CloseHandle(hFile); + errno = EPERM; + return -1; + } + + if (wantTrunc) { + if (SetFilePointer(hFile, 0, NULL, FILE_BEGIN) == + INVALID_SET_FILE_POINTER || + !SetEndOfFile(hFile)) { + CloseHandle(hFile); + if (created) { + (void)_unlink(path); + } + errno = EACCES; + return -1; + } + } + return 0; +} + +FILE* wolfCLU_CreateSecureFile(const char* path, const char* mode, + int ownerOnly) +{ + SECURITY_ATTRIBUTES sa; + SECURITY_ATTRIBUTES* pSA = NULL; + PSECURITY_DESCRIPTOR pSD = NULL; + HANDLE hFile; + DWORD existing; + DWORD access; + DWORD disp; + int fd; + int crtFlags; + int wantTrunc; + int created = 0; + FILE* f = NULL; + + if (path == NULL || wolfCLU_ModeToWin32(mode, &access, &disp, &crtFlags, + &wantTrunc) != 0) { + errno = EINVAL; + return NULL; + } + + if (!ownerOnly) { + /* Nothing secret is being written, so behave exactly like + * fopen(path, mode): reuse whatever the path already names rather + * than requiring a brand new file. CON, NUL and redirected handles + * are all legitimate -out targets. */ + return fopen(path, mode); + } + + /* Key material: refuse rather than clobber a reparse point, and never + * destroy what the path already names. An existing regular file is + * truncated in place after the checks below, so a refused or failed open + * leaves the previous key intact. */ + existing = GetFileAttributesA(path); + if (existing != INVALID_FILE_ATTRIBUTES) { + if ((existing & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { + errno = ELOOP; + return NULL; + } + if ((existing & FILE_ATTRIBUTE_DIRECTORY) != 0) { + errno = EEXIST; + return NULL; + } + } + + if (!ConvertStringSecurityDescriptorToSecurityDescriptorA( + WOLFCLU_OWNER_ONLY_SDDL, SDDL_REVISION_1, &pSD, NULL)) { + errno = EACCES; + return NULL; + } + sa.nLength = sizeof(SECURITY_ATTRIBUTES); + sa.bInheritHandle = FALSE; + sa.lpSecurityDescriptor = pSD; + pSA = &sa; + + /* READ_CONTROL/WRITE_DAC for the owner and DACL work below. */ + SetLastError(0); + hFile = CreateFileA(path, access | READ_CONTROL | WRITE_DAC, 0, pSA, disp, + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OPEN_REPARSE_POINT, NULL); + if (hFile == INVALID_HANDLE_VALUE) { + /* Callers pick their error message off errno, so give them one that + * reflects this failure rather than whatever a previous CRT call + * happened to leave behind. */ + DWORD err = GetLastError(); + errno = (err == ERROR_FILE_NOT_FOUND || err == ERROR_PATH_NOT_FOUND) + ? ENOENT : EACCES; + } + else if (disp == OPEN_EXISTING) { + /* OPEN_EXISTING never creates a file - CreateFileA() only succeeds + * here because the file already existed, so GetLastError() carries + * no ERROR_ALREADY_EXISTS signal to read (it may be stale from an + * unrelated earlier call). Trust the disposition, not the error. */ + created = 0; + } + else { + /* The pre-open GetFileAttributesA() snapshot is racy: another + * process can create the file between that check and this call. + * CreateFileA() itself is authoritative here - for CREATE_ALWAYS/ + * OPEN_ALWAYS it sets last-error to ERROR_ALREADY_EXISTS on success + * iff the file already existed, even though existed/disp above may + * disagree. */ + created = (GetLastError() != ERROR_ALREADY_EXISTS); + } + + if (hFile != INVALID_HANDLE_VALUE && + wolfCLU_FinishKeyHandle(hFile, path, pSD, created, wantTrunc) + != 0) { + hFile = INVALID_HANDLE_VALUE; + } + + if (hFile != INVALID_HANDLE_VALUE) { + fd = _open_osfhandle((intptr_t)hFile, crtFlags); + if (fd != -1) { + f = _fdopen(fd, mode); + } + if (f == NULL) { + if (fd != -1) _close(fd); + else CloseHandle(hFile); + if (created) { + (void)_unlink(path); + } + errno = EACCES; + } + } + if (pSD != NULL) { + LocalFree(pSD); + } + return f; +} + +/* No-follow open for in-place updates. */ +FILE* wolfCLU_OpenExistingSecureFile(const char* path, const char* mode, + int ownerOnly) +{ + HANDLE hFile; + int fd = -1; + FILE* f = NULL; + DWORD access; + DWORD disp; + int crtFlags; + int wantTrunc; + DWORD attrs; + DWORD err; + + if (path == NULL || wolfCLU_ModeToWin32(mode, &access, &disp, &crtFlags, + &wantTrunc) != 0) { + errno = EINVAL; + return NULL; + } + + attrs = GetFileAttributesA(path); + if (attrs == INVALID_FILE_ATTRIBUTES) { + err = GetLastError(); + errno = (err == ERROR_FILE_NOT_FOUND || err == ERROR_PATH_NOT_FOUND) + ? ENOENT : EIO; + return NULL; + } + if (attrs & FILE_ATTRIBUTE_REPARSE_POINT) { + errno = ELOOP; + return NULL; + } + + hFile = CreateFileA(path, + access | (ownerOnly ? (READ_CONTROL | WRITE_DAC) : 0), 0, NULL, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OPEN_REPARSE_POINT, NULL); + if (hFile == INVALID_HANDLE_VALUE) { + err = GetLastError(); + errno = (err == ERROR_FILE_NOT_FOUND || err == ERROR_PATH_NOT_FOUND) + ? ENOENT : EIO; + return NULL; + } + + /* Re-check after open: path may have been replaced with a reparse + * point between GetFileAttributesA and CreateFileA. */ + { + BY_HANDLE_FILE_INFORMATION bhfi; + if (!GetFileInformationByHandle(hFile, &bhfi) || + (bhfi.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)) { + CloseHandle(hFile); + errno = ELOOP; + return NULL; + } + /* As at creation: a second hard link sees every update and shares + * the DACL set below. */ + if (ownerOnly && bhfi.nNumberOfLinks > 1) { + CloseHandle(hFile); + errno = EMLINK; + return NULL; + } + } + + if (ownerOnly) { + PSECURITY_DESCRIPTOR pSD = NULL; + BOOL ok; + + if (wolfCLU_HandleOwnedBySelf(hFile) != 0) { + CloseHandle(hFile); + errno = EPERM; + return NULL; + } + if (!ConvertStringSecurityDescriptorToSecurityDescriptorA( + WOLFCLU_OWNER_ONLY_SDDL, SDDL_REVISION_1, &pSD, NULL)) { + CloseHandle(hFile); + errno = EPERM; + return NULL; + } + ok = SetKernelObjectSecurity(hFile, DACL_SECURITY_INFORMATION, pSD); + LocalFree(pSD); + /* Otherwise key material lands behind the file's old ACL. */ + if (!ok) { + CloseHandle(hFile); + errno = EPERM; + return NULL; + } + } + + /* Truncation is deferred to here rather than folded into the disposition + * so the ACL above is applied before the old contents are dropped. */ + if (wantTrunc) { + if (SetFilePointer(hFile, 0, NULL, FILE_BEGIN) == + INVALID_SET_FILE_POINTER || + !SetEndOfFile(hFile)) { + CloseHandle(hFile); + errno = EACCES; + return NULL; + } + } + + fd = _open_osfhandle((intptr_t)hFile, crtFlags); + if (fd == -1) { + CloseHandle(hFile); + return NULL; + } + f = _fdopen(fd, mode); + if (f == NULL) { + _close(fd); + } + return f; +} +#else +#ifndef O_NOFOLLOW + #define O_NOFOLLOW 0 +#endif +/* Creation modes, before umask. Key material is owner-only; everything else + * gets the same default fopen() would have used. */ +#define WOLFCLU_KEY_FILE_MODE (S_IRUSR | S_IWUSR) +#define WOLFCLU_OUT_FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | \ + S_IROTH | S_IWOTH) + +/* Translate a stdio mode string into the open(2) flags fopen() would use for + * it, so both platforms honour mode identically. Returns 0 on success, -1 for + * a mode string fopen() would not accept. */ +static int wolfCLU_ModeToOpenFlags(const char* mode, int* flagsOut) +{ + int update; + + if (mode == NULL || mode[0] == '\0') { + return -1; + } + /* 'b' and friends may appear in any order; only '+' changes direction. */ + update = (XSTRSTR(mode, "+") != NULL); + + switch (mode[0]) { + case 'r': + *flagsOut = update ? O_RDWR : O_RDONLY; + break; + case 'w': + *flagsOut = (update ? O_RDWR : O_WRONLY) | O_CREAT | O_TRUNC; + break; + case 'a': + *flagsOut = (update ? O_RDWR : O_WRONLY) | O_CREAT | O_APPEND; + break; + default: + return -1; + } + return 0; +} + +/* Remove a file this call created, but only while path still names the file + * fd holds: in an attacker-writable directory the path may already have been + * replaced by something we must not delete. Call before closing fd. */ +static void wolfCLU_UnlinkOwnFd(int fd, const char* path) +{ + struct stat fst, lst; + + if (fstat(fd, &fst) == 0 && lstat(path, &lst) == 0 && + fst.st_dev == lst.st_dev && fst.st_ino == lst.st_ino) { + (void)unlink(path); + } +} + +/* Validate and lock down a freshly opened key file descriptor: confirm it is + * still the regular file lstat() saw, owned by us and not multiply linked, + * then truncate and tighten it. A file this call created is removed again + * when the checks refuse it, so a rejected path is left as it was found. + * fd is closed on failure. Takes the pre-open lstat result and whether the + * path already existed. + * return 0 on success, -1 with errno set otherwise */ +/* Shared tail of wolfCLU_CreateSecureFile()'s and + * wolfCLU_OpenExistingSecureFile()'s owner-only opens: confirm fd is still + * the regular file pre (from the pre-open lstat()) saw, then apply the + * owner-only protections (owner check, single-link check, mode tightened to + * WOLFCLU_KEY_FILE_MODE) when ownerOnly is set, and truncate last if asked. + * A file this call created (existed == 0) is removed again on any refusal, + * so a rejected path is left as it was found. fd is closed on failure. + * return 0 on success, -1 with errno set otherwise */ +static int wolfCLU_FinishKeyFd(int fd, const char* path, + const struct stat* pre, int existed, int wantTrunc, int ownerOnly) +{ + struct stat st; + + /* O_NOFOLLOW rejects a symlink swapped in after the lstat, but the path + * could still have been replaced by a regular file owned by someone + * else; check what was actually opened. */ + if (fstat(fd, &st) != 0 || !S_ISREG(st.st_mode)) { + if (!existed) { + wolfCLU_UnlinkOwnFd(fd, path); + } + close(fd); + errno = EEXIST; + return -1; + } + if (existed && (st.st_dev != pre->st_dev || st.st_ino != pre->st_ino)) { + close(fd); + errno = ELOOP; + return -1; + } + if (ownerOnly && st.st_uid != geteuid()) { + if (!existed) { + wolfCLU_UnlinkOwnFd(fd, path); + } + close(fd); + errno = EPERM; + return -1; + } + if (ownerOnly && st.st_nlink > 1) { + if (!existed) { + wolfCLU_UnlinkOwnFd(fd, path); + } + close(fd); + errno = EMLINK; + return -1; + } + /* Tighten the mode before truncating, so a pre-existing key still holds + * its contents on every path this call goes on to refuse, and is never + * left group/world readable while it holds the new key. */ + if (ownerOnly && + (st.st_mode & (mode_t)~S_IFMT) != (mode_t)WOLFCLU_KEY_FILE_MODE && + fchmod(fd, WOLFCLU_KEY_FILE_MODE) != 0) { + /* Cleanup below makes its own syscalls; the caller reads errno. */ + int err = errno; + if (!existed) { + wolfCLU_UnlinkOwnFd(fd, path); + } + close(fd); + errno = err; + return -1; + } + if (wantTrunc && ftruncate(fd, 0) != 0) { + int err = errno; + if (!existed) { + wolfCLU_UnlinkOwnFd(fd, path); + } + close(fd); + errno = err; + return -1; + } + return 0; +} + +FILE* wolfCLU_CreateSecureFile(const char* path, const char* mode, + int ownerOnly) +{ + int fd; + int flags; + int wantTrunc; + int existed = 0; + FILE* f; + struct stat pre; + + if (path == NULL || wolfCLU_ModeToOpenFlags(mode, &flags) != 0) { + errno = EINVAL; + return NULL; + } + + if (!ownerOnly) { + /* Nothing secret is being written, so behave exactly like + * fopen(path, mode): follow symlinks, and create, truncate or append + * exactly as the mode string asks. Special files (/dev/stdout, + * /dev/null, FIFOs) and symlinks to regular files are all legitimate + * -out targets. */ + fd = open(path, flags, WOLFCLU_OUT_FILE_MODE); + if (fd < 0) { + return NULL; + } + f = fdopen(fd, mode); + if (f == NULL) { + close(fd); + } + return f; + } + + /* Key material: never write through a symlink, and never silently + * destroy whatever the path already names. Anything that is not a regular + * file is refused with a distinguishable errno so the caller can say why. + * + * Truncation is done by an explicit ftruncate() after the checks below + * rather than by O_TRUNC, so a refused target keeps its contents and a + * failed open leaves the previous key untouched. The mode is then forced + * down to owner-only, since O_CREAT only applies WOLFCLU_KEY_FILE_MODE to + * a file it actually creates and a pre-existing file would otherwise keep + * its old, possibly permissive, mode. */ + wantTrunc = (flags & O_TRUNC) != 0; + flags &= ~O_TRUNC; + + if (lstat(path, &pre) == 0) { + if (S_ISLNK(pre.st_mode)) { + errno = ELOOP; + return NULL; + } + if (!S_ISREG(pre.st_mode)) { + errno = EEXIST; + return NULL; + } + existed = 1; + } + else if (errno != ENOENT) { + return NULL; + } + + fd = open(path, flags | O_NOFOLLOW | + (((flags & O_CREAT) != 0 && !existed) ? O_EXCL : 0), + WOLFCLU_KEY_FILE_MODE); + if (fd < 0) { + return NULL; + } + + if (wolfCLU_FinishKeyFd(fd, path, &pre, existed, wantTrunc, 1) != 0) { + return NULL; + } + + f = fdopen(fd, mode); + if (f == NULL) { + int err = errno; + if (!existed) { + /* remove the stray empty file we created */ + wolfCLU_UnlinkOwnFd(fd, path); + } + close(fd); + errno = err; + } + return f; +} + +/* No-follow open for in-place updates. */ +FILE* wolfCLU_OpenExistingSecureFile(const char* path, const char* mode, + int ownerOnly) +{ + int fd; + int flags; + int wantTrunc; + FILE* f; + struct stat pre; + + if (path == NULL || wolfCLU_ModeToOpenFlags(mode, &flags) != 0) { + errno = EINVAL; + return NULL; + } + /* This helper only ever updates a file that is already there, so O_CREAT + * is dropped and a missing path is reported as ENOENT by the lstat. */ + wantTrunc = (flags & O_TRUNC) != 0; + flags &= ~(O_CREAT | O_TRUNC); + + if (lstat(path, &pre) != 0) { + return NULL; /* errno from lstat, including ENOENT */ + } + /* Keep the two refusals distinguishable: a symlink is the attack this + * helper exists to stop, while a directory or FIFO is just the wrong + * kind of target. */ + if (S_ISLNK(pre.st_mode)) { + errno = ELOOP; + return NULL; + } + if (!S_ISREG(pre.st_mode)) { + errno = EEXIST; + return NULL; + } + fd = open(path, flags | O_NOFOLLOW); + if (fd < 0) { + return NULL; + } + /* Shares wolfCLU_CreateSecureFile()'s post-open checks and owner-only + * lockdown, so both entry points enforce the exact same permission + * policy on a key file - e.g. a pre-existing 0700 file is tightened to + * 0600 here too. The file always pre-existed at this point (existed=1), + * so a refusal here never unlinks it. */ + if (wolfCLU_FinishKeyFd(fd, path, &pre, 1, wantTrunc, ownerOnly) != 0) { + return NULL; + } + f = fdopen(fd, mode); + if (f == NULL) { + int err = errno; + close(fd); + errno = err; + } + return f; +} +#endif /* _WIN32 */ + +void wolfCLU_LogKeyOpenFailure(const char* path, int err) +{ + /* Distinguish every deliberate refusal from a plain open failure so the + * user is not left guessing why a writable path was rejected. Shared by + * every key-file open, including wolfCLU_OpenExistingSecureFile() + * callers, which get no logging from the open itself. */ + if (path == NULL) { + /* An option given as the last argv token binds a NULL optarg; never + * hand that to %s. */ + wolfCLU_LogError("No key file name given"); + } + else if (err == ELOOP) { + wolfCLU_LogError("Refusing to write key material through the " + "symlink %s", path); + } + else if (err == EEXIST) { + wolfCLU_LogError("Refusing to write key material to %s: not a " + "regular file", path); + } + else if (err == EPERM) { + wolfCLU_LogError("Refusing to write key material to %s: owned by " + "another user", path); + } + else if (err == EMLINK) { + wolfCLU_LogError("Refusing to write key material to %s: file has " + "more than one hard link", path); + } + else { + wolfCLU_LogError("Unable to open output file %s", path); + } +} + +FILE* wolfCLU_OpenKeyFile(const char* path) +{ + FILE* f; + + errno = 0; + f = wolfCLU_CreateSecureFile(path, "wb", 1); + + if (f == NULL) { + wolfCLU_LogKeyOpenFailure(path, errno); + } + return f; +} + +FILE* wolfCLU_OpenSecureFileForUpdate(const char* path) +{ + FILE* f; + int openErr; + + errno = 0; + f = wolfCLU_OpenExistingSecureFile(path, "rb+", 1); + if (f != NULL) { + return f; + } + + /* Only a missing file earns a second attempt: creating it is the one + * case handled differently here, since callers expect the returned + * handle to stay readable and writable in place, not just wb's + * write-only truncate. "wb+" creates it (empty, so O_TRUNC is a no-op) + * while still opening it O_RDWR, unlike wolfCLU_OpenKeyFile()'s "wb". + * Every other errno is a deliberate refusal, and the open above logs + * nothing, so report it here or the caller fails with no diagnostic at + * all. */ + openErr = errno; + if (openErr == ENOENT) { + errno = 0; + f = wolfCLU_CreateSecureFile(path, "wb+", 1); + if (f == NULL) { + wolfCLU_LogKeyOpenFailure(path, errno); + } + return f; + } + wolfCLU_LogKeyOpenFailure(path, openErr); + return NULL; +} + +int wolfCLU_CloseOutFile(FILE* file, const char* path) +{ + /* The last chunk written can still be sitting in stdio's buffer, so a + * flush failure (e.g. ENOSPC/EIO) is only reported here, at close, even + * though the writes that filled the buffer already reported success. */ + if (file == NULL) { + return WOLFCLU_SUCCESS; + } + if (XFCLOSE(file) != 0) { + wolfCLU_LogError("Failed to write output file %s", path); + return OUTPUT_FILE_ERROR; + } + return WOLFCLU_SUCCESS; +} + +FILE* wolfCLU_OpenOutFile(const char* path) +{ + FILE* f = wolfCLU_CreateSecureFile(path, "wb", 0); + + if (f == NULL) { + /* An option given as the last argv token binds a NULL optarg; never + * hand that to %s. */ + if (path == NULL) { + wolfCLU_LogError("No output file name given"); + } + else { + wolfCLU_LogError("Unable to open output file %s", path); + } + } + return f; +} + +FILE* wolfCLU_OpenOutFileDistinctFrom(const char* path, FILE* inFile) +{ + FILE* f; +#ifndef _WIN32 + int fd; + int flags; + struct stat outSt; + struct stat inSt; + + if (inFile == NULL) { + /* Nothing to alias; the ordinary open already has the right + * semantics (including special files such as /dev/stdout). */ + return wolfCLU_OpenOutFile(path); + } + if (path == NULL || wolfCLU_ModeToOpenFlags("wb", &flags) != 0) { + errno = EINVAL; + wolfCLU_LogError("No output file name given"); + return NULL; + } + + /* Truncation is deferred past the identity check below, so if -out is + * swapped to alias -in after wolfCLU_PathsRefEqual() ran, the input is + * still intact when the alias is caught. O_TRUNC here instead would + * destroy it before anything could be compared. */ + fd = open(path, flags & ~O_TRUNC, WOLFCLU_OUT_FILE_MODE); + if (fd < 0) { + wolfCLU_LogError("Unable to open output file %s", path); + return NULL; + } + + if (fstat(fd, &outSt) != 0 || fstat(fileno(inFile), &inSt) != 0) { + close(fd); + wolfCLU_LogError("Unable to open output file %s", path); + return NULL; + } + if (outSt.st_dev == inSt.st_dev && outSt.st_ino == inSt.st_ino) { + close(fd); + wolfCLU_LogError("-in and -out name the same file %s", path); + return NULL; + } + + /* Only a regular file needs (or tolerates) truncation; a FIFO or + * character device is a legitimate -out and must be left alone. */ + if (S_ISREG(outSt.st_mode) && ftruncate(fd, 0) != 0) { + close(fd); + wolfCLU_LogError("Unable to open output file %s", path); + return NULL; + } + + f = fdopen(fd, "wb"); + if (f == NULL) { + close(fd); + wolfCLU_LogError("Unable to open output file %s", path); + } + return f; +#else + HANDLE hFile; + HANDLE hIn; + DWORD access; + DWORD disp; + int crtFlags; + int wantTrunc; + int fd; + BY_HANDLE_FILE_INFORMATION outInfo; + BY_HANDLE_FILE_INFORMATION inInfo; + + if (inFile == NULL) { + /* Nothing to alias; the ordinary open already has the right + * semantics (including special files such as CON/NUL). */ + return wolfCLU_OpenOutFile(path); + } + if (path == NULL || wolfCLU_ModeToWin32("wb", &access, &disp, &crtFlags, + &wantTrunc) != 0) { + errno = EINVAL; + wolfCLU_LogError("No output file name given"); + return NULL; + } + + /* OPEN_ALWAYS does not truncate on its own (unlike CREATE_ALWAYS), so + * truncation stays deferred past the identity check below, the same + * way the POSIX branch defers it past O_TRUNC: if -out is swapped to + * alias -in after wolfCLU_PathsRefEqual() ran, the input is still + * intact when the alias is caught here. */ + hFile = CreateFileA(path, access, 0, NULL, disp, FILE_ATTRIBUTE_NORMAL, + NULL); + if (hFile == INVALID_HANDLE_VALUE) { + wolfCLU_LogError("Unable to open output file %s", path); + return NULL; + } + + hIn = (HANDLE)_get_osfhandle(_fileno(inFile)); + if (hIn == INVALID_HANDLE_VALUE || + !GetFileInformationByHandle(hFile, &outInfo) || + !GetFileInformationByHandle(hIn, &inInfo)) { + CloseHandle(hFile); + wolfCLU_LogError("Unable to open output file %s", path); + return NULL; + } + if (outInfo.dwVolumeSerialNumber == inInfo.dwVolumeSerialNumber && + outInfo.nFileIndexHigh == inInfo.nFileIndexHigh && + outInfo.nFileIndexLow == inInfo.nFileIndexLow) { + CloseHandle(hFile); + wolfCLU_LogError("-in and -out name the same file %s", path); + return NULL; + } + + /* Only a regular file needs (or tolerates) truncation; a character + * device (CON, NUL) is a legitimate -out and must be left alone. */ + if ((outInfo.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0 && + (SetFilePointer(hFile, 0, NULL, FILE_BEGIN) == + INVALID_SET_FILE_POINTER || + !SetEndOfFile(hFile))) { + CloseHandle(hFile); + wolfCLU_LogError("Unable to open output file %s", path); + return NULL; + } + + fd = _open_osfhandle((intptr_t)hFile, crtFlags); + if (fd != -1) { + f = _fdopen(fd, "wb"); + } + else { + f = NULL; + } + if (f == NULL) { + if (fd != -1) { + _close(fd); + } + else { + CloseHandle(hFile); + } + wolfCLU_LogError("Unable to open output file %s", path); + } + return f; +#endif /* !_WIN32 */ +} + +#ifdef _WIN32 + #define WOLFCLU_PATH_BUF_SZ MAX_PATH + /* Two canonicalized paths to compare. */ + #define WOLFCLU_PATH_WORK_SZ (WOLFCLU_PATH_BUF_SZ * 2) +#else + /* PATH_MAX is optional in POSIX and absent on e.g. GNU/Hurd, where paths + * have no fixed upper bound; fall back to a generous fixed size. The + * fallback is kept in wolfCLU's own namespace rather than defining + * PATH_MAX, which belongs to the implementation. */ + #ifdef PATH_MAX + #define WOLFCLU_PATH_BUF_SZ PATH_MAX + #else + #define WOLFCLU_PATH_BUF_SZ 4096 + #endif + /* Two canonicalized paths plus the three scratch buffers + * wolfCLU_ResolveParentPath() needs. */ + #define WOLFCLU_PATH_WORK_SZ (WOLFCLU_PATH_BUF_SZ * 5) + +/* Rewrite path as "/" into out, borrowing + * three WOLFCLU_PATH_BUF_SZ buffers from scratch. realpath() requires its + * target to exist, but -out/-keyout name files that are typically created + * later in the same call, so only the parent (which does exist) is resolved. + * notExist, if non-NULL, is set to 1 when the failure was specifically + * "the parent directory doesn't exist" (as opposed to e.g. an oversized + * path) -- the two are distinguishable to the caller and mean different + * things: a path whose parent doesn't exist yet cannot already alias + * another file, while other failures are genuinely inconclusive. + * return 1 on success, 0 if the path cannot be canonicalized */ +static int wolfCLU_ResolveParentPath(const char* path, char* out, + word32 outSz, char* scratch, int* notExist) +{ + char* dirBuf = scratch; + char* baseBuf = scratch + WOLFCLU_PATH_BUF_SZ; + char* resolvedDir = scratch + (WOLFCLU_PATH_BUF_SZ * 2); + + if (notExist != NULL) { + *notExist = 0; + } + if (XSTRLEN(path) >= WOLFCLU_PATH_BUF_SZ) { + return 0; + } + /* dirname()/basename() may modify their argument, so each gets a copy. */ + XSTRNCPY(dirBuf, path, WOLFCLU_PATH_BUF_SZ - 1); + dirBuf[WOLFCLU_PATH_BUF_SZ - 1] = '\0'; + XSTRNCPY(baseBuf, path, WOLFCLU_PATH_BUF_SZ - 1); + baseBuf[WOLFCLU_PATH_BUF_SZ - 1] = '\0'; + + /* realpath()'s resolved-path output can be as long as PATH_MAX reports, + * but on platforms that skip the #ifdef PATH_MAX branch above there is + * no such bound: the filesystem may hand back a path longer than the + * fixed WOLFCLU_PATH_BUF_SZ fallback, which would overflow resolvedDir. + * Guard explicitly rather than trusting realpath() to respect the + * buffer size it was never told about. */ + { + /* realpath()'s glibc/BSD "return a malloc()'d buffer" extension + * (POSIX.1-2008) is used here rather than a caller-supplied buffer + * precisely so the buffer is sized to the result: that's what makes + * the length check below meaningful instead of just moving the + * overflow into realpath() itself. This allocation comes from the + * platform's malloc(), not wolfSSL's allocator, so it is freed with + * free(), not XFREE(). */ + char* tmp; + errno = 0; + tmp = realpath(dirname(dirBuf), NULL); + if (tmp == NULL) { + if (notExist != NULL) { + *notExist = (errno == ENOENT); + } + return 0; + } + if (XSTRLEN(tmp) >= WOLFCLU_PATH_BUF_SZ) { + free(tmp); + return 0; + } + XSTRNCPY(resolvedDir, tmp, WOLFCLU_PATH_BUF_SZ - 1); + resolvedDir[WOLFCLU_PATH_BUF_SZ - 1] = '\0'; + free(tmp); + } + if (XSNPRINTF(out, outSz, "%s/%s", resolvedDir, basename(baseBuf)) + >= (int)outSz) { + return 0; + } + return 1; +} + +/* Compare two existing paths by their unique file identity (device + inode) + * rather than by string form. This is the only reliable way to detect that + * -in and -out name the same underlying file when a symlink or hard link is + * involved: two different, non-canonicalizable-to-each-other path strings + * can still refer to the same inode. Sets *haveResult to 1 only when both + * paths could be stat()'d, since a not-yet-created -out cannot be compared + * this way and the caller needs to know to fall back to path comparison. */ +static int wolfCLU_FileIdEqual(const char* pathA, const char* pathB, + int* haveResult) +{ + struct stat stA; + struct stat stB; + + *haveResult = 0; + if (stat(pathA, &stA) != 0 || stat(pathB, &stB) != 0) { + return 0; + } + /* Only a regular file can be destroyed by the truncating open this + * guard protects. Two paths to one character device or FIFO share a + * dev/ino pair but alias nothing that can be lost -- an interactive + * '-in /dev/stdin -out /dev/stdout' resolves both to the same tty, and + * must keep working. wolfCLU_OpenOutFileDistinctFrom() already scopes + * its truncation to S_ISREG for the same reason. */ + if (!S_ISREG(stA.st_mode) || !S_ISREG(stB.st_mode)) { + *haveResult = 1; + return 0; + } + *haveResult = 1; + return (stA.st_dev == stB.st_dev && stA.st_ino == stB.st_ino); +} +#endif /* _WIN32 */ + +#ifdef _WIN32 +/* Windows equivalent of wolfCLU_FileIdEqual(): compares the volume serial + * number and file index, which (unlike the path string) are unaffected by + * symlinks, hard links, or junctions. Only usable when both files already + * exist. */ +static int wolfCLU_FileIdEqual(const char* pathA, const char* pathB, + int* haveResult) +{ + HANDLE hA; + HANDLE hB; + BY_HANDLE_FILE_INFORMATION infoA; + BY_HANDLE_FILE_INFORMATION infoB; + int ret = 0; + + *haveResult = 0; + + hA = CreateFileA(pathA, 0, FILE_SHARE_READ | FILE_SHARE_WRITE | + FILE_SHARE_DELETE, NULL, OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, NULL); + if (hA == INVALID_HANDLE_VALUE) { + return 0; + } + hB = CreateFileA(pathB, 0, FILE_SHARE_READ | FILE_SHARE_WRITE | + FILE_SHARE_DELETE, NULL, OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, NULL); + if (hB == INVALID_HANDLE_VALUE) { + CloseHandle(hA); + return 0; + } + + if (GetFileInformationByHandle(hA, &infoA) && + GetFileInformationByHandle(hB, &infoB)) { + *haveResult = 1; + ret = (infoA.dwVolumeSerialNumber == infoB.dwVolumeSerialNumber && + infoA.nFileIndexHigh == infoB.nFileIndexHigh && + infoA.nFileIndexLow == infoB.nFileIndexLow); + } + + CloseHandle(hA); + CloseHandle(hB); + return ret; +} +#endif /* _WIN32 */ + +/* return WOLFCLU_PATHS_SAME when both paths provably name the same file, + * WOLFCLU_PATHS_DISTINCT when they are provably distinct, and + * WOLFCLU_PATHS_UNDETERMINED when the comparison was inconclusive + * (allocation failure, unresolvable path, etc.). This guards an + * overwrite-in-place check, so an undetermined result still fails closed - + * the caller must treat it as a possible match - but stays distinguishable + * from a definite match so the caller can report the real cause. */ +int wolfCLU_PathsRefEqual(const char* pathA, const char* pathB) +{ + char* work; + char* fullA; + char* fullB; + int ret; + int haveIdResult; + + if (pathA == NULL || pathB == NULL) { + return WOLFCLU_PATHS_DISTINCT; + } + if (XSTRCMP(pathA, pathB) == 0) { + return WOLFCLU_PATHS_SAME; + } + + /* Prefer comparing by file identity (inode/dev on POSIX, volume serial + * + file index on Windows): unlike any string comparison, it correctly + * catches symlink and hard-link aliases pointing at the same file. This + * only works when both paths already exist, e.g. it can't help when + * -out will be created fresh by this run. */ + ret = wolfCLU_FileIdEqual(pathA, pathB, &haveIdResult); + if (haveIdResult) { + return ret ? WOLFCLU_PATHS_SAME : WOLFCLU_PATHS_DISTINCT; + } + + /* PATH_MAX buffers are far past the stack budget for one function, so + * the whole working set comes from a single allocation. */ + work = (char*)XMALLOC(WOLFCLU_PATH_WORK_SZ, HEAP_HINT, + DYNAMIC_TYPE_TMP_BUFFER); + if (work == NULL) { + /* Can't canonicalize without scratch space; fail closed, but let + * the caller know this wasn't a definite match. */ + return WOLFCLU_PATHS_UNDETERMINED; + } + fullA = work; + fullB = work + WOLFCLU_PATH_BUF_SZ; + + /* Canonicalize to catch path aliases. */ +#ifdef _WIN32 + { + DWORD retA = GetFullPathNameA(pathA, WOLFCLU_PATH_BUF_SZ, fullA, NULL); + DWORD retB = GetFullPathNameA(pathB, WOLFCLU_PATH_BUF_SZ, fullB, NULL); + if (retA == 0 || retA >= WOLFCLU_PATH_BUF_SZ || + retB == 0 || retB >= WOLFCLU_PATH_BUF_SZ) { + /* Couldn't resolve one side; fail closed rather than assume + * the paths are distinct, but flag it as undetermined rather + * than a definite match. */ + ret = WOLFCLU_PATHS_UNDETERMINED; + } + else { + ret = (_stricmp(fullA, fullB) == 0) ? WOLFCLU_PATHS_SAME : + WOLFCLU_PATHS_DISTINCT; + } + } +#else + { + char* scratch = work + (WOLFCLU_PATH_BUF_SZ * 2); + int notExistA = 0; + int notExistB = 0; + int okA = wolfCLU_ResolveParentPath(pathA, fullA, WOLFCLU_PATH_BUF_SZ, + scratch, ¬ExistA); + int okB = wolfCLU_ResolveParentPath(pathB, fullB, WOLFCLU_PATH_BUF_SZ, + scratch, ¬ExistB); + + if (okA && okB) { + ret = (XSTRCMP(fullA, fullB) == 0) ? WOLFCLU_PATHS_SAME : + WOLFCLU_PATHS_DISTINCT; + } + else if ((okA || notExistA) && (okB || notExistB)) { + /* Every unresolved side failed only because its parent + * directory doesn't exist yet (typical of a fresh -out): such + * a path can't already exist, so it can't alias the other, + * already string-distinct path. The eventual open of that + * path will fail with the real, more useful diagnostic. */ + ret = WOLFCLU_PATHS_DISTINCT; + } + else { + /* At least one side failed for a different reason (oversized + * path, allocation failure); fail closed rather than assume + * the paths are distinct. */ + ret = WOLFCLU_PATHS_UNDETERMINED; + } + } +#endif + + XFREE(work, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + return ret; +} + +int wolfCLU_RejectSamePath(const char* pathA, const char* pathB) +{ + int pathCmp = wolfCLU_PathsRefEqual(pathA, pathB); + + if (pathCmp == WOLFCLU_PATHS_SAME) { + wolfCLU_LogError("-in and -out name the same file %s", pathB); + return WOLFCLU_FATAL_ERROR; + } + if (pathCmp == WOLFCLU_PATHS_UNDETERMINED) { + wolfCLU_LogError("Unable to determine if -in and -out name " + "different files"); + return WOLFCLU_FATAL_ERROR; + } + return WOLFCLU_SUCCESS; +} + +FILE* wolfCLU_OpenPairedOutFile(const char* in, const char* out, + FILE* inFile) +{ + /* wolfCLU_RejectSamePath() gives a friendly refusal, by path string + * alone, before any work is done on the input. wolfCLU_OpenOutFile + * DistinctFrom() repeats the check at the fd level right before -out + * truncates, closing the TOCTOU window between that early check and + * this open (e.g. -out replaced with a symlink to -in in between) - + * it is the one both callers must not skip. Keeping both, rather than + * relying on the fd-level check alone, is a deliberate trade of one + * extra stat() per paired -in/-out invocation for a clearer error + * message ("-in and -out name the same file" up front instead of only + * after -in has already been opened). */ + if (wolfCLU_RejectSamePath(in, out) != WOLFCLU_SUCCESS) { + return NULL; + } + return wolfCLU_OpenOutFileDistinctFrom(out, inFile); +} + +static WOLFSSL_BIO* wolfCLU_WrapSecureFileBio(FILE* f, const char* path) +{ + WOLFSSL_BIO* bioOut = (f != NULL) ? + wolfSSL_BIO_new_fp(f, BIO_CLOSE) : NULL; + + if (bioOut == NULL && f != NULL) { + /* wolfCLU_OpenKeyFile()/wolfCLU_OpenOutFile() already logged when + * f itself was NULL; only log here for the BIO-wrap failure. The + * path is deliberately left alone: it may name a pre-existing file + * or special file the user asked to write to, and removing it would + * destroy more than the empty file we would have created. */ + XFCLOSE(f); + wolfCLU_LogError("Unable to open output file %s", path); + } + return bioOut; +} + +WOLFSSL_BIO* wolfCLU_OpenKeyFileBio(const char* path) +{ + return wolfCLU_WrapSecureFileBio(wolfCLU_OpenKeyFile(path), path); +} + +WOLFSSL_BIO* wolfCLU_OpenOutFileBio(const char* path) +{ + return wolfCLU_WrapSecureFileBio(wolfCLU_OpenOutFile(path), path); +} + +WOLFSSL_BIO* wolfCLU_OpenOutOrKeyFileBio(const char* path, + WOLFCLU_OUT_KIND kind) +{ + return (kind == WOLFCLU_OUT_SECRET) ? wolfCLU_OpenKeyFileBio(path) : + wolfCLU_OpenOutFileBio(path); +} + +#endif /* !WOLFCLU_NO_FILESYSTEM */ + #ifndef WOLFCLU_NO_TERM_SUPPORT int wolfCLU_GetPassword(char* password, int* passwordSz, char* arg) @@ -1313,18 +2747,64 @@ int wolfCLU_GetOpt(int argc, char** argv, const char *options, } +/* Stream bioIn in chunks to update(). */ +static int wolfCLU_bioReadUpdate(WOLFSSL_BIO* bioIn, + int (*update)(void* updateCtx, const byte* data, word32 sz), + void* updateCtx) +{ + byte chunk[MAX_IO_CHUNK_SZ]; + int bytesRead; + int ret = WOLFCLU_SUCCESS; + + while (ret == WOLFCLU_SUCCESS) { + bytesRead = wolfSSL_BIO_read(bioIn, chunk, sizeof(chunk)); + if (bytesRead < 0) { + wolfCLU_LogError("Error reading data"); + ret = WOLFCLU_FATAL_ERROR; + break; + } + else if (bytesRead == 0) { + break; + } + if (update(updateCtx, chunk, (word32)bytesRead) != 0) { + wolfCLU_LogError("Hash update failed"); + ret = WOLFCLU_FATAL_ERROR; + } + } + + wolfCLU_ForceZero(chunk, sizeof(chunk)); + return ret; +} + +struct wolfCLU_hashUpdateCtx { + wc_HashAlg* hashAlg; + enum wc_HashType hashType; +}; + +static int wolfCLU_hashUpdateCb(void* updateCtx, const byte* data, word32 sz) +{ + struct wolfCLU_hashUpdateCtx* ctx = + (struct wolfCLU_hashUpdateCtx*)updateCtx; + return wc_HashUpdate(ctx->hashAlg, ctx->hashType, data, sz); +} + +static int wolfCLU_hmacUpdateCb(void* updateCtx, const byte* data, word32 sz) +{ + return (wolfSSL_HMAC_Update((WOLFSSL_HMAC_CTX*)updateCtx, data, sz) + == WOLFSSL_SUCCESS) ? 0 : WOLFCLU_FATAL_ERROR; +} + /* Stream-hash data read from bioIn using hashType and write the digest to * outDigest. On entry *outDigestSz is the capacity of outDigest; on success * it is updated to the actual digest length. */ int wolfCLU_streamHashBio(WOLFSSL_BIO* bioIn, enum wc_HashType hashType, byte* outDigest, word32* outDigestSz) { - byte chunk[MAX_IO_CHUNK_SZ]; wc_HashAlg hashAlg; + struct wolfCLU_hashUpdateCtx updateCtx; int hashInit = 0; - int bytesRead; int dsz; - int ret = WOLFCLU_SUCCESS; + int ret; if (bioIn == NULL || outDigest == NULL || outDigestSz == NULL) { return BAD_FUNC_ARG; @@ -1342,21 +2822,9 @@ int wolfCLU_streamHashBio(WOLFSSL_BIO* bioIn, enum wc_HashType hashType, } hashInit = 1; - while (ret == WOLFCLU_SUCCESS) { - bytesRead = wolfSSL_BIO_read(bioIn, chunk, sizeof(chunk)); - if (bytesRead < 0) { - wolfCLU_LogError("Error reading data"); - ret = WOLFCLU_FATAL_ERROR; - break; - } - else if (bytesRead == 0) { - break; - } - if (wc_HashUpdate(&hashAlg, hashType, chunk, (word32)bytesRead) != 0) { - wolfCLU_LogError("Hash update failed"); - ret = WOLFCLU_FATAL_ERROR; - } - } + updateCtx.hashAlg = &hashAlg; + updateCtx.hashType = hashType; + ret = wolfCLU_bioReadUpdate(bioIn, wolfCLU_hashUpdateCb, &updateCtx); if (ret == WOLFCLU_SUCCESS) { if (wc_HashFinal(&hashAlg, hashType, outDigest) != 0) { @@ -1379,7 +2847,7 @@ int wolfCLU_hmacHash(WOLFSSL_HMAC_CTX *ctx, void* key, word32 len, enum wc_HashType alg, WOLFSSL_BIO* in, byte* out, word32* outSz) { int ret = WOLFCLU_SUCCESS; - byte chunk[MAX_IO_CHUNK_SZ]; + byte digest[WC_MAX_DIGEST_SIZE]; word32 hmacLen = 0; const WOLFSSL_EVP_MD* md = NULL; @@ -1392,7 +2860,12 @@ int wolfCLU_hmacHash(WOLFSSL_HMAC_CTX *ctx, void* key, word32 len, * Cast to int so unrelated hash types don't trip -Wswitch-enum. */ switch ((int)alg) { case WC_HASH_TYPE_MD5: + #ifndef NO_MD5 md = wolfSSL_EVP_md5(); + #else + wolfCLU_LogError("MD5 not compiled in"); + ret = WOLFCLU_FATAL_ERROR; + #endif break; case WC_HASH_TYPE_SHA: md = wolfSSL_EVP_sha1(); @@ -1422,28 +2895,11 @@ int wolfCLU_hmacHash(WOLFSSL_HMAC_CTX *ctx, void* key, word32 len, } if (ret == WOLFCLU_SUCCESS) { - int bytesRead = 0; - while (ret == WOLFCLU_SUCCESS) { - bytesRead = wolfSSL_BIO_read(in, chunk, sizeof(chunk)); - if (bytesRead < 0) { - wolfCLU_LogError("Error reading data"); - ret = WOLFCLU_FATAL_ERROR; - break; - } - else if (bytesRead == 0) { - break; - } - if (wolfSSL_HMAC_Update(ctx, chunk, (word32)bytesRead) - != WOLFSSL_SUCCESS) { - wolfCLU_LogError("Hash update failed"); - ret = WOLFCLU_FATAL_ERROR; - } - } - wolfCLU_ForceZero(chunk, sizeof(chunk)); + ret = wolfCLU_bioReadUpdate(in, wolfCLU_hmacUpdateCb, ctx); } if (ret == WOLFCLU_SUCCESS) { - if (wolfSSL_HMAC_Final(ctx, chunk, &hmacLen) != WOLFSSL_SUCCESS) { + if (wolfSSL_HMAC_Final(ctx, digest, &hmacLen) != WOLFSSL_SUCCESS) { wolfCLU_LogError("Unable to get hmac hash of data."); ret = WOLFCLU_FATAL_ERROR; } @@ -1451,7 +2907,7 @@ int wolfCLU_hmacHash(WOLFSSL_HMAC_CTX *ctx, void* key, word32 len, if (ret == WOLFCLU_SUCCESS) { if (hmacLen <= *outSz) { - XMEMCPY(out, chunk, hmacLen); + XMEMCPY(out, digest, hmacLen); *outSz = hmacLen; } else { @@ -1460,6 +2916,6 @@ int wolfCLU_hmacHash(WOLFSSL_HMAC_CTX *ctx, void* key, word32 len, } } - wolfCLU_ForceZero(chunk, sizeof(chunk)); + wolfCLU_ForceZero(digest, sizeof(digest)); return ret; } diff --git a/src/tools/clu_rand.c b/src/tools/clu_rand.c index d203e590..8c5c15bf 100644 --- a/src/tools/clu_rand.c +++ b/src/tools/clu_rand.c @@ -316,9 +316,8 @@ int wolfCLU_Rand(int argc, char** argv) /* Open output ("wb") only after the count validates, so a bad/missing * count never truncates an existing file. */ if (ret == WOLFCLU_SUCCESS && outFile != NULL) { - bioOut = wolfSSL_BIO_new_file(outFile, "wb"); + bioOut = wolfCLU_OpenOutFileBio(outFile); if (bioOut == NULL) { - wolfCLU_LogError("Unable to open output file %s", outFile); ret = WOLFCLU_FATAL_ERROR; } } diff --git a/src/x509/clu_ca_setup.c b/src/x509/clu_ca_setup.c index 566cc3ae..543e8f51 100644 --- a/src/x509/clu_ca_setup.c +++ b/src/x509/clu_ca_setup.c @@ -78,6 +78,21 @@ static void wolfCLU_CAHelp(void) } #endif +#ifndef WOLFCLU_NO_FILESYSTEM +/* Open a key file for reading and wrap it in a BIO, so the four key + * options below share one open and one error message. Returns NULL and + * logs on failure. */ +static WOLFSSL_BIO* wolfCLU_CAOpenKeyBio(const char* path, const char* what) +{ + WOLFSSL_BIO* bio = wolfSSL_BIO_new_file(path, "rb"); + + if (bio == NULL) { + wolfCLU_LogError("Unable to open %s file %s", what, path); + } + return bio; +} +#endif /* !WOLFCLU_NO_FILESYSTEM */ + /* return WOLFCLU_SUCCESS on success */ int wolfCLU_CASetup(int argc, char** argv) { @@ -126,37 +141,30 @@ int wolfCLU_CASetup(int argc, char** argv) break; case WOLFCLU_KEY: - keyIn = wolfSSL_BIO_new_file(optarg, "rb"); + keyIn = wolfCLU_CAOpenKeyBio(optarg, "private key"); if (keyIn == NULL) { - wolfCLU_LogError("Unable to open private key file %s", - optarg); ret = WOLFCLU_FATAL_ERROR; } break; #if defined(WOLFSSL_DUAL_ALG_CERTS) && defined(HAVE_DILITHIUM) case WOLFCLU_SUBJKEY: - subjKey = wolfSSL_BIO_new_file(optarg, "rb"); + subjKey = wolfCLU_CAOpenKeyBio(optarg, "subject key"); if (subjKey == NULL) { - wolfCLU_LogError("Unable to open subject key file %s", - optarg); ret = WOLFCLU_FATAL_ERROR; } break; case WOLFCLU_ALTKEY: - altKey = wolfSSL_BIO_new_file(optarg, "rb"); + altKey = wolfCLU_CAOpenKeyBio(optarg, "alternate key"); if (altKey == NULL) { - wolfCLU_LogError("Unable to open alternate key file %s", - optarg); ret = WOLFCLU_FATAL_ERROR; } break; case WOLFCLU_ALTPUB: - altKeyPub = wolfSSL_BIO_new_file(optarg, "rb"); + altKeyPub = wolfCLU_CAOpenKeyBio(optarg, + "alternate public key"); if (altKeyPub == NULL) { - wolfCLU_LogError("Unable to open \ - alternate public key file %s", optarg); ret = WOLFCLU_FATAL_ERROR; } break; diff --git a/src/x509/clu_cert_setup.c b/src/x509/clu_cert_setup.c index a7cda803..41f7c48d 100644 --- a/src/x509/clu_cert_setup.c +++ b/src/x509/clu_cert_setup.c @@ -480,9 +480,8 @@ int wolfCLU_certSetup(int argc, char **argv) /* try to open output file if set */ if (ret == WOLFCLU_SUCCESS && outFile != NULL) { - out = wolfSSL_BIO_new_file(outFile, "wb"); + out = wolfCLU_OpenOutFileBio(outFile); if (out == NULL) { - wolfCLU_LogError("unable to open/create output file"); ret = WOLFCLU_FATAL_ERROR; } } diff --git a/src/x509/clu_request_setup.c b/src/x509/clu_request_setup.c index 6cea40c0..1d0fc0de 100644 --- a/src/x509/clu_request_setup.c +++ b/src/x509/clu_request_setup.c @@ -998,9 +998,9 @@ int wolfCLU_requestSetup(int argc, char** argv) } if (ret == WOLFCLU_SUCCESS && bioOut == NULL && out != NULL) { - bioOut = wolfSSL_BIO_new_file(out, "wb"); + /* CSR/certificate output, not secret. */ + bioOut = wolfCLU_OpenOutFileBio(out); if (bioOut == NULL) { - wolfCLU_LogError("Unable to open output file %s", out); ret = WOLFCLU_FATAL_ERROR; } } @@ -1110,7 +1110,8 @@ int wolfCLU_requestSetup(int argc, char** argv) WOLFSSL_BIO* keyOutBio; if (keyOut != NULL) { - keyOutBio = wolfSSL_BIO_new_file(keyOut, "wb"); + /* The freshly generated private key, owner-only. */ + keyOutBio = wolfCLU_OpenKeyFileBio(keyOut); } else { keyOutBio = wolfSSL_BIO_new(wolfSSL_BIO_s_file()); @@ -1123,7 +1124,12 @@ int wolfCLU_requestSetup(int argc, char** argv) } if (keyOutBio == NULL) { - wolfCLU_LogError("Error opening keyout file %s", keyOut); + /* wolfCLU_OpenKeyFileBio() already logged the specific reason + * when keyOut != NULL; only the stdout fallback needs a + * message here. */ + if (keyOut == NULL) { + wolfCLU_LogError("Error opening keyout stdout"); + } ret = WOLFCLU_FATAL_ERROR; } diff --git a/src/x509/clu_x509_sign.c b/src/x509/clu_x509_sign.c index 75d5b382..eec18b98 100644 --- a/src/x509/clu_x509_sign.c +++ b/src/x509/clu_x509_sign.c @@ -903,9 +903,8 @@ int wolfCLU_GenChimeraCertSign(WOLFSSL_BIO *bioCaKey, WOLFSSL_BIO *bioAltCaKey, } if (ret == WOLFCLU_SUCCESS) { - out = wolfSSL_BIO_new_file(outFileName, "wb"); + out = wolfCLU_OpenOutFileBio(outFileName); if (out == NULL) { - wolfCLU_LogError("Unable to open out file %s", outFileName); ret = WOLFCLU_FATAL_ERROR; } else { @@ -1604,6 +1603,26 @@ static void _setPolicy(word32* ret, char* str, word32 matchMask, } +/* Open a CA state file (serial counter, database/index) for read-modify-write + * access. These persist across every signing operation, so they get the same + * no-follow, owner-only protection as key material rather than the plain + * fopen()-equivalent behavior used for one-shot -out writes. */ +static WOLFSSL_BIO* wolfCLU_OpenCaStateFileBio(const char* path, + const char* mode) +{ + FILE* f = wolfCLU_CreateSecureFile(path, mode, 1); + WOLFSSL_BIO* bio; + + if (f == NULL) { + return NULL; + } + bio = wolfSSL_BIO_new_fp(f, BIO_CLOSE); + if (bio == NULL) { + XFCLOSE(f); + } + return bio; +} + static int wolfCLU_ParsePolicy(WOLFCLU_CERT_SIGN* csigner, char* sect) { WOLFSSL_CONF* conf; @@ -1689,7 +1708,7 @@ WOLFCLU_CERT_SIGN* wolfCLU_readSignConfig(char* config, char* sect) if (ret != NULL) { tmp = wolfSSL_NCONF_get_string(conf, CAsection, "database"); if (tmp != NULL) { - ret->dataBase = wolfSSL_BIO_new_file(tmp, "ab+"); + ret->dataBase = wolfCLU_OpenCaStateFileBio(tmp, "ab+"); if (ret->dataBase == NULL) { wolfCLU_LogError("Unable to open data base file %s", tmp); @@ -1726,7 +1745,7 @@ WOLFCLU_CERT_SIGN* wolfCLU_readSignConfig(char* config, char* sect) if (serial != NULL) { WOLFSSL_BIO* s; - s = wolfSSL_BIO_new_file(serial, "rb+"); + s = wolfCLU_OpenCaStateFileBio(serial, "rb+"); if (s == NULL) { wolfCLU_LogError("Unable to open serial file %s", serial); diff --git a/tests/dgst/dgst-test.py b/tests/dgst/dgst-test.py index 456224b5..1dff1e7b 100644 --- a/tests/dgst/dgst-test.py +++ b/tests/dgst/dgst-test.py @@ -9,12 +9,15 @@ import unittest sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -from wolfclu_test import (CERTS_DIR, is_fips, run_wolfssl, test_main, - truncate_sparse) +from wolfclu_test import ( + no_filesystem, CERTS_DIR, is_fips, run_wolfssl, test_main, + truncate_sparse +) DGST_DIR = os.path.dirname(os.path.abspath(__file__)) +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class DgstVerifyTest(unittest.TestCase): @classmethod @@ -22,11 +25,7 @@ def setUpClass(cls): if not os.path.isdir(CERTS_DIR): raise unittest.SkipTest("certs directory not found") - config_log = os.path.join(".", "config.log") - if os.path.isfile(config_log): - with open(config_log, "r") as f: - if "disable-filesystem" in f.read(): - raise unittest.SkipTest("filesystem support disabled") + def test_verify_sha256_rsa(self): r = run_wolfssl("dgst", "-sha256", "-verify", @@ -141,7 +140,7 @@ def test_missing_data_file_detected(self): r = run_wolfssl("dgst", "-sha256", "-verify", os.path.join(CERTS_DIR, "server-keyPub.pem"), "-signature", os.path.join(DGST_DIR, "sha256-rsa.sig")) - self.assertGreaterEqual(r.returncode, 0, r.stderr) + self.assertNotEqual(r.returncode, 0, r.stderr) def test_complete_args_not_misflagged(self): """A well-formed dgst command must not trip the malformed-argument @@ -154,6 +153,7 @@ def test_complete_args_not_misflagged(self): self.assertEqual(r.returncode, 0, r.stderr) +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class DgstLargeFileTest(unittest.TestCase): LARGE_FILE = "large-test.txt" @@ -163,11 +163,7 @@ def setUpClass(cls): if not os.path.isdir(CERTS_DIR): raise unittest.SkipTest("certs directory not found") - config_log = os.path.join(".", "config.log") - if os.path.isfile(config_log): - with open(config_log, "r") as f: - if "disable-filesystem" in f.read(): - raise unittest.SkipTest("filesystem support disabled") + # Create large file: 5000 copies of server-key.der der_path = os.path.join(CERTS_DIR, "server-key.der") @@ -245,6 +241,7 @@ def test_enc_dec_large_file(self): "Decryption of large file failed") +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class LargeFileDgstTest(unittest.TestCase): """A signature over a >4 GiB file must NOT verify a tampered copy. @@ -346,6 +343,7 @@ def test_tampered_last_byte_fails_verify(self): self.assertNotEqual(r.returncode, 0) +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class DgstSignVerifyRoundtripTest(unittest.TestCase): @classmethod @@ -389,6 +387,7 @@ def test_ecc_sign_verify_roundtrip(self): self.assertEqual(r.returncode, 0, r.stderr) +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class DgstHmacTest(unittest.TestCase): """HMAC test vectors for `dgst -mac HMAC`. @@ -437,12 +436,6 @@ class DgstHmacTest(unittest.TestCase): @classmethod def setUpClass(cls): - config_log = os.path.join(".", "config.log") - if os.path.isfile(config_log): - with open(config_log, "r") as f: - if "disable-filesystem" in f.read(): - raise unittest.SkipTest("filesystem support disabled") - cls._tmpdir = tempfile.mkdtemp(prefix="wolfclu-hmac-") cls.data_file = os.path.join(cls._tmpdir, "data.bin") with open(cls.data_file, "wb") as f: diff --git a/tests/encrypt/enc-test.py b/tests/encrypt/enc-test.py index aa30cfbd..edec0317 100644 --- a/tests/encrypt/enc-test.py +++ b/tests/encrypt/enc-test.py @@ -11,7 +11,9 @@ import unittest sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -from wolfclu_test import CERTS_DIR, WOLFSSL_BIN, run_wolfssl, test_main +from wolfclu_test import ( + no_filesystem, CERTS_DIR, WOLFSSL_BIN, run_wolfssl, test_main +) # The interactive password prompt only reads from stdin when stdin is a real # terminal (wolfCLU_GetStdinPassword -> tcgetattr fails on a pipe), so driving @@ -30,6 +32,7 @@ def run_enc(*args, password=""): stdin=subprocess.DEVNULL, timeout=60) +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class EncDecryptTest(unittest.TestCase): @classmethod @@ -37,11 +40,7 @@ def setUpClass(cls): if not os.path.isdir(CERTS_DIR): raise unittest.SkipTest("certs directory not found") - config_log = os.path.join(".", "config.log") - if os.path.isfile(config_log): - with open(config_log, "r") as f: - if "disable-filesystem" in f.read(): - raise unittest.SkipTest("filesystem support disabled") + def _cleanup(self, *files): for f in files: @@ -129,6 +128,52 @@ def test_aes128_roundtrip(self): self.assertTrue(filecmp.cmp(orig, dec, shallow=False), "decrypted file does not match original") + def test_in_out_same_file_refused(self): + """-in and -out on one file would truncate the input mid-read.""" + src = "enc_inplace.txt" + self._cleanup(src) + + with open(src, "w") as f: + f.write("plaintext that must survive\n") + + r = run_enc("enc", "-aes-128-cbc", "-in", src, "-out", src, + password="test") + self.assertNotEqual(r.returncode, 0, "in-place enc should fail") + with open(src) as f: + self.assertEqual(f.read(), "plaintext that must survive\n", + "input file was modified") + + r = run_enc("enc", "-d", "-aes-128-cbc", "-in", src, "-out", src, + password="test") + self.assertNotEqual(r.returncode, 0, "in-place dec should fail") + with open(src) as f: + self.assertEqual(f.read(), "plaintext that must survive\n", + "input file was modified") + + def test_in_out_same_file_refused_camellia(self): + """-in and -out on one file would truncate the input mid-read (non-EVP path).""" + if not _camellia_available(): + self.skipTest("camellia support not compiled in") + src = "enc_inplace_camellia.txt" + self._cleanup(src) + + with open(src, "w") as f: + f.write("plaintext that must survive\n") + + r = run_enc("enc", "-camellia-128-cbc", "-in", src, "-out", src, + password="test") + self.assertNotEqual(r.returncode, 0, "in-place enc should fail") + with open(src) as f: + self.assertEqual(f.read(), "plaintext that must survive\n", + "input file was modified") + + r = run_enc("enc", "-d", "-camellia-128-cbc", "-in", src, "-out", src, + password="test") + self.assertNotEqual(r.returncode, 0, "in-place dec should fail") + with open(src) as f: + self.assertEqual(f.read(), "plaintext that must survive\n", + "input file was modified") + def test_small_file(self): small = "enc_small.txt" enc = "enc_small.txt.enc" @@ -180,6 +225,7 @@ def test_explicit_hex_key_iv(self): "{}".format(r.stderr)) +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class EncInteropTest(unittest.TestCase): """Test interoperability with OpenSSL (skipped if openssl not available).""" @@ -336,6 +382,7 @@ def test_pbkdf2_wolfssl_pass_flag(self): self.assertTrue(filecmp.cmp(orig, dec, shallow=False)) +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class EncPassSourceTest(unittest.TestCase): """Regression tests for issue 6133. @@ -351,11 +398,7 @@ def setUpClass(cls): if not os.path.isdir(CERTS_DIR): raise unittest.SkipTest("certs directory not found") - config_log = os.path.join(".", "config.log") - if os.path.isfile(config_log): - with open(config_log, "r") as f: - if "disable-filesystem" in f.read(): - raise unittest.SkipTest("filesystem support disabled") + def _cleanup(self, *files): for f in files: @@ -420,6 +463,7 @@ def test_supported_pass_source_still_works(self): self.assertTrue(filecmp.cmp(orig, dec, shallow=False)) +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class EncLegacyNamesTest(unittest.TestCase): @classmethod @@ -479,6 +523,7 @@ def _camellia_available(): os.remove(probe) +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class EncStdinInputTest(unittest.TestCase): """Regression tests for stack buffer overflow fix (scanf -> fgets). @@ -492,11 +537,7 @@ def setUpClass(cls): if not os.path.isdir(CERTS_DIR): raise unittest.SkipTest("certs directory not found") - config_log = os.path.join(".", "config.log") - if os.path.isfile(config_log): - with open(config_log, "r") as f: - if "disable-filesystem" in f.read(): - raise unittest.SkipTest("filesystem support disabled") + cls.has_camellia = _camellia_available() @@ -640,6 +681,7 @@ def test_camellia_outname_too_long_reprompt(self): "Camellia roundtrip mismatch after too-long reprompt") +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class EncKeyInputTest(unittest.TestCase): """Tests for the -key (hex on CLI) and -inkey (key from file) flags.""" @@ -653,11 +695,7 @@ def setUpClass(cls): if not os.path.isdir(CERTS_DIR): raise unittest.SkipTest("certs directory not found") - config_log = os.path.join(".", "config.log") - if os.path.isfile(config_log): - with open(config_log, "r") as f: - if "disable-filesystem" in f.read(): - raise unittest.SkipTest("filesystem support disabled") + def _cleanup(self, *files): for f in files: @@ -914,6 +952,7 @@ def test_rand_hex_to_inkey_workflow(self): @unittest.skipUnless(HAVE_PTY, "pty not available (non-POSIX)") +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class EncStdinPasswordTest(unittest.TestCase): """Interactive stdin-password path of `encrypt` (F-5970). diff --git a/tests/genkey_sign_ver/genkey-sign-ver-test.py b/tests/genkey_sign_ver/genkey-sign-ver-test.py index 615fe385..438886dc 100644 --- a/tests/genkey_sign_ver/genkey-sign-ver-test.py +++ b/tests/genkey_sign_ver/genkey-sign-ver-test.py @@ -2,11 +2,13 @@ """Key generation, signing, and verification tests for wolfCLU.""" import os +import subprocess import sys import unittest sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -from wolfclu_test import WOLFSSL_BIN, CERTS_DIR, run_wolfssl, test_main +from wolfclu_test import (WOLFSSL_BIN, CERTS_DIR, run_wolfssl, + skip_if_no_filesystem, test_main) # Files that tests may create; cleaned up by tearDownClass _TEMP_FILES = [] @@ -14,16 +16,36 @@ def _cleanup_files(files): for f in files: - if os.path.exists(f): + # lexists, not exists: a symlink whose target is already gone would + # otherwise be left behind and break the next run with EEXIST. + if os.path.lexists(f): os.remove(f) def _has_algorithm(algo): - """Check if an algorithm is available in the current build.""" + """Check if an algorithm is available in the current build. + + Only the compiled-in key list is consulted. Substring-matching the whole + help text would always report rsa as present, because the usage EXAMPLE + in wolfCLU_genKeyHelp() names it unconditionally. + """ r = run_wolfssl("-genkey", "-h") - combined = r.stdout + r.stderr - # Look for the algorithm name in the help output - return algo in combined + keys = set() + in_list = False + for line in (r.stdout + r.stderr).splitlines(): + line = line.strip() + if line.startswith("Available keys with current configure settings"): + in_list = True + continue + if not in_list: + continue + # The list runs to the banner of asterisks that follows it. + if line.startswith("*"): + break + if not line or line.startswith("KEYS:"): + continue + keys.add(line) + return algo in keys class _GenkeySignVerifyBase(unittest.TestCase): @@ -33,11 +55,7 @@ class _GenkeySignVerifyBase(unittest.TestCase): @classmethod def setUpClass(cls): - config_log = os.path.join(".", "config.log") - if os.path.isfile(config_log): - with open(config_log, "r") as f: - if "disable-filesystem" in f.read(): - raise unittest.SkipTest("filesystem support disabled") + skip_if_no_filesystem() with open(cls.SIGN_FILE, "w") as f: f.write("Sign this test data\n") @@ -53,15 +71,9 @@ def _track(self, *files): def _gen_sign_badverify(self, algo, keybase, sig_file, fmt, extra_genkey_args=None, use_output_flag=False): - """Generate a key, sign SIGN_FILE, then verify the (valid) signature - against a *different* message and assert the command fails with a - non-zero (non-crash) exit. - - Verifying a genuine signature against tampered input produces a - well-formed signature that simply does not match: the verify API - returns successfully with stat/res != 1. The buggy code logged - "Invalid Signature." but still exited 0; the fix must turn that into - a failure exit (F-5362).""" + """Sign, then verify against a different message: must fail with a + non-zero (non-crash) exit, not just log "Invalid Signature." while + still exiting 0.""" priv, pub = self._genkey(algo, keybase, fmt, extra_genkey_args, use_output_flag=use_output_flag) self._sign(algo, priv, fmt, sig_file) @@ -166,9 +178,46 @@ def test_ed25519_raw(self): self._gen_sign_verify("ed25519", "edkey", "ed-signed.sig", "raw") def test_ed25519_bad_verify(self): - """An Ed25519 signature that does not match must fail (F-5362).""" + """An Ed25519 signature that does not match must fail.""" self._gen_sign_badverify("ed25519", "edkey-bad", "ed-bad.sig", "der") + def test_ed25519_reads_key_through_symlink(self): + """Key reads follow symlinks. Refusing them would reject the layouts + keys normally live in (certbot's live/ symlinks, /etc/ssl/private + farms, /dev/stdin) while buying nothing, since anyone who can plant a + symlink at the key's path can replace the key file itself. The + no-symlink rule is enforced on the write paths instead.""" + if not hasattr(os, "symlink"): + self.skipTest("symlinks not supported") + priv, pub = self._genkey("ed25519", "edkey-symlink", "der", + use_output_flag=True) + sig_file = "ed-symlink.sig" + self._sign("ed25519", priv, "der", sig_file) + + priv_link = "edkey-symlink.priv.link" + pub_link = "edkey-symlink.pub.link" + signed = "ed-symlink-signed.sig" + self._track(priv_link, pub_link, signed) + try: + os.symlink(os.path.abspath(priv), priv_link) + os.symlink(os.path.abspath(pub), pub_link) + except (OSError, NotImplementedError) as e: + self.skipTest(f"could not create symlink: {e}") + + r = run_wolfssl("-ed25519", "-sign", "-inkey", priv_link, + "-inform", "der", "-in", self.SIGN_FILE, + "-out", signed) + self.assertEqual(r.returncode, 0, + f"sign through a symlinked private key failed: " + f"{r.stderr}") + + r = run_wolfssl("-ed25519", "-verify", "-inkey", pub_link, + "-inform", "der", "-sigfile", sig_file, + "-in", self.SIGN_FILE, "-pubin") + self.assertEqual(r.returncode, 0, + f"verify through a symlinked public key failed: " + f"{r.stderr}") + def test_ed25519_signature_size(self): """ED25519 signatures must be exactly 64 bytes.""" priv, pub = self._genkey("ed25519", "edkey-sztest", "der", @@ -207,7 +256,7 @@ def test_ecc_pem(self): self._gen_sign_verify("ecc", "ecckey", "ecc-signed.sig", "pem") def test_ecc_bad_verify(self): - """An ECC signature that does not match must fail (F-5362).""" + """An ECC signature that does not match must fail.""" self._gen_sign_badverify("ecc", "ecckey-bad", "ecc-bad.sig", "der") def test_ecc_der_key_size_and_roundtrip(self): @@ -250,9 +299,13 @@ def test_ecc_sign_invalid_key_fails(self): self.assertNotEqual(r.returncode, 0, "ECC signing with empty key should have failed") - def test_ecc_sign_empty_input_fails(self): - """Signing a 0-byte input file must fail gracefully (regression for - the XFSEEK/XFTELL size guards in wolfCLU_sign_data).""" + def test_ecc_sign_empty_input_succeeds(self): + """Signing a 0-byte input file must succeed: wolfCLU_sign_data() + reads the message through wolfCLU_ReadMessageFileToBuffer(), which + (like wolfCLU_ReadVerifyHash() on the verify side) accepts a + zero-length file rather than treating it as a size error, so sign + and verify agree on an empty message being legitimate input (e.g. + the Ed25519 RFC 8032 empty-message test vector).""" priv, _ = self._genkey("ecc", "ecc-empty-in", "der", use_output_flag=True) empty_in = "empty-input.txt" @@ -262,11 +315,12 @@ def test_ecc_sign_empty_input_fails(self): r = run_wolfssl("-ecc", "-sign", "-inkey", priv, "-inform", "der", "-in", empty_in, "-out", empty_sig) - self.assertNotEqual(r.returncode, 0, - "ECC signing of empty input should have failed") - self.assertGreaterEqual(r.returncode, 0, - "ECC sign of empty input crashed with signal " - "{}".format(r.returncode)) + self.assertEqual(r.returncode, 0, + "ECC signing of empty input should have succeeded: " + "{}".format(r.stderr)) + self.assertTrue(os.path.exists(empty_sig) and + os.path.getsize(empty_sig) > 0, + "ECC sign of empty input produced no signature") def test_ecc_sign_missing_inkey_value(self): """-inkey with no value must fail gracefully (no segfault).""" @@ -327,6 +381,235 @@ def test_rsa_sign_invalid_key_fails(self): "RSA signing with empty key should have failed") +def _icacls_entries(path): + """Return the list of ACE description strings icacls reports for path, + one string per trustee (e.g. "DOMAIN\\user:(F)").""" + try: + r = subprocess.run(["icacls", path], capture_output=True, text=True, + timeout=10) + except (OSError, subprocess.SubprocessError) as e: + # icacls missing or unusable is an environment limitation, not a + # failure of the code under test. + raise unittest.SkipTest("could not run icacls: {}".format(e)) + if r.returncode != 0: + raise unittest.SkipTest( + "icacls {} failed: {}".format(path, r.stderr)) + + entries = [] + for line in r.stdout.splitlines(): + line = line.rstrip() + if not line: + break + if line.lower().startswith("successfully processed"): + break + if line.startswith(path): + line = line[len(path):].strip() + else: + line = line.strip() + if line: + entries.append(line) + return entries + + +class KeyFilePermissionsTest(unittest.TestCase): + """wolfCLU_OpenKeyFile must write private keys with owner-only + permissions (POSIX 0600 / Windows single-owner ACE) and replace, not + append to, a pre-existing file. Windows is checked via icacls since + NTFS ACLs, not os.stat() mode bits, are what's enforced there.""" + + @classmethod + def setUpClass(cls): + skip_if_no_filesystem() + + @classmethod + def tearDownClass(cls): + _cleanup_files(_TEMP_FILES) + _TEMP_FILES.clear() + + def _assert_owner_only(self, priv, label): + if os.name == "nt": + entries = _icacls_entries(priv) + self.assertEqual(len(entries), 1, + "{}: expected exactly one owner-only ACL " + "entry, got: {}".format(label, entries)) + entry = entries[0] + self.assertIn("(F)", entry, + "{}: owner ACE missing full control: {}" + .format(label, entry)) + for forbidden in ("Everyone", "Authenticated Users", + "BUILTIN\\Users", "NT AUTHORITY"): + self.assertNotIn(forbidden, entry, + "{}: unexpected broad-access principal " + "{!r} in ACL: {}" + .format(label, forbidden, entry)) + else: + mode = os.stat(priv).st_mode & 0o777 + self.assertEqual(mode, 0o600, + "{}: private key file mode is {:o}, expected " + "600".format(label, mode)) + + def _priv_mode(self, keybase, algo, extra_args=(), outform="der"): + """Generate a keypair with algo and return the .priv path. + + Skips when algo is not compiled in. outform defaults to der; XMSS + only supports raw. + """ + if not _has_algorithm(algo): + self.skipTest("{} support not compiled in".format(algo)) + priv = keybase + ".priv" + pub = keybase + ".pub" + _TEMP_FILES.extend([priv, pub]) + args = ["-genkey", algo] + list(extra_args) + [ + "-out", keybase, "-outform", outform, "-output", "KEYPAIR"] + r = run_wolfssl(*args) + if "NOT_COMPILED_IN" in r.stderr or "not enabled" in r.stderr: + self.skipTest("not compiled in") + self.assertEqual(r.returncode, 0, r.stderr) + return priv + + def test_rsa_priv_key_mode_is_owner_only(self): + priv = self._priv_mode("rsakey-perm-test", "rsa", + ["-size", "2048"]) + self._assert_owner_only(priv, "RSA") + + def test_ecc_priv_key_mode_is_owner_only(self): + priv = self._priv_mode("ecckey-perm-test", "ecc") + self._assert_owner_only(priv, "ECC") + + def test_ed25519_priv_key_mode_is_owner_only(self): + priv = self._priv_mode("edkey-perm-test", "ed25519") + self._assert_owner_only(priv, "Ed25519") + + def test_dh_priv_key_mode_is_owner_only(self): + params_file = "dh-perm-test.params" + keyfile = "dh-perm-test.key" + _TEMP_FILES.extend([params_file, keyfile]) + + # Probe and generate in one shot: 1024-bit DH parameter generation is + # a primality search, so a throwaway second run is the slowest and + # most timeout-prone thing this module could do. + r = run_wolfssl("dhparam", "1024", "-out", params_file) + if "DH support not compiled into wolfSSL" in r.stdout + r.stderr: + self.skipTest("DH support not compiled in") + if "NOT_COMPILED_IN" in r.stderr or "not enabled" in r.stderr: + self.skipTest("not compiled in") + self.assertEqual(r.returncode, 0, r.stderr) + + r = run_wolfssl("dhparam", "-in", params_file, "-genkey", + "-out", keyfile) + if "NOT_COMPILED_IN" in r.stderr or "not enabled" in r.stderr: + self.skipTest("not compiled in") + self.assertEqual(r.returncode, 0, r.stderr) + self._assert_owner_only(keyfile, "DH") + + def test_dsa_priv_key_mode_is_owner_only(self): + params_file = "dsa-perm-test.params" + keyfile = "dsa-perm-test.key" + _TEMP_FILES.extend([params_file, keyfile]) + + # Same as DH above: one generation, not two. + r = run_wolfssl("dsaparam", "-out", params_file, "1024") + if "DSA support not compiled into wolfSSL" in r.stdout + r.stderr: + self.skipTest("DSA support not compiled in") + if "NOT_COMPILED_IN" in r.stderr or "not enabled" in r.stderr: + self.skipTest("not compiled in") + self.assertEqual(r.returncode, 0, r.stderr) + + r = run_wolfssl("dsaparam", "-in", params_file, "-genkey", + "-out", keyfile) + if "NOT_COMPILED_IN" in r.stderr or "not enabled" in r.stderr: + self.skipTest("not compiled in") + self.assertEqual(r.returncode, 0, r.stderr) + self._assert_owner_only(keyfile, "DSA") + + def test_dilithium_priv_key_mode_is_owner_only(self): + priv = self._priv_mode("dilithium-perm-test", "dilithium", + ["-level", "2"]) + self._assert_owner_only(priv, "Dilithium") + + def test_mldsa_priv_key_mode_is_owner_only(self): + priv = self._priv_mode("mldsa-perm-test", "ml-dsa", ["-level", "2"]) + self._assert_owner_only(priv, "ML-DSA") + + def test_xmss_priv_key_mode_is_owner_only(self): + """XMSS writes its private key from a wolfSSL callback rather than + through the genkey path, so it needs its own coverage. The option is + -height, and raw is the only format XMSS supports.""" + priv = self._priv_mode("xmss-perm-test", "xmss", ["-height", "10"], + outform="raw") + self._assert_owner_only(priv, "XMSS") + + + @unittest.skipIf(os.name == "nt", + "symlink attack path is POSIX-specific") + def test_symlink_at_priv_path_is_not_followed(self): + """A pre-existing symlink at the -out path must not be followed: + key material must never land at the symlink's target, and the + target's contents must be untouched.""" + if not _has_algorithm("rsa"): + self.skipTest("rsa support not compiled in") + keybase = "rsakey-symlink-test" + priv = keybase + ".priv" + pub = keybase + ".pub" + target = "rsakey-symlink-target.txt" + _TEMP_FILES.extend([priv, pub, target]) + + with open(target, "wb") as f: + f.write(b"attacker-owned file; must not be overwritten") + os.symlink(target, priv) + + r = run_wolfssl("-genkey", "rsa", "-size", "2048", "-out", keybase, + "-outform", "der", "-output", "KEYPAIR") + + with open(target, "rb") as f: + target_content = f.read() + self.assertEqual(target_content, + b"attacker-owned file; must not be overwritten", + "symlink target was written through; key " + "material leaked to an attacker-controlled path") + + # The refusal itself is the contract, not just the absence of + # collateral damage: a regression that quietly wrote the key + # somewhere else and reported success would leave the target + # untouched too. + self.assertNotEqual(r.returncode, 0, + "-genkey reported success for a symlinked -out " + "path that it is supposed to refuse") + self.assertIn("symlink", (r.stdout + r.stderr).lower(), + "refusal did not explain that the path is a symlink") + self.assertTrue(os.path.islink(priv), + "-genkey removed or replaced the symlink instead of " + "refusing it") + + def test_preexisting_priv_file_is_replaced(self): + """A stale file at the target path must be replaced, not appended + to or left with mixed content, and must end up owner-only.""" + if not _has_algorithm("rsa"): + self.skipTest("rsa support not compiled in") + keybase = "rsakey-replace-test" + priv = keybase + ".priv" + pub = keybase + ".pub" + _TEMP_FILES.extend([priv, pub]) + + with open(priv, "wb") as f: + f.write(b"stale placeholder content") + if os.name != "nt": + os.chmod(priv, 0o644) + + r = run_wolfssl("-genkey", "rsa", "-size", "2048", "-out", keybase, + "-outform", "der", "-output", "KEYPAIR") + if "NOT_COMPILED_IN" in r.stderr or "not enabled" in r.stderr: + self.skipTest("not compiled in") + self.assertEqual(r.returncode, 0, r.stderr) + + with open(priv, "rb") as f: + content = f.read() + self.assertNotIn(b"stale placeholder content", content, + "stale content survived key generation") + + self._assert_owner_only(priv, "replaced RSA") + + @unittest.skipUnless(_has_algorithm("dilithium"), "dilithium not available") class DilithiumTest(_GenkeySignVerifyBase): @@ -348,7 +631,7 @@ def test_dilithium_pem(self): skip_priv_verify=True, use_output_flag=True) def test_dilithium_bad_verify(self): - """A Dilithium signature that does not match must fail (F-5362).""" + """A Dilithium signature that does not match must fail.""" for level in [2, 3, 5]: with self.subTest(level=level): self._gen_sign_badverify( @@ -364,6 +647,8 @@ def test_output_pub_only(self): r = run_wolfssl("-genkey", "dilithium", "-level", "2", "-out", "mldsakey_pub", "-outform", "der", "-output", "pub") + if "NOT_COMPILED_IN" in r.stderr or "not enabled" in r.stderr: + self.skipTest("not compiled in") self.assertEqual(r.returncode, 0, r.stderr) self.assertTrue(os.path.exists(pub), ".pub file missing") self.assertFalse(os.path.exists(priv), ".priv unexpectedly created") @@ -376,6 +661,8 @@ def test_output_priv_only(self): r = run_wolfssl("-genkey", "dilithium", "-level", "2", "-out", "mldsakey_priv", "-outform", "der", "-output", "priv") + if "NOT_COMPILED_IN" in r.stderr or "not enabled" in r.stderr: + self.skipTest("not compiled in") self.assertEqual(r.returncode, 0, r.stderr) self.assertTrue(os.path.exists(priv), ".priv file missing") self.assertFalse(os.path.exists(pub), ".pub unexpectedly created") @@ -541,11 +828,7 @@ class SignVerifySetupArgsTest(unittest.TestCase): @classmethod def setUpClass(cls): - config_log = os.path.join(".", "config.log") - if os.path.isfile(config_log): - with open(config_log, "r") as f: - if "disable-filesystem" in f.read(): - raise unittest.SkipTest("filesystem support disabled") + skip_if_no_filesystem() with open(cls.SIGN_FILE, "w") as f: f.write("Sign this test data\n") diff --git a/tests/hash/hash-test.py b/tests/hash/hash-test.py index 5e62759e..c245864b 100644 --- a/tests/hash/hash-test.py +++ b/tests/hash/hash-test.py @@ -8,7 +8,8 @@ import unittest sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -from wolfclu_test import (CERTS_DIR, run_wolfssl, test_main, truncate_sparse) +from wolfclu_test import (CERTS_DIR, run_wolfssl, skip_if_no_filesystem, + test_main, truncate_sparse) HASH_DIR = os.path.dirname(os.path.abspath(__file__)) CERT_FILE = os.path.join(CERTS_DIR, "ca-cert.pem") @@ -28,11 +29,7 @@ def setUpClass(cls): if not os.path.isdir(CERTS_DIR): raise unittest.SkipTest("certs directory not found") - config_log = os.path.join(".", "config.log") - if os.path.isfile(config_log): - with open(config_log, "r") as f: - if "disable-filesystem" in f.read(): - raise unittest.SkipTest("filesystem support disabled") + skip_if_no_filesystem() def test_sha(self): r = run_wolfssl("-hash", "-sha", "-in", CERT_FILE) @@ -82,11 +79,7 @@ def setUpClass(cls): if not os.path.isdir(CERTS_DIR): raise unittest.SkipTest("certs directory not found") - config_log = os.path.join(".", "config.log") - if os.path.isfile(config_log): - with open(config_log, "r") as f: - if "disable-filesystem" in f.read(): - raise unittest.SkipTest("filesystem support disabled") + skip_if_no_filesystem() def test_md5(self): r = run_wolfssl("md5", CERT_FILE) @@ -176,6 +169,54 @@ def test_tampered_last_byte_changes_hash(self): self.assertNotEqual(r1.stdout.strip(), r2.stdout.strip()) +class HashOutTargetTest(unittest.TestCase): + """-out must accept every target fopen() accepts. + + Non-secret output is not hardened against symlinks, so writing to + /dev/stdout or through a symlink has to keep working. + """ + + @classmethod + def setUpClass(cls): + if not os.path.isdir(CERTS_DIR): + raise unittest.SkipTest("certs directory not found") + skip_if_no_filesystem() + cls._tmpdir = tempfile.mkdtemp(prefix="wolfclu-hash-out-") + + @classmethod + def tearDownClass(cls): + shutil.rmtree(getattr(cls, "_tmpdir", ""), ignore_errors=True) + + def test_out_dev_stdout(self): + if not os.path.exists("/dev/stdout"): + self.skipTest("/dev/stdout not available") + target = os.path.join(self._tmpdir, "stdout-redirect.bin") + with open(target, "wb") as f: + r = run_wolfssl("-hash", "-sha256", "-in", CERT_FILE, + "-out", "/dev/stdout", stdout=f) + self.assertEqual(r.returncode, 0, r.stderr) + self.assertEqual(os.path.getsize(target), 32) + + def test_out_through_symlink(self): + if not hasattr(os, "symlink"): + self.skipTest("symlinks not supported") + target = os.path.join(self._tmpdir, "real.bin") + link = os.path.join(self._tmpdir, "link.bin") + open(target, "wb").close() + try: + os.symlink(target, link) + except (OSError, NotImplementedError) as e: + self.skipTest("could not create symlink: {}".format(e)) + + r = run_wolfssl("-hash", "-sha256", "-in", CERT_FILE, "-out", link) + self.assertEqual(r.returncode, 0, r.stderr) + self.assertTrue(os.path.islink(link), + "-out replaced the symlink instead of writing " + "through it") + self.assertEqual(os.path.getsize(target), 32, + "-out did not write through the symlink") + + class HashArgErrorTest(unittest.TestCase): """Argument-handling regression tests.""" diff --git a/tests/ocsp/ocsp-test.py b/tests/ocsp/ocsp-test.py index 077be417..124d3100 100644 --- a/tests/ocsp/ocsp-test.py +++ b/tests/ocsp/ocsp-test.py @@ -15,7 +15,9 @@ import unittest sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -from wolfclu_test import WOLFSSL_BIN, CERTS_DIR, test_main, find_free_port +from wolfclu_test import ( + no_filesystem, WOLFSSL_BIN, CERTS_DIR, test_main, find_free_port +) HAS_OPENSSL = shutil.which("openssl") is not None @@ -107,6 +109,7 @@ def _run_client(binary, port, extra_args=None): return r.returncode, r.stdout + r.stderr +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class _OCSPInteropBase(unittest.TestCase): """Base class for a single client/responder combination. @@ -310,6 +313,7 @@ def test_12_graceful_shutdown(self): # Concrete test classes for each client/responder combination. # Each gets a dynamically assigned port in setUpClass to avoid conflicts. +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestWolfsslClientWolfsslResponder(_OCSPInteropBase): CLIENT_BIN = WOLFSSL_BIN RESPONDER_BIN = WOLFSSL_BIN @@ -326,6 +330,7 @@ def test_01_client_start_up(self): @unittest.skipUnless(HAS_OPENSSL, "openssl not available") +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestWolfsslClientOpensslResponder(_OCSPInteropBase): CLIENT_BIN = WOLFSSL_BIN RESPONDER_BIN = "openssl" @@ -341,6 +346,7 @@ def test_01_client_start_up(self): @unittest.skipUnless(HAS_OPENSSL, "openssl not available") +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestOpensslClientWolfsslResponder(_OCSPInteropBase): CLIENT_BIN = "openssl" RESPONDER_BIN = WOLFSSL_BIN @@ -355,6 +361,7 @@ def test_01_client_start_up(self): self.assertIn("good", out.lower(), out) @unittest.skipUnless(HAS_OPENSSL, "openssl not available") +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestOpensslClientOpensslResponder(_OCSPInteropBase): CLIENT_BIN = "openssl" RESPONDER_BIN = "openssl" @@ -368,6 +375,7 @@ def test_01_client_start_up(self): self.assertEqual(rc, 0, out) self.assertIn("good", out.lower(), out) +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestPortValidation(unittest.TestCase): """Boundary tests for the -port range check in wolfCLU_OcspSetup. @@ -451,6 +459,7 @@ def test_port_missing_argument_rejected(self): "expected missing-argument diagnostic for -port") +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestNrequestValidation(unittest.TestCase): """Boundary tests for the -nrequest range check in wolfCLU_OcspSetup. diff --git a/tests/pkcs/pkcs12-test.py b/tests/pkcs/pkcs12-test.py index 7d067f1c..12e6121b 100644 --- a/tests/pkcs/pkcs12-test.py +++ b/tests/pkcs/pkcs12-test.py @@ -95,5 +95,32 @@ def test_nocerts_with_passout(self): self.assertEqual(r.returncode, 0, r.stderr) + def test_out_without_filename_fails(self): + """Trailing -out binds NULL, silently leaking bundle to stdout.""" + r = run_wolfssl("pkcs12", "-nodes", "-passin", 'pass:wolfSSL test', + "-passout", "pass:", "-in", P12_FILE, "-out") + self.assertNotEqual(r.returncode, 0, + "-out without filename must fail") + self.assertNotIn("-----BEGIN", r.stdout, + "bundle leaked to stdout") + + def test_in_out_same_file_refused(self): + """-in and -out naming the same file must be refused.""" + same = "pkcs12_inplace.p12" + self.addCleanup( + lambda: os.remove(same) if os.path.exists(same) else None) + with open(P12_FILE, "rb") as src: + original = src.read() + with open(same, "wb") as f: + f.write(original) + + r = run_wolfssl("pkcs12", "-nodes", "-passin", 'pass:wolfSSL test', + "-passout", "pass:", "-in", same, "-out", same) + self.assertNotEqual(r.returncode, 0, + "in-place update must fail") + with open(same, "rb") as f: + self.assertEqual(f.read(), original, "-in was modified") + + if __name__ == "__main__": test_main() diff --git a/tests/pkcs/pkcs7-test.py b/tests/pkcs/pkcs7-test.py index b6baa472..3490ab10 100644 --- a/tests/pkcs/pkcs7-test.py +++ b/tests/pkcs/pkcs7-test.py @@ -12,6 +12,12 @@ class Pkcs7Test(unittest.TestCase): + def _cleanup(self, *files): + for f in files: + self.addCleanup(lambda p=f: os.remove(p) + if os.path.exists(p) else None) + + @classmethod def setUpClass(cls): if not os.path.isdir(CERTS_DIR): @@ -96,5 +102,31 @@ def test_stdin_input(self): self.assertIn(b"BEGIN PKCS7", r.stdout + r.stderr) + def test_out_without_filename_fails(self): + """A trailing -out binds a NULL optarg; without the guard the output + silently goes to stdout with a success exit code.""" + r = run_wolfssl("pkcs7", "-in", + os.path.join(CERTS_DIR, "signed.p7b"), + "-inform", "der", "-out") + self.assertNotEqual(r.returncode, 0, + "-out without filename must fail") + + def test_in_out_same_file_refused(self): + """-in and -out naming the same file must be refused.""" + same = "pkcs7_inplace.p7b" + self._cleanup(same) + with open(os.path.join(CERTS_DIR, "signed.p7b"), "rb") as src: + original = src.read() + with open(same, "wb") as f: + f.write(original) + + r = run_wolfssl("pkcs7", "-in", same, "-inform", "der", + "-out", same, "-outform", "pem") + self.assertNotEqual(r.returncode, 0, + "in-place update must fail") + with open(same, "rb") as f: + self.assertEqual(f.read(), original, "-in was modified") + + if __name__ == "__main__": test_main() diff --git a/tests/pkcs/pkcs8-test.py b/tests/pkcs/pkcs8-test.py index 2a23aa3d..90a930f1 100644 --- a/tests/pkcs/pkcs8-test.py +++ b/tests/pkcs/pkcs8-test.py @@ -116,6 +116,33 @@ def test_fail_wrong_format(self): "-inform", "DER", "-passin", "pass:yassl123") self.assertNotEqual(r.returncode, 0) + def test_out_without_filename_fails(self): + """A trailing -out binds a NULL optarg. Without the guard the key is + silently written to stdout with a success exit code.""" + r = run_wolfssl("pkcs8", "-in", + os.path.join(CERTS_DIR, "server-keyEnc.pem"), + "-passin", "pass:yassl123", "-out") + self.assertNotEqual(r.returncode, 0, + "-out without filename must fail") + self.assertNotIn("-----BEGIN", r.stdout, + "private key leaked to stdout") + + def test_in_out_same_file_refused(self): + """-in and -out naming the same file must be refused.""" + same = "pkcs8_inplace.pem" + self._cleanup(same) + with open(os.path.join(CERTS_DIR, "server-key.pem"), "rb") as src: + original = src.read() + with open(same, "wb") as f: + f.write(original) + + r = run_wolfssl("pkcs8", "-in", same, "-out", same, "-topk8", + "-nocrypt") + self.assertNotEqual(r.returncode, 0, + "in-place update must fail") + with open(same, "rb") as f: + self.assertEqual(f.read(), original, "-in was modified") + if __name__ == "__main__": test_main() diff --git a/tests/pkey/pkey-test.py b/tests/pkey/pkey-test.py index 91b528d1..7ec1b7d9 100644 --- a/tests/pkey/pkey-test.py +++ b/tests/pkey/pkey-test.py @@ -108,5 +108,29 @@ def test_out_to_file(self): self.assertIn("BEGIN PUBLIC KEY", f.read()) + def test_out_without_filename_fails(self): + """A trailing -out binds a NULL optarg; without the guard the key is + silently written to stdout with a success exit code.""" + r = run_wolfssl("pkey", "-in", + os.path.join(CERTS_DIR, "server-key.pem"), "-out") + self.assertNotEqual(r.returncode, 0, + "-out without filename must fail") + + def test_in_out_same_file_refused(self): + """-in and -out naming the same file must be refused.""" + same = "pkey_inplace.pem" + self._cleanup(same) + with open(os.path.join(CERTS_DIR, "server-key.pem"), "rb") as src: + original = src.read() + with open(same, "wb") as f: + f.write(original) + + r = run_wolfssl("pkey", "-in", same, "-out", same) + self.assertNotEqual(r.returncode, 0, + "in-place update must fail") + with open(same, "rb") as f: + self.assertEqual(f.read(), original, "-in was modified") + + if __name__ == "__main__": test_main() diff --git a/tests/pkey/rsa-test.py b/tests/pkey/rsa-test.py index 3e58b275..4d243dec 100644 --- a/tests/pkey/rsa-test.py +++ b/tests/pkey/rsa-test.py @@ -180,5 +180,29 @@ def test_pubout_from_private(self): self.assertEqual(r.stdout.strip(), RSA_PUBKEY_PEM) + def test_out_without_filename_fails(self): + """A trailing -out binds a NULL optarg; without the guard the key is + silently written to stdout with a success exit code.""" + r = run_wolfssl("rsa", "-in", + os.path.join(CERTS_DIR, "server-key.pem"), "-out") + self.assertNotEqual(r.returncode, 0, + "-out without filename must fail") + + def test_in_out_same_file_refused(self): + """-in and -out naming the same file must be refused.""" + same = "rsa_inplace.pem" + self._cleanup(same) + with open(os.path.join(CERTS_DIR, "server-key.pem"), "rb") as src: + original = src.read() + with open(same, "wb") as f: + f.write(original) + + r = run_wolfssl("rsa", "-in", same, "-out", same) + self.assertNotEqual(r.returncode, 0, + "in-place update must fail") + with open(same, "rb") as f: + self.assertEqual(f.read(), original, "-in was modified") + + if __name__ == "__main__": test_main() diff --git a/tests/tools/include.am b/tests/tools/include.am new file mode 100644 index 00000000..30201144 --- /dev/null +++ b/tests/tools/include.am @@ -0,0 +1,17 @@ +# vim:ft=automake +# included from top level Makefile.am +# All paths should be given relative to root directory + +check_PROGRAMS += tests/tools/tools_unit_test + +# No _LDADD is needed: configure.ac's AC_CHECK_LIB([wolfssl], ...) puts +# -lwolfssl in the global $(LIBS), which automake appends to every program +# link, and bin_PROGRAMS wolfssl links the same implicit way. +# +# No per-target _CFLAGS/_CPPFLAGS either: setting any of them forces +# per-target object names, which compiles clu_funcs.c and clu_log.c a second +# time instead of reusing the objects the wolfssl binary already builds. +tests_tools_tools_unit_test_SOURCES = \ + tests/tools/tools_unit_test.c \ + src/clu_log.c \ + src/tools/clu_funcs.c diff --git a/tests/tools/tools_unit_test.c b/tests/tools/tools_unit_test.c new file mode 100644 index 00000000..c22ee569 --- /dev/null +++ b/tests/tools/tools_unit_test.c @@ -0,0 +1,1022 @@ +/* tools_unit_test.c */ + +#include +#include +#include +#include +/* struct stat/stat() are used on both platforms; MSVC and MinGW supply them + * from too, so this cannot live in the POSIX arm below. */ +#include +#include +#ifdef _WIN32 + #include + #define GETPID _getpid +#else + #include + #define GETPID getpid +#endif + +#include +#include +#include + +/* Everything under test is compiled out without a stdio filesystem, so the + * whole suite reports the automake "skipped" status in that configuration. */ +#ifndef WOLFCLU_NO_FILESYSTEM + +static int fail = 0; + +/* Total assertions actually executed, pass or fail. Distinguishes "every + * fixture in this environment was unbuildable, nothing ran" (automake SKIP) + * from "some fixtures were unbuildable, but everything that did run passed" + * (automake PASS) - the latter must not be reported as skipped. */ +static int checked = 0; + +#define CHECK(cond, msg) \ + do { \ + checked++; \ + if (!(cond)) { \ + printf("FAIL: %s (%s:%d)\n", msg, __FILE__, __LINE__); \ + fail++; \ + } \ + } while (0) + +/* An environment that cannot build a fixture (no symlinks, no hard links, no + * FIFOs) must be distinguishable from a run that genuinely asserted. */ +static int skipped = 0; +#define SKIP(msg) do { \ + printf("SKIP: %s (%s:%d)\n", msg, __FILE__, __LINE__); \ + skipped++; \ +} while(0) + +static void testReadFileToBuffer(void) +{ + byte* buf = NULL; + int bufSz = 0; + int ret; + char testFile[64]; + FILE* f; + + XSNPRINTF(testFile, sizeof(testFile), "test_read_file_%d.tmp", + (int)GETPID()); + + /* NULL args */ + ret = wolfCLU_ReadFileToBuffer(NULL, 100, &buf, &bufSz); + CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), "ReadFileToBuffer NULL path"); + ret = wolfCLU_ReadFileToBuffer(testFile, 100, NULL, &bufSz); + CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), "ReadFileToBuffer NULL outBuf"); + ret = wolfCLU_ReadFileToBuffer(testFile, 100, &buf, NULL); + CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), "ReadFileToBuffer NULL outSz"); + ret = wolfCLU_ReadFileToBuffer(testFile, 0, &buf, &bufSz); + CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), "ReadFileToBuffer maxSz <= 0"); + + /* Missing file */ + remove(testFile); /* Ensure it doesn't exist */ + ret = wolfCLU_ReadFileToBuffer(testFile, 100, &buf, &bufSz); + CHECK(ret == WOLFCLU_FATAL_ERROR, "ReadFileToBuffer missing file"); + + /* Empty file */ + f = fopen(testFile, "wb"); + if (f) { + fclose(f); + ret = wolfCLU_ReadFileToBuffer(testFile, 100, &buf, &bufSz); + CHECK(ret == WOLFCLU_FATAL_ERROR, "ReadFileToBuffer empty file"); + + /* Unlike ReadFileToBuffer(), the message-file variant must accept + * an empty file: some algorithms (e.g. Ed25519, RFC 8032 test + * vector 1) sign/verify a 0-byte message. */ + ret = wolfCLU_ReadMessageFileToBuffer(testFile, 100, &buf, &bufSz); + CHECK(ret == WOLFCLU_SUCCESS, + "ReadMessageFileToBuffer empty file succeeds"); + CHECK(bufSz == 0, "ReadMessageFileToBuffer empty file size"); + if (buf) { + CHECK(buf[0] == '\0', + "ReadMessageFileToBuffer empty file null terminated"); + XFREE(buf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + } + remove(testFile); + } else { + CHECK(0, "ReadFileToBuffer empty file: fopen failed"); + } + + /* File exceeds maxSz */ + f = fopen(testFile, "wb"); + if (f) { + if (fwrite("12345", 1, 5, f) == 5) { + fclose(f); + ret = wolfCLU_ReadFileToBuffer(testFile, 4, &buf, &bufSz); + CHECK(ret == WOLFCLU_FATAL_ERROR, "ReadFileToBuffer exceeds maxSz"); + } else { + fclose(f); + CHECK(0, "ReadFileToBuffer exceeds maxSz: fwrite failed"); + } + remove(testFile); + } else { + CHECK(0, "ReadFileToBuffer exceeds maxSz: fopen failed"); + } + + /* Valid read */ + f = fopen(testFile, "wb"); + if (f) { + if (fwrite("12345", 1, 5, f) == 5) { + fclose(f); + ret = wolfCLU_ReadFileToBuffer(testFile, 10, &buf, &bufSz); + CHECK(ret == WOLFCLU_SUCCESS, "ReadFileToBuffer valid read"); + CHECK(bufSz == 5, "ReadFileToBuffer size"); + if (buf) { + CHECK(XMEMCMP(buf, "12345", 5) == 0, + "ReadFileToBuffer content"); + CHECK(buf[5] == '\0', "ReadFileToBuffer null terminated"); + XFREE(buf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + } + } else { + fclose(f); + CHECK(0, "ReadFileToBuffer valid read: fwrite failed"); + } + remove(testFile); + } else { + CHECK(0, "ReadFileToBuffer valid read: fopen failed"); + } +} + +static void testPathsRefEqual(void) +{ + FILE* f; + char relPath[64]; + char dotRelPath[80]; + + CHECK(wolfCLU_PathsRefEqual(NULL, NULL) == 0, "PathsRefEqual NULLs"); + CHECK(wolfCLU_PathsRefEqual("a", NULL) == 0, "PathsRefEqual one NULL"); + CHECK(wolfCLU_PathsRefEqual("same.txt", "same.txt") == 1, + "PathsRefEqual identical"); + CHECK(wolfCLU_PathsRefEqual("a.txt", "b.txt") == 0, + "PathsRefEqual different"); + + XSNPRINTF(relPath, sizeof(relPath), "test_ref_equal_%d.tmp", + (int)GETPID()); + XSNPRINTF(dotRelPath, sizeof(dotRelPath), "./%s", relPath); + + /* Non-existent files drop through to canonicalization */ + CHECK(wolfCLU_PathsRefEqual(relPath, dotRelPath) == 1, + "PathsRefEqual absolute/relative non-existent"); + + f = fopen(relPath, "wb"); + if (f) { + fclose(f); + /* Tests the dev/ino check for existing files. */ + CHECK(wolfCLU_PathsRefEqual(relPath, dotRelPath) == 1, + "PathsRefEqual absolute/relative existing"); + remove(relPath); + } + else { + CHECK(0, "PathsRefEqual absolute/relative: fopen failed"); + } + +#ifndef _WIN32 + /* A symlink aliasing the same target must be caught even though its + * canonicalized parent-dir+basename string never matches the target's: + * this is the case wolfCLU_OpenOutFile() following the symlink and + * truncating the target mid-read depends on being detected. */ + { + char target[64]; + char link[64]; + + XSNPRINTF(target, sizeof(target), "test_ref_equal_tgt_%d.tmp", + (int)GETPID()); + XSNPRINTF(link, sizeof(link), "test_ref_equal_link_%d.tmp", + (int)GETPID()); + remove(target); + remove(link); + + f = fopen(target, "wb"); + if (f == NULL) { + CHECK(0, "PathsRefEqual symlink fixture create target"); + } + else { + fclose(f); + if (symlink(target, link) != 0) { + SKIP("PathsRefEqual symlink alias: symlink() failed"); + } + else { + CHECK(wolfCLU_PathsRefEqual(target, link) == 1, + "PathsRefEqual symlink alias"); + remove(link); + } + remove(target); + } + } + + /* Two distinct names hard-linked to the same inode must likewise be + * caught: writing through either truncates the other's data in place. */ + { + char target[64]; + char hlink[64]; + + XSNPRINTF(target, sizeof(target), "test_ref_equal_htgt_%d.tmp", + (int)GETPID()); + XSNPRINTF(hlink, sizeof(hlink), "test_ref_equal_hlink_%d.tmp", + (int)GETPID()); + remove(target); + remove(hlink); + + f = fopen(target, "wb"); + if (f == NULL) { + CHECK(0, "PathsRefEqual hardlink fixture create target"); + } + else { + fclose(f); + if (link(target, hlink) != 0) { + SKIP("PathsRefEqual hardlink alias: link() failed"); + } + else { + CHECK(wolfCLU_PathsRefEqual(target, hlink) == 1, + "PathsRefEqual hardlink alias"); + remove(hlink); + } + remove(target); + } + } + + /* A path whose parent directory doesn't exist can't already be (or + * alias) an existing file, so this is a definite DISTINCT rather than + * an UNDETERMINED fail-closed -- the caller's subsequent open attempt + * is what should report the missing directory. */ + CHECK(wolfCLU_PathsRefEqual(relPath, "no_such_dir_xyz/out.tmp") == + WOLFCLU_PATHS_DISTINCT, + "PathsRefEqual nonexistent parent dir is distinct"); + + /* An oversized path can't be canonicalized at all; that ambiguity must + * still fail closed as UNDETERMINED. PATH_MAX is what the internal + * buffer sizes off of when defined; the fallback matches its own + * fallback so this stays oversized either way. */ + { +#ifdef PATH_MAX + char hugePath[PATH_MAX + 16]; +#else + char hugePath[4096 + 16]; +#endif + + XMEMSET(hugePath, 'a', sizeof(hugePath) - 1); + hugePath[sizeof(hugePath) - 1] = '\0'; + CHECK(wolfCLU_PathsRefEqual(relPath, hugePath) == + WOLFCLU_PATHS_UNDETERMINED, + "PathsRefEqual oversized path is undetermined"); + } +#endif /* !_WIN32 */ +} + +#ifndef _WIN32 +static void testOpenOutAndKeyFile(void) +{ + char target[64]; + char link[64]; + FILE* f; + struct stat st; + + XSNPRINTF(target, sizeof(target), "test_openfile_%d.tmp", (int)GETPID()); + XSNPRINTF(link, sizeof(link), "test_openlink_%d.tmp", (int)GETPID()); + remove(target); + remove(link); + + /* Non-secret output writes through a symlink and leaves it in place. */ + f = fopen(target, "wb"); + if (f == NULL) { + CHECK(0, "OpenOutFile fixture create target"); + return; + } + fclose(f); + if (symlink(target, link) != 0) { + /* Filesystem without symlink support; nothing to assert. */ + SKIP("OpenOutFile/OpenKeyFile symlink assertions: symlink() failed"); + } else { + f = wolfCLU_OpenOutFile(link); + CHECK(f != NULL, "OpenOutFile follows symlink"); + if (f != NULL) { + fputs("data", f); + fclose(f); + } + CHECK(lstat(link, &st) == 0 && S_ISLNK(st.st_mode), + "OpenOutFile leaves symlink intact"); + CHECK(stat(target, &st) == 0 && st.st_size == 4, + "OpenOutFile wrote through symlink"); + + /* Key output refuses the same symlink rather than following it. */ + f = wolfCLU_OpenKeyFile(link); + CHECK(f == NULL, "OpenKeyFile refuses symlink"); + if (f != NULL) { + fclose(f); + } + CHECK(lstat(link, &st) == 0 && S_ISLNK(st.st_mode), + "OpenKeyFile leaves symlink intact"); + CHECK(stat(target, &st) == 0 && st.st_size == 4, + "OpenKeyFile did not truncate symlink target"); + + remove(link); + } + + /* Key output re-tightens permissions on an existing loose file. */ + CHECK(chmod(target, 0666) == 0, "OpenKeyFile fixture chmod"); + f = wolfCLU_OpenKeyFile(target); + CHECK(f != NULL, "OpenKeyFile plain path"); + if (f != NULL) { + fclose(f); + CHECK(stat(target, &st) == 0 && + (st.st_mode & (S_IRWXG | S_IRWXO)) == 0, + "OpenKeyFile is owner-only"); + } + + remove(target); +} + +/* The refusals below are reachable only from C: the Python end-to-end tests + * cannot make wolfCLU aim a key write at a hard link or a FIFO. */ +static void testKeyFileRefusals(void) +{ + char target[64]; + char hard[64]; + char fifo[64]; + FILE* f; + struct stat st; + + XSNPRINTF(target, sizeof(target), "test_refuse_%d.tmp", (int)GETPID()); + XSNPRINTF(hard, sizeof(hard), "test_refuse_link_%d.tmp", (int)GETPID()); + XSNPRINTF(fifo, sizeof(fifo), "test_refuse_fifo_%d.tmp", (int)GETPID()); + remove(target); + remove(hard); + remove(fifo); + + /* A second hard link would keep the old key readable through the other + * name, so the write is refused with EMLINK. */ + f = fopen(target, "wb"); + if (f == NULL) { + CHECK(0, "KeyFileRefusals fixture create"); + return; + } + fputs("old key", f); + fclose(f); + + if (link(target, hard) == 0) { + /* errno is only asserted against wolfCLU_CreateSecureFile(), which + * returns without logging. wolfCLU_OpenKeyFile() calls + * wolfCLU_LogError() on the way out, and a library call is allowed + * to set errno even when it succeeds. */ + errno = 0; + f = wolfCLU_CreateSecureFile(target, "wb", 1); + CHECK(f == NULL, "CreateSecureFile refuses multiply linked file"); + CHECK(errno == EMLINK, "CreateSecureFile reports EMLINK"); + if (f != NULL) { + fclose(f); + } + f = wolfCLU_OpenKeyFile(target); + CHECK(f == NULL, "OpenKeyFile refuses multiply linked file"); + if (f != NULL) { + fclose(f); + } + CHECK(stat(target, &st) == 0 && st.st_size == 7, + "OpenKeyFile left the multiply linked file intact"); + remove(hard); + } + else { + SKIP("OpenKeyFile hard link refusal: link() failed"); + } + remove(target); + + /* A FIFO is not a regular file: refused with EEXIST, not followed. */ + if (mkfifo(fifo, 0600) == 0) { + errno = 0; + f = wolfCLU_CreateSecureFile(fifo, "wb", 1); + CHECK(f == NULL, "CreateSecureFile refuses FIFO"); + CHECK(errno == EEXIST, "CreateSecureFile reports EEXIST for FIFO"); + if (f != NULL) { + fclose(f); + } + f = wolfCLU_OpenKeyFile(fifo); + CHECK(f == NULL, "OpenKeyFile refuses FIFO"); + if (f != NULL) { + fclose(f); + } + CHECK(lstat(fifo, &st) == 0 && S_ISFIFO(st.st_mode), + "OpenKeyFile left the FIFO in place"); + remove(fifo); + } + else { + SKIP("OpenKeyFile FIFO refusal: mkfifo() failed"); + } +} + +static void testOpenExistingSecureFile(void) +{ + char target[64]; + char symLink[64]; + char hard[64]; + char missing[64]; + FILE* f; + struct stat st; + char buf[16]; + + XSNPRINTF(target, sizeof(target), "test_existing_%d.tmp", (int)GETPID()); + XSNPRINTF(symLink, sizeof(symLink), "test_existing_link_%d.tmp", + (int)GETPID()); + XSNPRINTF(hard, sizeof(hard), "test_existing_hard_%d.tmp", (int)GETPID()); + XSNPRINTF(missing, sizeof(missing), "test_existing_no_%d.tmp", + (int)GETPID()); + remove(target); + remove(symLink); + remove(hard); + remove(missing); + + /* A path that is not there is ENOENT, not a silent create. */ + errno = 0; + f = wolfCLU_OpenExistingSecureFile(missing, "rb+", 1); + CHECK(f == NULL, "OpenExistingSecureFile refuses missing path"); + CHECK(errno == ENOENT, "OpenExistingSecureFile reports ENOENT"); + CHECK(stat(missing, &st) != 0, + "OpenExistingSecureFile did not create the missing path"); + if (f != NULL) { + fclose(f); + } + + f = fopen(target, "wb"); + if (f == NULL) { + CHECK(0, "OpenExistingSecureFile fixture create"); + return; + } + fputs("keydata", f); + fclose(f); + + /* Group/other bits are cleared on the way in. */ + CHECK(chmod(target, 0666) == 0, "OpenExistingSecureFile fixture chmod"); + f = wolfCLU_OpenExistingSecureFile(target, "rb+", 1); + CHECK(f != NULL, "OpenExistingSecureFile opens regular file"); + if (f != NULL) { + CHECK(fread(buf, 1, 7, f) == 7, + "OpenExistingSecureFile did not truncate on rb+"); + fclose(f); + CHECK(stat(target, &st) == 0 && + (st.st_mode & (S_IRWXG | S_IRWXO)) == 0, + "OpenExistingSecureFile tightened to owner-only"); + } + + /* A symlink at the path is refused rather than followed. */ + if (symlink(target, symLink) == 0) { + errno = 0; + f = wolfCLU_OpenExistingSecureFile(symLink, "rb+", 1); + CHECK(f == NULL, "OpenExistingSecureFile refuses symlink"); + CHECK(errno == ELOOP, "OpenExistingSecureFile reports ELOOP"); + if (f != NULL) { + fclose(f); + } + remove(symLink); + } + else { + SKIP("OpenExistingSecureFile symlink refusal: symlink() failed"); + } + + /* A second hard link is refused, the same way it is at creation time. */ + if (link(target, hard) == 0) { + errno = 0; + f = wolfCLU_OpenExistingSecureFile(target, "rb+", 1); + CHECK(f == NULL, "OpenExistingSecureFile refuses hard-linked file"); + CHECK(errno == EMLINK, "OpenExistingSecureFile reports EMLINK"); + if (f != NULL) { + fclose(f); + } + /* ownerOnly clear is the read path, which does not care. */ + f = wolfCLU_OpenExistingSecureFile(target, "rb", 0); + CHECK(f != NULL, "OpenExistingSecureFile allows hard link without " + "ownerOnly"); + if (f != NULL) { + fclose(f); + } + remove(hard); + } + else { + SKIP("OpenExistingSecureFile hard link refusal: link() failed"); + } + + /* NULL path is rejected rather than dereferenced. */ + errno = 0; + f = wolfCLU_OpenExistingSecureFile(NULL, "rb", 1); + CHECK(f == NULL, "OpenExistingSecureFile refuses NULL path"); + CHECK(errno == EINVAL, "OpenExistingSecureFile reports EINVAL"); + if (f != NULL) { + fclose(f); + } + + remove(target); +} +/* wolfCLU_PathsRefEqual() is a point-in-time check, so -out can be swapped + * to alias -in between it and the open. wolfCLU_OpenOutFileDistinctFrom() + * is the backstop: it must catch the alias by file identity and, crucially, + * must not have truncated the input by the time it does. */ +static void testOpenOutFileDistinctFrom(void) +{ + char target[64]; + char hard[64]; + FILE* in; + FILE* out; + struct stat st; + + XSNPRINTF(target, sizeof(target), "test_distinct_%d.tmp", (int)GETPID()); + XSNPRINTF(hard, sizeof(hard), "test_distinct_hard_%d.tmp", (int)GETPID()); + remove(target); + remove(hard); + + in = fopen(target, "wb"); + if (in == NULL) { + CHECK(0, "OpenOutFileDistinctFrom: fixture create"); + return; + } + fwrite("survive", 1, 7, in); + fclose(in); + + if (link(target, hard) != 0) { + SKIP("OpenOutFileDistinctFrom alias: link() failed"); + } + else { + in = fopen(target, "rb"); + if (in == NULL) { + CHECK(0, "OpenOutFileDistinctFrom: reopen input"); + } + else { + /* Same inode reached by a different path string: exactly what + * a lost race would hand the open. */ + out = wolfCLU_OpenOutFileDistinctFrom(hard, in); + CHECK(out == NULL, + "OpenOutFileDistinctFrom refuses an aliasing -out"); + if (out != NULL) { + fclose(out); + } + CHECK(stat(target, &st) == 0 && st.st_size == 7, + "OpenOutFileDistinctFrom left the input untruncated"); + fclose(in); + } + remove(hard); + } + + /* A genuinely distinct -out must still open and truncate. */ + in = fopen(target, "rb"); + if (in == NULL) { + CHECK(0, "OpenOutFileDistinctFrom: reopen input for distinct case"); + } + else { + XSNPRINTF(hard, sizeof(hard), "test_distinct_out_%d.tmp", + (int)GETPID()); + remove(hard); + out = wolfCLU_OpenOutFileDistinctFrom(hard, in); + CHECK(out != NULL, "OpenOutFileDistinctFrom opens a distinct -out"); + if (out != NULL) { + fclose(out); + remove(hard); + } + fclose(in); + } + + /* A NULL input means there is nothing to alias. */ + XSNPRINTF(hard, sizeof(hard), "test_distinct_null_%d.tmp", (int)GETPID()); + remove(hard); + out = wolfCLU_OpenOutFileDistinctFrom(hard, NULL); + CHECK(out != NULL, "OpenOutFileDistinctFrom accepts a NULL input"); + if (out != NULL) { + fclose(out); + remove(hard); + } + + remove(target); +} + +#else /* _WIN32 */ + +/* Available since Windows 10 1703; older SDK headers may not define it. + * Without it CreateSymbolicLinkA() needs an elevated token, which a CI + * runner will not have - callers below treat that as SKIP, not FAIL. */ +#ifndef SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED +#define SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED 0x2 +#endif + +static void testOpenOutAndKeyFileWin(void) +{ + char target[64]; + char link[64]; + FILE* f; + WIN32_FILE_ATTRIBUTE_DATA fad; + + XSNPRINTF(target, sizeof(target), "test_openfile_%d.tmp", (int)GETPID()); + XSNPRINTF(link, sizeof(link), "test_openlink_%d.tmp", (int)GETPID()); + remove(target); + remove(link); + + /* Non-secret output writes through a reparse point and leaves it in + * place, exactly as wolfCLU_OpenKeyFile() below must not. */ + f = fopen(target, "wb"); + if (f == NULL) { + CHECK(0, "OpenOutFile fixture create target"); + return; + } + fclose(f); + + if (!CreateSymbolicLinkA(link, target, + SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED)) { + /* No Developer Mode / not elevated: nothing to assert. */ + SKIP("OpenOutFile/OpenKeyFile reparse point assertions: " + "CreateSymbolicLinkA() failed"); + } + else { + f = wolfCLU_OpenOutFile(link); + CHECK(f != NULL, "OpenOutFile follows reparse point"); + if (f != NULL) { + fputs("data", f); + fclose(f); + } + CHECK(GetFileAttributesExA(link, GetFileExInfoStandard, &fad) && + (fad.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0, + "OpenOutFile leaves reparse point intact"); + + /* Key output refuses the same reparse point rather than following + * it. */ + errno = 0; + f = wolfCLU_OpenKeyFile(link); + CHECK(f == NULL, "OpenKeyFile refuses reparse point"); + if (f != NULL) { + fclose(f); + } + CHECK(GetFileAttributesExA(link, GetFileExInfoStandard, &fad) && + (fad.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0, + "OpenKeyFile leaves reparse point intact"); + CHECK(GetFileAttributesExA(target, GetFileExInfoStandard, &fad) && + fad.nFileSizeLow == 4, + "OpenKeyFile did not truncate reparse point target"); + + remove(link); + } + + /* Plain path still opens normally. */ + f = wolfCLU_OpenKeyFile(target); + CHECK(f != NULL, "OpenKeyFile plain path"); + if (f != NULL) { + fclose(f); + } + + remove(target); +} + +/* The refusals below are reachable only from C: the Python end-to-end tests + * cannot make wolfCLU aim a key write at a hard-linked file. */ +static void testKeyFileRefusalsWin(void) +{ + char target[64]; + char hard[64]; + FILE* f; + WIN32_FILE_ATTRIBUTE_DATA fad; + + XSNPRINTF(target, sizeof(target), "test_refuse_%d.tmp", (int)GETPID()); + XSNPRINTF(hard, sizeof(hard), "test_refuse_link_%d.tmp", (int)GETPID()); + remove(target); + remove(hard); + + /* A second hard link would keep the old key readable through the other + * name, so the write is refused with EMLINK. */ + f = fopen(target, "wb"); + if (f == NULL) { + CHECK(0, "KeyFileRefusals fixture create"); + return; + } + fputs("old key", f); + fclose(f); + + if (!CreateHardLinkA(hard, target, NULL)) { + SKIP("OpenKeyFile hard link refusal: CreateHardLinkA() failed"); + } + else { + errno = 0; + f = wolfCLU_CreateSecureFile(target, "wb", 1); + CHECK(f == NULL, "CreateSecureFile refuses multiply linked file"); + CHECK(errno == EMLINK, "CreateSecureFile reports EMLINK"); + if (f != NULL) { + fclose(f); + } + f = wolfCLU_OpenKeyFile(target); + CHECK(f == NULL, "OpenKeyFile refuses multiply linked file"); + if (f != NULL) { + fclose(f); + } + CHECK(GetFileAttributesExA(target, GetFileExInfoStandard, &fad) && + fad.nFileSizeLow == 7, + "OpenKeyFile left the multiply linked file intact"); + remove(hard); + } + remove(target); +} + +static void testOpenExistingSecureFileWin(void) +{ + char target[64]; + char symLink[64]; + char hard[64]; + char missing[64]; + FILE* f; + char buf[16]; + + XSNPRINTF(target, sizeof(target), "test_existing_%d.tmp", (int)GETPID()); + XSNPRINTF(symLink, sizeof(symLink), "test_existing_link_%d.tmp", + (int)GETPID()); + XSNPRINTF(hard, sizeof(hard), "test_existing_hard_%d.tmp", (int)GETPID()); + XSNPRINTF(missing, sizeof(missing), "test_existing_no_%d.tmp", + (int)GETPID()); + remove(target); + remove(symLink); + remove(hard); + remove(missing); + + /* A path that is not there is ENOENT, not a silent create. */ + errno = 0; + f = wolfCLU_OpenExistingSecureFile(missing, "rb+", 1); + CHECK(f == NULL, "OpenExistingSecureFile refuses missing path"); + CHECK(errno == ENOENT, "OpenExistingSecureFile reports ENOENT"); + CHECK(GetFileAttributesA(missing) == INVALID_FILE_ATTRIBUTES, + "OpenExistingSecureFile did not create the missing path"); + if (f != NULL) { + fclose(f); + } + + f = fopen(target, "wb"); + if (f == NULL) { + CHECK(0, "OpenExistingSecureFile fixture create"); + return; + } + fputs("keydata", f); + fclose(f); + + f = wolfCLU_OpenExistingSecureFile(target, "rb+", 1); + CHECK(f != NULL, "OpenExistingSecureFile opens regular file"); + if (f != NULL) { + CHECK(fread(buf, 1, 7, f) == 7, + "OpenExistingSecureFile did not truncate on rb+"); + fclose(f); + } + + /* A reparse point at the path is refused rather than followed. */ + if (!CreateSymbolicLinkA(symLink, target, + SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED)) { + SKIP("OpenExistingSecureFile reparse point refusal: " + "CreateSymbolicLinkA() failed"); + } + else { + errno = 0; + f = wolfCLU_OpenExistingSecureFile(symLink, "rb+", 1); + CHECK(f == NULL, "OpenExistingSecureFile refuses reparse point"); + CHECK(errno == ELOOP, "OpenExistingSecureFile reports ELOOP"); + if (f != NULL) { + fclose(f); + } + remove(symLink); + } + + /* A second hard link is refused, the same way it is at creation time. */ + if (!CreateHardLinkA(hard, target, NULL)) { + SKIP("OpenExistingSecureFile hard link refusal: " + "CreateHardLinkA() failed"); + } + else { + errno = 0; + f = wolfCLU_OpenExistingSecureFile(target, "rb+", 1); + CHECK(f == NULL, "OpenExistingSecureFile refuses hard-linked file"); + CHECK(errno == EMLINK, "OpenExistingSecureFile reports EMLINK"); + if (f != NULL) { + fclose(f); + } + /* ownerOnly clear is the read path, which does not care. */ + f = wolfCLU_OpenExistingSecureFile(target, "rb", 0); + CHECK(f != NULL, "OpenExistingSecureFile allows hard link without " + "ownerOnly"); + if (f != NULL) { + fclose(f); + } + remove(hard); + } + + /* NULL path is rejected rather than dereferenced. */ + errno = 0; + f = wolfCLU_OpenExistingSecureFile(NULL, "rb", 1); + CHECK(f == NULL, "OpenExistingSecureFile refuses NULL path"); + CHECK(errno == EINVAL, "OpenExistingSecureFile reports EINVAL"); + if (f != NULL) { + fclose(f); + } + + remove(target); +} +#endif /* !_WIN32 */ + +/* The BIO wrappers hand the FILE* to wolfSSL with BIO_CLOSE, so freeing the + * BIO must close the underlying file rather than leak it. */ +static void testSecureFileBios(void) +{ + char path[64]; + WOLFSSL_BIO* bio; + struct stat st; + + XSNPRINTF(path, sizeof(path), "test_bio_%d.tmp", (int)GETPID()); + remove(path); + + bio = wolfCLU_OpenOutFileBio(path); + CHECK(bio != NULL, "OpenOutFileBio opens"); + if (bio != NULL) { + CHECK(wolfSSL_BIO_write(bio, "bio", 3) == 3, "OpenOutFileBio writes"); + wolfSSL_BIO_free(bio); + /* If BIO_free did not close the FILE*, the data would still be + * sitting in the stdio buffer and the file would be short. */ + CHECK(stat(path, &st) == 0 && st.st_size == 3, + "OpenOutFileBio flushed and closed on BIO_free"); + } + remove(path); + + bio = wolfCLU_OpenKeyFileBio(path); + CHECK(bio != NULL, "OpenKeyFileBio opens"); + if (bio != NULL) { + CHECK(wolfSSL_BIO_write(bio, "key", 3) == 3, "OpenKeyFileBio writes"); + wolfSSL_BIO_free(bio); + CHECK(stat(path, &st) == 0 && st.st_size == 3, + "OpenKeyFileBio flushed and closed on BIO_free"); +#ifndef _WIN32 + CHECK(stat(path, &st) == 0 && (st.st_mode & (S_IRWXG | S_IRWXO)) == 0, + "OpenKeyFileBio is owner-only"); +#endif + } + remove(path); + + /* isSecret picks the key variant, which is the owner-only one. */ + bio = wolfCLU_OpenOutOrKeyFileBio(path, 1); + CHECK(bio != NULL, "OpenOutOrKeyFileBio opens"); + if (bio != NULL) { + wolfSSL_BIO_free(bio); +#ifndef _WIN32 + CHECK(stat(path, &st) == 0 && (st.st_mode & (S_IRWXG | S_IRWXO)) == 0, + "OpenOutOrKeyFileBio(isSecret=1) is owner-only"); +#endif + } + remove(path); + + /* isSecret=0 opens as a regular output file. */ + bio = wolfCLU_OpenOutOrKeyFileBio(path, 0); + CHECK(bio != NULL, "OpenOutOrKeyFileBio(isSecret=0) opens"); + if (bio != NULL) { + wolfSSL_BIO_free(bio); +#ifndef _WIN32 + CHECK(stat(path, &st) == 0, + "OpenOutOrKeyFileBio(isSecret=0) stat"); +#endif + } + remove(path); +} + +static void testDerSetLength(void) +{ + byte out[8]; + word32 sz; + + /* size-only mode (output == NULL) */ + CHECK(wolfCLU_DerSetLength(0, NULL) == 1, "DerSetLength size-only 0"); + CHECK(wolfCLU_DerSetLength(127, NULL) == 1, "DerSetLength size-only 127"); + CHECK(wolfCLU_DerSetLength(128, NULL) == 2, "DerSetLength size-only 128"); + CHECK(wolfCLU_DerSetLength(255, NULL) == 2, "DerSetLength size-only 255"); + CHECK(wolfCLU_DerSetLength(256, NULL) == 3, "DerSetLength size-only 256"); + CHECK(wolfCLU_DerSetLength(65535, NULL) == 3, + "DerSetLength size-only 65535"); + CHECK(wolfCLU_DerSetLength(65536, NULL) == 4, + "DerSetLength size-only 65536"); + CHECK(wolfCLU_DerSetLength(0xFFFFFF, NULL) == 4, + "DerSetLength size-only 0xFFFFFF"); + CHECK(wolfCLU_DerSetLength(0x1000000, NULL) == 5, + "DerSetLength size-only 0x1000000"); + CHECK(wolfCLU_DerSetLength(0xFFFFFFFF, NULL) == 5, + "DerSetLength size-only 0xFFFFFFFF"); + + /* short-form: length < 0x80 encodes as a single byte */ + XMEMSET(out, 0, sizeof(out)); + sz = wolfCLU_DerSetLength(0, out); + CHECK(sz == 1 && out[0] == 0x00, "DerSetLength encode 0"); + + XMEMSET(out, 0, sizeof(out)); + sz = wolfCLU_DerSetLength(127, out); + CHECK(sz == 1 && out[0] == 0x7F, "DerSetLength encode 127"); + + /* long-form boundary: 128 requires 0x81 0x80 */ + XMEMSET(out, 0, sizeof(out)); + sz = wolfCLU_DerSetLength(128, out); + CHECK(sz == 2 && out[0] == 0x81 && out[1] == 0x80, + "DerSetLength encode 128"); + + XMEMSET(out, 0, sizeof(out)); + sz = wolfCLU_DerSetLength(255, out); + CHECK(sz == 2 && out[0] == 0x81 && out[1] == 0xFF, + "DerSetLength encode 255"); + + /* long-form boundary: 256 requires 0x82 0x01 0x00 */ + XMEMSET(out, 0, sizeof(out)); + sz = wolfCLU_DerSetLength(256, out); + CHECK(sz == 3 && out[0] == 0x82 && out[1] == 0x01 && out[2] == 0x00, + "DerSetLength encode 256"); + + XMEMSET(out, 0, sizeof(out)); + sz = wolfCLU_DerSetLength(65535, out); + CHECK(sz == 3 && out[0] == 0x82 && out[1] == 0xFF && out[2] == 0xFF, + "DerSetLength encode 65535"); + + /* long-form boundary: 65536 requires 0x83 0x01 0x00 0x00 */ + XMEMSET(out, 0, sizeof(out)); + sz = wolfCLU_DerSetLength(65536, out); + CHECK(sz == 4 && out[0] == 0x83 && out[1] == 0x01 && + out[2] == 0x00 && out[3] == 0x00, "DerSetLength encode 65536"); + + /* This is the boundary the BytePrecisionCopy()-based encoder this + * function replaced used to under-count: 3-byte lengths need a 4-byte + * long form (0x83 + 3 length bytes). */ + XMEMSET(out, 0, sizeof(out)); + sz = wolfCLU_DerSetLength(0xFFFFFF, out); + CHECK(sz == 4 && out[0] == 0x83 && out[1] == 0xFF && out[2] == 0xFF && + out[3] == 0xFF, "DerSetLength encode 0xFFFFFF"); + + /* long-form boundary: 0x1000000 requires 5 bytes: 0x84 0x01 0x00 0x00 + * 0x00 */ + XMEMSET(out, 0, sizeof(out)); + sz = wolfCLU_DerSetLength(0x1000000, out); + CHECK(sz == 5 && out[0] == 0x84 && out[1] == 0x01 && out[2] == 0x00 && + out[3] == 0x00 && out[4] == 0x00, "DerSetLength encode 0x1000000"); + + /* largest word32 length: 0x84 0xFF 0xFF 0xFF 0xFF */ + XMEMSET(out, 0, sizeof(out)); + sz = wolfCLU_DerSetLength(0xFFFFFFFF, out); + CHECK(sz == 5 && out[0] == 0x84 && out[1] == 0xFF && out[2] == 0xFF && + out[3] == 0xFF && out[4] == 0xFF, + "DerSetLength encode 0xFFFFFFFF"); +} + + +/* Every CHECK() in some fixtures (e.g. testKeyFileRefusals(), whose EMLINK/ + * FIFO-refusal coverage only runs when link()/mkfifo() succeed) is nested + * inside an environment-dependent branch, with SKIP() on the other side. + * `checked` is one process-wide counter, so a fixture that hit 0 CHECK()s + * is otherwise invisible: main()'s overall pass/skip decision is masked by + * unrelated fixtures that always assert. Make that visible per-fixture. */ +static void runFixture(const char* name, void (*fn)(void)) +{ + int before = checked; + fn(); + if (checked == before) { + printf("WARN: %s contributed 0 assertions (environment may lack " + "the feature it tests)\n", name); + } +} + +int main(void) +{ + /* The BIO wrappers and every wolfCLU_LogError() path reach into the + * wolfSSL compat layer, which src/clu_main.c brackets the same way. */ + if (wolfSSL_Init() != WOLFSSL_SUCCESS) { + printf("FAIL: wolfSSL_Init\n"); + return 1; + } + + runFixture("testReadFileToBuffer", testReadFileToBuffer); + runFixture("testPathsRefEqual", testPathsRefEqual); +#ifndef _WIN32 + runFixture("testOpenOutAndKeyFile", testOpenOutAndKeyFile); + runFixture("testKeyFileRefusals", testKeyFileRefusals); + runFixture("testOpenExistingSecureFile", testOpenExistingSecureFile); + runFixture("testOpenOutFileDistinctFrom", testOpenOutFileDistinctFrom); +#else + runFixture("testOpenOutAndKeyFileWin", testOpenOutAndKeyFileWin); + runFixture("testKeyFileRefusalsWin", testKeyFileRefusalsWin); + runFixture("testOpenExistingSecureFileWin", testOpenExistingSecureFileWin); +#endif + runFixture("testSecureFileBios", testSecureFileBios); + runFixture("testDerSetLength", testDerSetLength); + + wolfSSL_Cleanup(); + + if (fail == 0) { + if (skipped > 0) { + printf("All tools_unit_test tests passed (%d skipped).\n", skipped); + } else { + printf("All tools_unit_test tests passed.\n"); + } + } + else { + printf("%d tools_unit_test test(s) FAILED.\n", fail); + } + + /* SKIP only when nothing ran at all; a partial run (some fixtures + * unbuildable, everything else passed) is still a PASS. */ + return fail ? 1 : (checked > 0 ? 0 : 77); +} + +#else /* WOLFCLU_NO_FILESYSTEM */ + +int main(void) +{ + printf("tools_unit_test skipped: built with --disable-filesystem.\n"); + return 77; /* automake SKIP */ +} + +#endif /* !WOLFCLU_NO_FILESYSTEM */ diff --git a/tests/wolfclu_test.py b/tests/wolfclu_test.py index d49e4ada..3b30f8b6 100644 --- a/tests/wolfclu_test.py +++ b/tests/wolfclu_test.py @@ -72,15 +72,22 @@ def _find_certs_dir(): CERTS_DIR = _find_certs_dir() -def run_wolfssl(*args, stdin_data=None, timeout=60): +def run_wolfssl(*args, stdin_data=None, timeout=60, stdout=None): """Run the wolfssl binary with the given arguments. Returns a CompletedProcess instance. A default timeout of 60 seconds prevents indefinite hangs in CI. Network-facing tests (s_client, ocsp) manage their own timeouts. + Pass stdout (an open file) to redirect the child's stdout instead of + capturing it; the returned .stdout is None in that case. """ cmd = [WOLFSSL_BIN] + list(args) - kwargs = dict(capture_output=True, text=True, timeout=timeout) + kwargs = dict(text=True, timeout=timeout) + if stdout is not None: + kwargs["stdout"] = stdout + kwargs["stderr"] = subprocess.PIPE + else: + kwargs["capture_output"] = True if stdin_data is not None: kwargs["input"] = stdin_data else: @@ -88,6 +95,32 @@ def run_wolfssl(*args, stdin_data=None, timeout=60): return subprocess.run(cmd, **kwargs) +_NO_FILESYSTEM = None + + +def no_filesystem(): + """True when the build under test was configured --disable-filesystem, + in which case every file-backed subcommand refuses to run. + + Use as a class decorator: + @unittest.skipIf(no_filesystem(), "filesystem support disabled") + """ + global _NO_FILESYSTEM + if _NO_FILESYSTEM is None: + _NO_FILESYSTEM = False + config_log = os.path.join(".", "config.log") + if os.path.isfile(config_log): + with open(config_log, "r") as f: + _NO_FILESYSTEM = "disable-filesystem" in f.read() + return _NO_FILESYSTEM + + +def skip_if_no_filesystem(): + """no_filesystem() as a SkipTest. Call from setUpClass.""" + if no_filesystem(): + raise unittest.SkipTest("filesystem support disabled") + + def is_fips(): """True when linked against a FIPS wolfSSL build (per `wolfssl -v`).""" r = run_wolfssl("-v") diff --git a/tests/x509/CRL-verify-test.py b/tests/x509/CRL-verify-test.py index f2ed613a..ae1bc50b 100644 --- a/tests/x509/CRL-verify-test.py +++ b/tests/x509/CRL-verify-test.py @@ -6,7 +6,7 @@ import unittest sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -from wolfclu_test import CERTS_DIR, run_wolfssl, test_main +from wolfclu_test import no_filesystem, CERTS_DIR, run_wolfssl, test_main def _has_crl(): @@ -33,6 +33,7 @@ def _cleanup(*files): os.remove(f) +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestCRLVerify(unittest.TestCase): """CRL verification tests.""" @@ -150,6 +151,7 @@ def test_crl_invalid_outform_error_message(self): combined)) +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestCRLText(unittest.TestCase): """CRL -text output tests.""" diff --git a/tests/x509/x509-ca-test.py b/tests/x509/x509-ca-test.py index 514d00d7..23aecaee 100644 --- a/tests/x509/x509-ca-test.py +++ b/tests/x509/x509-ca-test.py @@ -7,7 +7,9 @@ import unittest sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -from wolfclu_test import WOLFSSL_BIN, CERTS_DIR, run_wolfssl, test_main +from wolfclu_test import ( + no_filesystem, WOLFSSL_BIN, CERTS_DIR, run_wolfssl, test_main +) # Use absolute forward-slash paths so wolfSSL recognizes them as absolute. # Temporary artefacts go under the build directory (CWD under automake), @@ -211,6 +213,7 @@ def _has_altextend(): return "altextend" in combined +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestCAHelp(unittest.TestCase): """ca -h and -help should succeed.""" @@ -224,6 +227,7 @@ def test_ca_help(self): +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestCASelfSign(unittest.TestCase): """ca -selfsign tests.""" @@ -308,6 +312,7 @@ def test_selfsign_verify_fails_wrong_ca(self): +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestCACreateAndVerify(unittest.TestCase): """ca certificate creation and verification.""" @@ -349,6 +354,7 @@ def test_create_and_verify(self): +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestCAOverrideConfig(unittest.TestCase): """Override config options with command-line flags.""" @@ -392,7 +398,7 @@ def test_override_extensions_md_days_cert_keyfile(self): self.assertEqual(r.returncode, 0, r.stderr) - +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestCAKeyMismatch(unittest.TestCase): """ca with mismatched key should fail.""" @@ -427,6 +433,7 @@ def test_key_mismatch(self): +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestCAUniqueSubjectAndSerial(unittest.TestCase): """unique_subject enforcement and serial number handling.""" @@ -547,6 +554,7 @@ def test_rand_file_changes(self): +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestCAPolicy(unittest.TestCase): """Policy section enforcement.""" @@ -646,6 +654,7 @@ def test_common_name_mismatch_fails(self): +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestCAChimera(unittest.TestCase): """Chimera certificate (altextend) tests.""" @@ -729,6 +738,7 @@ def test_chimera_cert(self): +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestCAOutdirPath(unittest.TestCase): """Test path concatenation for -out with new_certs_dir.""" diff --git a/tests/x509/x509-process-test.py b/tests/x509/x509-process-test.py index 3e63ed86..64d84d35 100644 --- a/tests/x509/x509-process-test.py +++ b/tests/x509/x509-process-test.py @@ -8,7 +8,9 @@ import unittest sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -from wolfclu_test import WOLFSSL_BIN, CERTS_DIR, run_wolfssl, test_main +from wolfclu_test import ( + no_filesystem, WOLFSSL_BIN, CERTS_DIR, run_wolfssl, test_main +) TESTS_X509_DIR = os.path.dirname(os.path.abspath(__file__)) HAS_OPENSSL = shutil.which("openssl") is not None @@ -84,6 +86,7 @@ def _cleanup(*files): os.remove(f) +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestX509ProcessValid(unittest.TestCase): """run1: valid PEM/DER format conversions and combined file handling.""" @@ -235,6 +238,7 @@ def test_1i_combined_pem(self): "combined PEM output differs from original") +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestX509ProcessInvalidInput(unittest.TestCase): """run2: invalid argument combinations should fail.""" @@ -292,6 +296,7 @@ def test_2p_outform_noout(self): self._fail("-outform", "-noout") +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestX509ProcessValidFiles(unittest.TestCase): """run3: valid input file operations and field extraction.""" @@ -448,6 +453,7 @@ def test_3l_email_from_generated_cert(self): self.assertEqual(r.returncode, 0, r.stderr) +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestX509ProcessInvalidFiles(unittest.TestCase): """run4: invalid input files should fail.""" @@ -504,6 +510,7 @@ def test_4f_nonexistent_file_pem(self): self.assertNotEqual(r.returncode, 0) +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestMalformedArguments(unittest.TestCase): """ Regression: for malformed arguments """ @@ -518,6 +525,7 @@ def test_5a_malformed_subj_argument(self): self.assertGreater(len(r.stderr), 0) +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestX509ModulusNoout(unittest.TestCase): """Regression: x509 -modulus -noout must not crash.""" diff --git a/tests/x509/x509-req-test.py b/tests/x509/x509-req-test.py index 772a7bef..e9839b43 100644 --- a/tests/x509/x509-req-test.py +++ b/tests/x509/x509-req-test.py @@ -9,7 +9,10 @@ import unittest sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -from wolfclu_test import WOLFSSL_BIN, CERTS_DIR, is_fips, run_wolfssl, test_main +from wolfclu_test import ( + no_filesystem, WOLFSSL_BIN, CERTS_DIR, is_fips, run_wolfssl, + test_main +) def _tmp(name): @@ -115,6 +118,7 @@ def _flip_last_der_byte(src, dst): f.write(data) +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestReqNew(unittest.TestCase): """Test req -new with various options.""" @@ -135,6 +139,7 @@ def _clean(self, *files): for f in files: self.addCleanup(lambda p=f: _cleanup(p)) + def test_req_new_with_subj(self): """req -new -subj creates cert with correct subject.""" tmp = _tmp("test_req_subj.cert") @@ -479,6 +484,7 @@ def test_req_addext_unsupported_alt_type_fails(self): "test_req_addext_badtype.crt") +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestReqPemDerRoundTrip(unittest.TestCase): """Test PEM <-> DER round-trip for CSR.""" @@ -522,6 +528,7 @@ def test_pem_to_der_to_pem(self): "PEM -> DER -> PEM round-trip mismatch") +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestReqVerify(unittest.TestCase): """Test req -verify, including that a tampered CSR fails (F-5363).""" @@ -581,6 +588,7 @@ def test_verify_tampered_csr_no_output(self): self.assertNotIn("BEGIN CERTIFICATE REQUEST", r.stdout) +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestX509ReqSign(unittest.TestCase): """Test x509 -req -signkey signing.""" @@ -638,6 +646,7 @@ def test_x509_req_signkey_succeeds(self): self.assertEqual(r.returncode, 0, r.stderr) +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestX509ReqHashAlgorithms(unittest.TestCase): """Test hash algorithm options for x509 -req.""" @@ -708,6 +717,7 @@ def test_sha224_sig_algorithm(self): +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestX509ReqExtensions(unittest.TestCase): """Test extensions from config file for x509 -req.""" @@ -752,6 +762,7 @@ def test_extfile_v3_alt_ca(self): +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestReqConfigSubject(unittest.TestCase): """Test subject from config file.""" @@ -784,6 +795,7 @@ def test_subject_from_config(self): "Got: {!r}".format(subject_line)) +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestReqDefaultBasicConstraints(unittest.TestCase): """Test default basic constraints extension.""" @@ -807,6 +819,7 @@ def test_default_ca_true(self): self.assertIn("CA:TRUE", r2.stdout) +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestReqFIPS(unittest.TestCase): """FIPS-conditional tests.""" @@ -869,6 +882,7 @@ def test_newkey_with_passout_keyout(self): +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestReqHashAndKeyAlgos(unittest.TestCase): """Test hash and key algorithm options for req.""" @@ -918,6 +932,7 @@ def test_sha512(self): +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestReqAltNamesFullSkip(unittest.TestCase): """Test full alt_names extension with skipped indices.""" @@ -954,6 +969,7 @@ def test_v3_alt_req_full_tenthname(self): +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestReqPromptValidation(unittest.TestCase): """Test prompt-based config validation.""" @@ -994,6 +1010,7 @@ def test_long_country_code_fails(self): self.assertNotEqual(r.returncode, 0) +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestReqCSRAttributes(unittest.TestCase): """Test CSR attribute printing.""" @@ -1023,6 +1040,7 @@ def test_unsupported_attributes_fail(self): "CSR with unsupported attributes should fail") +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestReqCSRVersion(unittest.TestCase): """Test CSR version number.""" @@ -1117,6 +1135,7 @@ def test_csr_version_openssl_interop(self): """ +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestReqKeyUsageAbbrev(unittest.TestCase): """Regression: abbreviated keyUsage names must not be accepted.""" @@ -1143,6 +1162,7 @@ def test_abbreviated_ku_rejected(self): "Abbreviated keyUsage 'd' should not match digitalSignature") +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestReqChallengePassword(unittest.TestCase): """req config with challengePassword attribute must succeed.""" diff --git a/tests/x509/x509-verify-test.py b/tests/x509/x509-verify-test.py index 2a92749e..220badd3 100644 --- a/tests/x509/x509-verify-test.py +++ b/tests/x509/x509-verify-test.py @@ -6,7 +6,7 @@ import unittest sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -from wolfclu_test import CERTS_DIR, run_wolfssl, test_main +from wolfclu_test import no_filesystem, CERTS_DIR, run_wolfssl, test_main def _has_crl(): @@ -19,6 +19,7 @@ def _has_crl(): return "recompile wolfSSL with CRL" not in combined +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestX509Verify(unittest.TestCase): """Certificate verification tests.""" @@ -125,6 +126,7 @@ def test_partial_chain_no_cafile_no_crash(self): # require a normal exit code regardless of verify success/failure. self.assertGreaterEqual(r.returncode, 0, r.stderr) +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestX509VerifyCRL(unittest.TestCase): """CRL-related verification tests.""" @@ -161,6 +163,7 @@ def test_crl_check_revoked_fails(self): self.assertNotEqual(r.returncode, 0) +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestX509VerifyChain(unittest.TestCase): """Certificate chain verification tests.""" diff --git a/wolfclu/clu_header_main.h b/wolfclu/clu_header_main.h index b3fa9c93..07ab00e8 100644 --- a/wolfclu/clu_header_main.h +++ b/wolfclu/clu_header_main.h @@ -117,9 +117,9 @@ extern "C" { #define MEGABYTE (1024*1024) #define KILOBYTE 1024 #ifdef FREERTOS - #define BYTE_UNIT KILOBYTE + #define BYTE_UNIT KILOBYTE #else - #define BYTE_UNIT MEGABYTE + #define BYTE_UNIT MEGABYTE #endif #define MAX_TERM_WIDTH 80 #define MAX_THREADS 64 @@ -443,8 +443,8 @@ int wolfCLU_streamHashBio(WOLFSSL_BIO* bioIn, enum wc_HashType hashType, * @param alg hash type to use (converted to EVP type) * @param in input BIO to read data from in MAX_IO_CHUNK_SZ chunks * @param out buffer to output digest to - * @param outSz On entry, capacity of out; on success, updated to number of - * bytes written to out. + * @param outSz On entry, capacity of out; on success, updated to number + * of bytes written to out. */ int wolfCLU_hmacHash(WOLFSSL_HMAC_CTX *ctx, void* key, word32 keyLen, enum wc_HashType alg, WOLFSSL_BIO* in, byte* out, word32* outSz); @@ -614,6 +614,206 @@ int wolfCLU_PKCS12(int argc, char** argv); */ void wolfCLU_ForceZero(void* mem, unsigned int len); +/** + * @brief DER definite-length encoder. Returns the encoded length in bytes. + * With output NULL nothing is written and only that size is returned, + * which is how callers size a buffer before encoding into it. + */ +word32 wolfCLU_DerSetLength(word32 length, byte* output); + +/* + * These helpers deliberately work in terms of FILE* and POSIX/Win32 file + * descriptors rather than wolfSSL's XFILE/XFOPEN porting macros: the + * permission and symlink guarantees they exist to provide have no equivalent + * in that abstraction. They are consequently declared and compiled only when + * a stdio filesystem is available, i.e. not when WOLFCLU_NO_FILESYSTEM is + * set. The results are assignable to XFILE only where XFILE is FILE*. + */ +#ifndef WOLFCLU_NO_FILESYSTEM + +/** + * @brief Read the whole of path into a newly allocated buffer. + * + * Returns WOLFCLU_SUCCESS, BAD_FUNC_ARG for a NULL argument or maxSz <= 0, + * MEMORY_E if the buffer cannot be allocated, or WOLFCLU_FATAL_ERROR when + * path cannot be opened, sized or read, or is empty or larger than maxSz. + * + * On success *outSz is the file size and *outBuf is an allocation of + * *outSz + 1 bytes whose trailing byte is a NUL, so the contents can be + * handed straight to a parser that expects a C string. The caller owns + * that allocation and frees it with + * XFREE(*outBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER). + * Neither output is written on failure. + */ +int wolfCLU_ReadFileToBuffer(const char* path, long maxSz, byte** outBuf, + int* outSz); + +/** + * @brief Same as wolfCLU_ReadFileToBuffer(), but a zero-length file is + * success rather than WOLFCLU_FATAL_ERROR. Use this for a message/digest + * being verified or signed, which some algorithms accept empty; never use + * it for a key or signature file, which a 0-byte file can never validly be. + */ +int wolfCLU_ReadMessageFileToBuffer(const char* path, long maxSz, + byte** outBuf, int* outSz); + +/** + * @brief Open path for writing. + * + * mode is a stdio mode string and is honoured identically on every platform: + * "wb" truncates, "ab" appends, "rb+" updates in place without truncating. + * + * With ownerOnly set, path is kept as a 0600 regular file owned by the + * caller, and a symlink, non-regular file, foreign-owned file or multiply + * linked file is refused (errno ELOOP, EEXIST, EPERM or EMLINK) rather than + * written to. A refused or failed open never destroys what path already + * names. With ownerOnly clear this behaves like fopen(path, mode), so + * symlinks and special files are valid targets. + */ +FILE* wolfCLU_CreateSecureFile(const char* path, const char* mode, + int ownerOnly); + +/** + * @brief Open an existing path for in-place update, refusing to follow a + * symlink. Reports ENOENT when path does not exist, ELOOP for a + * symlink and EEXIST for any other non-regular target. With ownerOnly + * set the file must be owned by the caller and singly linked (EPERM, + * EMLINK), and its group/other access is dropped before any write. + */ +FILE* wolfCLU_OpenExistingSecureFile(const char* path, const char* mode, + int ownerOnly); + +/** + * @brief Report why a key-file open was refused, given the errno it set. + * wolfCLU_OpenExistingSecureFile() logs nothing itself, so callers + * that use it directly must call this to avoid failing silently. + */ +void wolfCLU_LogKeyOpenFailure(const char* path, int err); + +/** + * @brief Open path for writing key material, with owner-only permissions. + * Refuses (and logs) rather than writing through a symlink. + */ +FILE* wolfCLU_OpenKeyFile(const char* path); + +/** + * @brief Open an owner-only key file for in-place, repeated update: + * preserves the existing file's contents and identity if path + * already exists (via wolfCLU_OpenExistingSecureFile(path, "rb+", 1)), + * or creates it securely on first use if it doesn't + * (via wolfCLU_OpenKeyFile()). For a caller that rewrites the same + * key file repeatedly (e.g. a signing state updated after every + * operation) and must neither truncate an existing file nor fail + * just because this is the first write. Logs its own failure on + * every path. + */ +FILE* wolfCLU_OpenSecureFileForUpdate(const char* path); + +/** + * @brief Open path for writing non-secret output, with default permissions. + */ +FILE* wolfCLU_OpenOutFile(const char* path); + +/** + * @brief Close an output file opened for writing, reporting a flush + * failure (e.g. ENOSPC/EIO) that would otherwise surface only as a + * silently truncated file. No-op (returns WOLFCLU_SUCCESS) if file + * is NULL, so callers can call this unconditionally on cleanup. + */ +int wolfCLU_CloseOutFile(FILE* file, const char* path); + +/* Whether an output file will hold key material. The hardening in + * wolfCLU_CreateSecureFile() only applies to WOLFCLU_OUT_SECRET, so this is + * the single point where that protection is switched on or off. It is an + * enum rather than an int because the deciding expression differs per tool + * (-genkey, !-pubout, -nokeys) and its polarity is not self-evident at the + * call site. */ +typedef enum { + WOLFCLU_OUT_PUBLIC = 0, /* default permissions, symlinks followed */ + WOLFCLU_OUT_SECRET = 1 /* owner-only, symlinks and aliases refused */ +} WOLFCLU_OUT_KIND; + +/** + * @brief Same as wolfCLU_OpenOutFile(), but proves the opened file is not + * inFile before truncating it, closing the window between a + * wolfCLU_PathsRefEqual() check and the open. inFile must already be + * open; pass NULL to fall back to wolfCLU_OpenOutFile(). Refuses and + * logs if the two turn out to be the same file, leaving it intact. + * On Windows no fd-level check is performed. + */ +FILE* wolfCLU_OpenOutFileDistinctFrom(const char* path, FILE* inFile); + +/* wolfCLU_PathsRefEqual() return values. WOLFCLU_PATHS_DISTINCT is the only + * value meaning the paths are provably different; both non-zero values + * refuse an overwrite-in-place, but WOLFCLU_PATHS_UNDETERMINED lets the + * caller report the real reason (e.g. an unresolvable -out directory) + * instead of claiming -in and -out name the same file. */ +#define WOLFCLU_PATHS_DISTINCT 0 +#define WOLFCLU_PATHS_SAME 1 +#define WOLFCLU_PATHS_UNDETERMINED 2 + +/** + * @brief Check if two path strings name (or might name) the same file. + * Returns WOLFCLU_PATHS_DISTINCT only when they are provably + * distinct, WOLFCLU_PATHS_SAME when they provably name the same + * file, and WOLFCLU_PATHS_UNDETERMINED when the comparison was + * inconclusive (for example a path whose parent directory cannot be + * canonicalized) - fails closed (still refuses an overwrite) without + * claiming a definite match. + * + * This is a point-in-time check: nothing stops -out from being + * replaced (e.g. with a symlink to -in) between this call and the + * later open. Closing that TOCTOU window would mean deferring + * -out's truncation until after an fd-level identity check against + * -in, which touches every caller of wolfCLU_OpenOutFile(), not + * just the ones calling this function. Accepted: it requires a + * second, co-resident actor with write access to the target + * directory racing this command, outside this CLI's single-user + * threat model. + */ +int wolfCLU_PathsRefEqual(const char* pathA, const char* pathB); + +/** + * @brief Common -in/-out same-file guard used before a truncating -out open: + * wraps wolfCLU_PathsRefEqual() and logs+returns WOLFCLU_FATAL_ERROR + * for both WOLFCLU_PATHS_SAME and WOLFCLU_PATHS_UNDETERMINED (fails + * closed), or WOLFCLU_SUCCESS when the paths are provably distinct. + * pathB (the -out path) is named in the SAME-file log message. + */ +int wolfCLU_RejectSamePath(const char* pathA, const char* pathB); + +/** + * @brief Open -out for a paired -in/-out operation, guarded against both + * naming the same file: combines wolfCLU_RejectSamePath(in, out) + * (friendly early message) and wolfCLU_OpenOutFileDistinctFrom(out, + * inFile) (TOCTOU-safe fd-level check right before truncation) in + * one call, so encrypt/decrypt-style callers that open -in and -out + * back to back don't each repeat the pair. inFile must already be + * open. Logs its own failure on every path; returns NULL on + * refusal or open failure, leaving inFile open either way. + */ +FILE* wolfCLU_OpenPairedOutFile(const char* in, const char* out, + FILE* inFile); + +/** + * @brief Open path for writing with owner-only permissions and wrap in BIO. + */ +WOLFSSL_BIO* wolfCLU_OpenKeyFileBio(const char* path); + +/** + * @brief Open path for writing with default permissions and wrap in BIO. + */ +WOLFSSL_BIO* wolfCLU_OpenOutFileBio(const char* path); + +/** + * @brief Call wolfCLU_OpenKeyFileBio or wolfCLU_OpenOutFileBio based on + * whether the output holds key material. + */ +WOLFSSL_BIO* wolfCLU_OpenOutOrKeyFileBio(const char* path, + WOLFCLU_OUT_KIND kind); + +#endif /* !WOLFCLU_NO_FILESYSTEM */ + /** * @brief example client */ @@ -666,7 +866,8 @@ int wolfCLU_OcspSetup(int argc, char** argv); const char* wolfCLU_GetDefaultHttpGet(void); /** - * @brief Get the length of the default HTTP GET request (without null terminator) + * @brief Get the length of the default HTTP GET request (without null + * terminator) * @return length of HTTP GET request */ int wolfCLU_GetDefaultHttpGetLength(void);