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_decrypt.c b/src/crypto/clu_decrypt.c index 8d8faf42..6e3f5657 100644 --- a/src/crypto/clu_decrypt.c +++ b/src/crypto/clu_decrypt.c @@ -60,6 +60,12 @@ int wolfCLU_decrypt(int alg, char* mode, byte* pwdKey, byte* key, int size, XMEMSET(&rng, 0, sizeof(rng)); + /* Opening the output truncates it, destroying the ciphertext mid-read. */ + if (wolfCLU_PathsRefEqual(in, out)) { + wolfCLU_LogError("-in and -out name the same file %s", in); + return DECRYPT_ERROR; + } + /* opens input file */ inFile = XFOPEN(in, "rb"); if (inFile == NULL) { @@ -68,8 +74,7 @@ int wolfCLU_decrypt(int alg, char* mode, byte* pwdKey, byte* key, int size, } /* opens output file */ - if ((outFile = XFOPEN(out, "wb")) == NULL) { - wolfCLU_LogError("Error creating output file."); + if ((outFile = wolfCLU_OpenOutFile(out)) == NULL) { XFCLOSE(inFile); return DECRYPT_ERROR; } diff --git a/src/crypto/clu_encrypt.c b/src/crypto/clu_encrypt.c index 750a6ca8..420999c8 100644 --- a/src/crypto/clu_encrypt.c +++ b/src/crypto/clu_encrypt.c @@ -60,6 +60,14 @@ int wolfCLU_encrypt(int alg, char* mode, byte* pwdKey, byte* key, int size, char* userInputBuffer = NULL; /* buffer when input is not a file */ + /* Checked before the branch below, which treats a non-existent -in as a + * literal string and writes it out to that same path: opening the output + * truncates it, destroying the plaintext mid-read. */ + if (wolfCLU_PathsRefEqual(in, out)) { + wolfCLU_LogError("-in and -out name the same file %s", in); + return WOLFCLU_FATAL_ERROR; + } + if (access (in, F_OK) == -1) { WOLFCLU_LOG(WOLFCLU_L0, "file did not exist, encrypting string following \"-i\"" "instead."); @@ -75,9 +83,8 @@ int wolfCLU_encrypt(int alg, char* mode, byte* pwdKey, byte* key, int size, XMEMCPY(userInputBuffer, in, inputLength); /* open the file to write */ - tempInFile = XFOPEN(in, "wb"); + tempInFile = wolfCLU_OpenOutFile(in); if (tempInFile == NULL) { - wolfCLU_LogError("unable to open file %s", in); XFREE(userInputBuffer, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); return BAD_FUNC_ARG; } @@ -146,25 +153,25 @@ int wolfCLU_encrypt(int alg, char* mode, byte* pwdKey, byte* key, int size, } /* open the outFile in write mode */ - outFile = XFOPEN(out, "wb"); + outFile = wolfCLU_OpenOutFile(out); if (outFile == NULL) { - wolfCLU_LogError("unable to open output file %s", out); XFCLOSE(inFile); 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); 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); return MEMORY_E; } @@ -196,7 +203,12 @@ 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); return hexRet; } }/* end hex or ascii */ @@ -211,6 +223,7 @@ 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); return FREAD_ERROR; }/* End feof check */ }/* End fread check */ @@ -221,6 +234,7 @@ 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); return ret; @@ -230,6 +244,7 @@ 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); return FATAL_ERROR; @@ -253,15 +268,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)) { @@ -278,8 +285,6 @@ int wolfCLU_encrypt(int alg, char* mode, byte* pwdKey, byte* key, int size, wolfCLU_freeBins(input, output, NULL, NULL, NULL); return FWRITE_ERROR; } - /* close the outFile */ - XFCLOSE(outFile); length -= tempMax; if (length < 0) @@ -287,6 +292,7 @@ int wolfCLU_encrypt(int alg, char* mode, byte* pwdKey, byte* key, int size, } /* closes the opened files and frees the memory */ + XFCLOSE(outFile); XFCLOSE(inFile); XMEMSET(key, 0, size); XMEMSET(iv, 0 , block); diff --git a/src/crypto/clu_evp_crypto.c b/src/crypto/clu_evp_crypto.c index 4b7c3443..d821e44e 100644 --- a/src/crypto/clu_evp_crypto.c +++ b/src/crypto/clu_evp_crypto.c @@ -77,6 +77,12 @@ 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_PathsRefEqual(fileIn, fileOut)) { + wolfCLU_LogError("-in and -out name the same file %s", fileIn); + return WOLFCLU_FATAL_ERROR; + } + /* Start up the random number generator */ if (wc_InitRng(&rng) != 0) { wolfCLU_LogError("Random Number Generator failed to start."); diff --git a/src/dh/clu_dh.c b/src/dh/clu_dh.c index 373ccb1d..535f39b8 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 */ @@ -446,7 +398,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 @@ -488,7 +442,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; @@ -534,10 +493,10 @@ 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); if (bioOut == NULL) { - wolfCLU_LogError("Unable to open output file %s", - optarg); ret = WOLFCLU_FATAL_ERROR; } #endif @@ -728,7 +687,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 +732,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 +747,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..b419ea4d 100644 --- a/src/dsa/clu_dsa.c +++ b/src/dsa/clu_dsa.c @@ -197,10 +197,10 @@ 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); if (bioOut == NULL) { - wolfCLU_LogError("Unable to open output file %s", - optarg); ret = WOLFCLU_FATAL_ERROR; } #endif @@ -288,7 +288,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 +326,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 +341,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..25694f0c 100644 --- a/src/ecparam/clu_ecparam.c +++ b/src/ecparam/clu_ecparam.c @@ -221,7 +221,9 @@ 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); if (bioOut == NULL) { ret = WOLFCLU_FATAL_ERROR; } diff --git a/src/genkey/clu_genkey.c b/src/genkey/clu_genkey.c index e129096d..de361729 100644 --- a/src/genkey/clu_genkey.c +++ b/src/genkey/clu_genkey.c @@ -26,13 +26,21 @@ #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. + * 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) { @@ -121,7 +129,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; } @@ -216,7 +224,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; } @@ -319,9 +327,13 @@ int wolfCLU_genKey_ED25519(WC_RNG* rng, char* fOutNm, int directive, int format) /* 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 +415,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 +626,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 +676,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 +708,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 +752,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 +800,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 +860,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; } @@ -934,7 +945,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; } @@ -1043,7 +1054,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; @@ -1162,10 +1173,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; } } @@ -1229,10 +1238,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; } } @@ -1283,13 +1290,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; @@ -1410,10 +1417,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; } } @@ -1482,10 +1487,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; } } @@ -1537,11 +1540,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) { @@ -1560,13 +1565,16 @@ 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_OpenExistingSecureFile(filename, "rb+", 1); if (!file) { /* Create the file if it didn't exist. */ - file = fopen(filename, "wb+"); + file = wolfCLU_OpenKeyFile(filename); if (!file) { - fprintf(stderr, "error: fopen(%s, \"w+\") failed.\n", filename); + fprintf(stderr, "error: could not open %s for writing.\n", + filename); return WC_XMSS_RC_WRITE_FAIL; } } @@ -1588,9 +1596,9 @@ 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+"); + file = wolfCLU_OpenExistingSecureFile(filename, "rb", 1); 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; } @@ -1608,12 +1616,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; @@ -1646,9 +1657,15 @@ enum wc_XmssRc wolfCLU_XmssKey_ReadCb(byte * priv, filename = context; - file = fopen(filename, "rb"); + /* Reading back the private key: refuse a symlink here too, so the key + * cannot be sourced from a path an attacker redirected. That refusal is + * unconditional in wolfCLU_OpenExistingSecureFile(), so ownerOnly is left + * clear: this is a read-only path, and a key provisioned by another + * account must stay usable for signing without having its mode rewritten + * underneath the owner. */ + file = wolfCLU_OpenExistingSecureFile(filename, "rb", 0); 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; } @@ -1665,12 +1682,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" */ @@ -1777,10 +1794,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 */ @@ -1847,7 +1863,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..66feadd8 100644 --- a/src/hash/clu_hash_setup.c +++ b/src/hash/clu_hash_setup.c @@ -294,9 +294,10 @@ int wolfCLU_hashSetup(int argc, char** argv) } if (ret == WOLFCLU_SUCCESS && outFile != NULL) { - bioOut = wolfSSL_BIO_new_file(outFile, "wb"); + /* A digest is not secret, so this keeps fopen() semantics: symlinks + * and /dev/stdout stay valid -out targets. */ + bioOut = wolfCLU_OpenOutFileBio(outFile); if (bioOut == NULL) { - wolfCLU_LogError("unable to open output file %s", outFile); ret = USER_INPUT_ERROR; } } diff --git a/src/pkcs/clu_pkcs12.c b/src/pkcs/clu_pkcs12.c index 29b08219..c3b41553 100644 --- a/src/pkcs/clu_pkcs12.c +++ b/src/pkcs/clu_pkcs12.c @@ -74,6 +74,7 @@ int wolfCLU_PKCS12(int argc, char** argv) WOLF_STACK_OF(WOLFSSL_X509) *extra = NULL; WOLFSSL_BIO *bioIn = NULL; WOLFSSL_BIO *bioOut = NULL; + char *outPath = NULL; opterr = 0; /* do not display unrecognized options */ optind = 0; /* start at indent 0 */ @@ -115,12 +116,10 @@ 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. */ + outPath = optarg; break; case WOLFCLU_HELP: @@ -140,6 +139,15 @@ int wolfCLU_PKCS12(int argc, char** argv) } } + /* open -out now that -nokeys has been fully parsed: printKeys defaults + * on, so -out holds private key material unless -nokeys turned it off. */ + if (ret == WOLFCLU_SUCCESS && outPath != NULL) { + bioOut = wolfCLU_OpenOutOrKeyFileBio(outPath, printKeys); + if (bioOut == NULL) { + ret = WOLFCLU_FATAL_ERROR; + } + } + /* with currently only supporting PKCS12 parsing, an input file is expected */ if (ret == WOLFCLU_SUCCESS && bioIn == NULL) { wolfCLU_LogError("No input file set"); diff --git a/src/pkcs/clu_pkcs7.c b/src/pkcs/clu_pkcs7.c index 41b642a0..3510b62a 100644 --- a/src/pkcs/clu_pkcs7.c +++ b/src/pkcs/clu_pkcs7.c @@ -97,10 +97,10 @@ int wolfCLU_PKCS7(int argc, char** argv) break; case WOLFCLU_OUTFILE: - bioOut = wolfSSL_BIO_new_file(optarg, "wb"); + /* This tool only ever prints certificates, never key + * material, so default (non-owner-only) permissions apply. */ + bioOut = wolfCLU_OpenOutFileBio(optarg); if (bioOut == NULL) { - wolfCLU_LogError("Unable to open output file %s", - optarg); ret = WOLFCLU_FATAL_ERROR; } break; diff --git a/src/pkcs/clu_pkcs8.c b/src/pkcs/clu_pkcs8.c index 9c4c7049..52e7e373 100644 --- a/src/pkcs/clu_pkcs8.c +++ b/src/pkcs/clu_pkcs8.c @@ -109,10 +109,10 @@ int wolfCLU_PKCS8(int argc, char** argv) break; case WOLFCLU_OUTFILE: - bioOut = wolfSSL_BIO_new_file(optarg, "wb"); + /* Always a private key (PKCS#1 or PKCS#8, encrypted or + * not), so lock it down owner-only. */ + bioOut = wolfCLU_OpenKeyFileBio(optarg); if (bioOut == NULL) { - wolfCLU_LogError("Unable to open output file %s", - optarg); ret = WOLFCLU_FATAL_ERROR; } break; diff --git a/src/pkey/clu_pkey.c b/src/pkey/clu_pkey.c index 4d98a78b..1bf7c6b5 100644 --- a/src/pkey/clu_pkey.c +++ b/src/pkey/clu_pkey.c @@ -426,6 +426,7 @@ int wolfCLU_pKeySetup(int argc, char** argv) WOLFSSL_EVP_PKEY *pkey = NULL; WOLFSSL_BIO *bioIn = NULL; WOLFSSL_BIO *bioOut = NULL; + char *outPath = NULL; optind = 0; /* start at indent 0 */ while ((option = wolfCLU_GetOpt(argc, argv, "", pkey_options, @@ -455,12 +456,10 @@ int wolfCLU_pKeySetup(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: 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. */ + outPath = optarg; break; case WOLFCLU_INFORM: @@ -488,6 +487,14 @@ int wolfCLU_pKeySetup(int argc, char** argv) } + /* open -out now that -pubout has been fully parsed. */ + if (ret == WOLFCLU_SUCCESS && outPath != NULL) { + bioOut = wolfCLU_OpenOutOrKeyFileBio(outPath, !pubOut); + if (bioOut == NULL) { + ret = WOLFCLU_FATAL_ERROR; + } + } + if (ret == WOLFCLU_SUCCESS && bioIn != NULL) { if (inForm == PEM_FORM) { if (pubIn) { diff --git a/src/pkey/clu_rsa.c b/src/pkey/clu_rsa.c index fa748e41..9ab8aad0 100644 --- a/src/pkey/clu_rsa.c +++ b/src/pkey/clu_rsa.c @@ -80,6 +80,7 @@ int wolfCLU_RSA(int argc, char** argv) WOLFSSL_BIO *bioIn = NULL; WOLFSSL_BIO *bioOut = NULL; WOLFSSL_RSA *rsa = NULL; + char *outPath = NULL; opterr = 0; /* do not display unrecognized options */ optind = 0; /* start at indent 0 */ @@ -101,12 +102,10 @@ int wolfCLU_RSA(int argc, char** argv) 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. */ + outPath = optarg; break; case WOLFCLU_INFORM: @@ -155,6 +154,15 @@ int wolfCLU_RSA(int argc, char** argv) } } + /* open -out now that -pubout has been fully parsed: the output is a + * private key unless -pubout (or -pubin, which implies it) was given. */ + if (ret == WOLFCLU_SUCCESS && outPath != NULL) { + bioOut = wolfCLU_OpenOutOrKeyFileBio(outPath, !pubOut); + if (bioOut == NULL) { + ret = WOLFCLU_FATAL_ERROR; + } + } + /* read in the RSA key */ if (ret == WOLFCLU_SUCCESS && bioIn != NULL) { if (inForm == PEM_FORM) { 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_sign.c b/src/sign-verify/clu_sign.c index 831dc8da..8bcbf12a 100644 --- a/src/sign-verify/clu_sign.c +++ b/src/sign-verify/clu_sign.c @@ -240,7 +240,9 @@ int wolfCLU_sign_data_rsa(byte* data, char* out, word32 dataSz, char* privKey, /* open, read, and store RSA key */ if (ret == 0) { - privKeyFile = XFOPEN(privKey, "rb"); + /* Private key read: refuse to follow a symlink, but don't rewrite + * the mode of a key that may be provisioned by another account. */ + privKeyFile = wolfCLU_OpenExistingSecureFile(privKey, "rb", 0); if (privKeyFile == NULL) { wolfCLU_LogError("unable to open file %s", privKey); ret = BAD_FUNC_ARG; @@ -323,7 +325,9 @@ 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"); + /* Signature output is not secret; default permissions match + * fopen()'s behavior. */ + s = wolfCLU_OpenOutFile(out); if (s == NULL) { wolfCLU_LogError("Failed to open output file"); ret = BAD_FUNC_ARG; @@ -400,7 +404,9 @@ int wolfCLU_sign_data_ecc(byte* data, char* out, word32 fSz, char* privKey, /* open, read, and store ecc key */ if (ret == 0) { - privKeyFile = XFOPEN(privKey, "rb"); + /* Private key read: refuse to follow a symlink, but don't rewrite + * the mode of a key that may be provisioned by another account. */ + privKeyFile = wolfCLU_OpenExistingSecureFile(privKey, "rb", 0); if (privKeyFile == NULL) { wolfCLU_LogError("unable to open file %s", privKey); ret = BAD_FUNC_ARG; @@ -507,7 +513,9 @@ int wolfCLU_sign_data_ecc(byte* data, char* out, word32 fSz, char* privKey, } if (ret >= 0) { XFILE s; - s = XFOPEN(out, "wb"); + /* Signature output is not secret; default permissions match + * fopen()'s behavior. */ + s = wolfCLU_OpenOutFile(out); if (s == NULL) { wolfCLU_LogError("Failed to open file"); ret = BAD_FUNC_ARG; @@ -583,7 +591,9 @@ int wolfCLU_sign_data_ed25519 (byte* data, char* out, word32 fSz, char* privKey, /* open, read, and store ED25519 key */ if (ret == 0) { - privKeyFile = XFOPEN(privKey, "rb"); + /* Private key read: refuse to follow a symlink, but don't rewrite + * the mode of a key that may be provisioned by another account. */ + privKeyFile = wolfCLU_OpenExistingSecureFile(privKey, "rb", 0); if (privKeyFile == NULL) { wolfCLU_LogError("unable to open file %s", privKey); ret = BAD_FUNC_ARG; @@ -679,7 +689,9 @@ 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"); + /* Signature output is not secret; default permissions match + * fopen()'s behavior. */ + s = wolfCLU_OpenOutFile(out); if (s == NULL) { wolfCLU_LogError("Failed to open file"); ret = BAD_FUNC_ARG; @@ -771,7 +783,9 @@ int wolfCLU_sign_data_dilithium (byte* data, char* out, word32 dataSz, char* pri /* open and read private key */ if (ret == 0) { - privKeyFile = XFOPEN(privKey, "rb"); + /* Private key read: refuse to follow a symlink, but don't rewrite + * the mode of a key that may be provisioned by another account. */ + privKeyFile = wolfCLU_OpenExistingSecureFile(privKey, "rb", 0); if (privKeyFile == NULL) { wolfCLU_LogError("Failed to open Private key FILE."); ret = BAD_FUNC_ARG; @@ -858,7 +872,9 @@ int wolfCLU_sign_data_dilithium (byte* data, char* out, word32 dataSz, char* pri if (ret == 0) { XFILE outFile; - outFile = XFOPEN(out, "wb"); + /* Signature output is not secret; default permissions match + * fopen()'s behavior. */ + outFile = wolfCLU_OpenOutFile(out); if (outFile == NULL) { wolfCLU_LogError("Failed to open output file %s", out); @@ -1037,7 +1053,9 @@ int wolfCLU_sign_data_xmss(byte* data, char* out, int fSz, char* privKey) /* output signature */ if (ret == 0) { - outFile = XFOPEN(out, "wb"); + /* Signature output is not secret; default permissions match + * fopen()'s behavior. */ + outFile = wolfCLU_OpenOutFile(out); if (outFile == NULL) { ret = OUTPUT_FILE_ERROR; wolfCLU_LogError("Failed to open file %s.\nRET: %d", out, ret); @@ -1232,7 +1250,9 @@ int wolfCLU_sign_data_xmssmt(byte* data, char* out, int fSz, char* privKey) /* output signature */ if (ret == 0) { - outFile = XFOPEN(out, "wb"); + /* Signature output is not secret; default permissions match + * fopen()'s behavior. */ + outFile = wolfCLU_OpenOutFile(out); if (outFile == NULL) { ret = OUTPUT_FILE_ERROR; wolfCLU_LogError("Failed to open file %s.\nRET: %d", out, ret); diff --git a/src/sign-verify/clu_verify.c b/src/sign-verify/clu_verify.c index da8db85b..ae4c0ff0 100644 --- a/src/sign-verify/clu_verify.c +++ b/src/sign-verify/clu_verify.c @@ -38,50 +38,23 @@ int wolfCLU_verify_signature(char* sig, char* hashFile, char* out, { 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 +63,30 @@ 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); - 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; + { + int hSzInt = 0; + int hRet = wolfCLU_ReadFileToBuffer(hashFile, WOLFCLU_MAX_FILE_SIZE, &hash, &hSzInt); + if (hRet != WOLFCLU_SUCCESS) { + ret = hRet; + break; + } + hSz = hSzInt; } - 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); - 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; + { + int hSzInt = 0; + int hRet = wolfCLU_ReadFileToBuffer(hashFile, WOLFCLU_MAX_FILE_SIZE, &hash, &hSzInt); + if (hRet != WOLFCLU_SUCCESS) { + ret = hRet; + break; + } + hSz = hSzInt; } - XFCLOSE(h); ret = wolfCLU_verify_signature_ed25519(data, (int)fSz, hash, (int)hSz, keyPath, pubIn, inForm); #endif @@ -184,49 +94,15 @@ 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; - 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; + { + int hSzInt = 0; + int hRet = wolfCLU_ReadFileToBuffer(hashFile, WOLFCLU_MAX_FILE_SIZE, &hash, &hSzInt); + if (hRet != WOLFCLU_SUCCESS) { + ret = hRet; + break; + } + hSz = hSzInt; } - XFCLOSE(h); ret = wolfCLU_verify_signature_dilithium(data, (int)fSz, hash, (int)hSz, keyPath, inForm); @@ -235,99 +111,30 @@ 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); - 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; + { + int hSzInt = 0; + int hRet = wolfCLU_ReadFileToBuffer(hashFile, WOLFCLU_MAX_FILE_SIZE, &hash, &hSzInt); + if (hRet != WOLFCLU_SUCCESS) { + ret = hRet; + break; + } + hSz = hSzInt; } - 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); - 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; + { + int hSzInt = 0; + int hRet = wolfCLU_ReadFileToBuffer(hashFile, WOLFCLU_MAX_FILE_SIZE, &hash, &hSzInt); + if (hRet != WOLFCLU_SUCCESS) { + ret = hRet; + break; + } + hSz = hSzInt; } - XFCLOSE(h); ret = wolfCLU_verify_signature_xmssmt(data, (int)fSz, hash, (int)hSz, keyPath); 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..b8fc87ce 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) diff --git a/src/tools/clu_funcs.c b/src/tools/clu_funcs.c index 364bef3f..24869abf 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,40 @@ 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. */ +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 +1169,1108 @@ 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 secureOpen) +{ + 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; + + if (secureOpen) { + /* Refuse to follow a symlink, so key material cannot be sourced from + * a path an attacker redirected. ownerOnly is left clear: this is a + * read-only path, and a key provisioned by another account must stay + * usable without having its mode rewritten underneath it. */ + f = wolfCLU_OpenExistingSecureFile(path, "rb", 0); + } + else { + 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) { + 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 opens through + * wolfCLU_OpenExistingSecureFile() so key material cannot be read from a + * symlinked path. Use this for every private key read. */ +int wolfCLU_ReadKeyFileToBuffer(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); + 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 existed = 0; + 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; + } + existed = 1; + } + + 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 { + /* 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 */ +static int wolfCLU_FinishKeyFd(int fd, const char* path, + const struct stat* pre, int existed, int wantTrunc) +{ + 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 (st.st_uid != geteuid()) { + if (!existed) { + wolfCLU_UnlinkOwnFd(fd, path); + } + close(fd); + errno = EPERM; + return -1; + } + if (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 ((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) != 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, post; + + 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; + } + if (fstat(fd, &post) != 0 || !S_ISREG(post.st_mode) || + post.st_dev != pre.st_dev || post.st_ino != pre.st_ino) { + close(fd); + errno = ELOOP; + return NULL; + } + if (ownerOnly && post.st_uid != geteuid()) { + close(fd); + errno = EPERM; + return NULL; + } + /* As at creation: a second hard link keeps a live view of every update + * and shares the mode tightened below. Both entry points must agree. */ + if (ownerOnly && post.st_nlink > 1) { + close(fd); + errno = EMLINK; + return NULL; + } + /* Match wolfCLU_FinishKeyFd(): force the mode down to exactly + * WOLFCLU_KEY_FILE_MODE rather than just clearing group/other bits, so + * both entry points enforce the same permission policy on a key file - + * e.g. a pre-existing 0700 file is tightened to 0600 here too. */ + if (ownerOnly && + (post.st_mode & (mode_t)~S_IFMT) != (mode_t)WOLFCLU_KEY_FILE_MODE) { + if (fchmod(fd, WOLFCLU_KEY_FILE_MODE) != 0) { + close(fd); + errno = EPERM; + return NULL; + } + } + /* Truncate only after the mode has been tightened, so the old contents + * are never dropped on a path this call is about to refuse. */ + if (wantTrunc && ftruncate(fd, 0) != 0) { + int err = errno; + close(fd); + errno = err; + return NULL; + } + f = fdopen(fd, mode); + if (f == NULL) { + int err = errno; + close(fd); + errno = err; + } + return f; +} +#endif /* _WIN32 */ + +FILE* wolfCLU_OpenKeyFile(const char* path) +{ + FILE* f; + + errno = 0; + f = wolfCLU_CreateSecureFile(path, "wb", 1); + + if (f == NULL) { + /* Distinguish every deliberate refusal from a plain open failure so + * the user is not left guessing why a writable path was rejected. */ + if (errno == ELOOP) { + wolfCLU_LogError("Refusing to write key material through the " + "symlink %s", path); + } + else if (errno == EEXIST) { + wolfCLU_LogError("Refusing to write key material to %s: not a " + "regular file", path); + } + else if (errno == EPERM) { + wolfCLU_LogError("Refusing to write key material to %s: owned by " + "another user", path); + } + else if (errno == 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); + } + } + return f; +} + +FILE* wolfCLU_OpenOutFile(const char* path) +{ + FILE* f = wolfCLU_CreateSecureFile(path, "wb", 0); + + if (f == NULL) { + wolfCLU_LogError("Unable to open output file %s", path); + } + return f; +} + +#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. + * return 1 on success, 0 if the path cannot be canonicalized */ +static int wolfCLU_ResolveParentPath(const char* path, char* out, + word32 outSz, char* scratch) +{ + char* dirBuf = scratch; + char* baseBuf = scratch + WOLFCLU_PATH_BUF_SZ; + char* resolvedDir = scratch + (WOLFCLU_PATH_BUF_SZ * 2); + + 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 = realpath(dirname(dirBuf), NULL); + if (tmp == NULL) { + 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; + } + *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 1 when both paths name (or might name) the same file, 0 only when + * they are provably distinct. This guards an overwrite-in-place check, so + * an inconclusive comparison (allocation failure, unresolvable path, etc.) + * fails closed - treated as a possible match - rather than silently letting + * -in and -out alias the same file. */ +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 0; + } + if (XSTRCMP(pathA, pathB) == 0) { + return 1; + } + + /* 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; + } + + /* 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. */ + return 1; + } + 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. */ + ret = 1; + } + else { + ret = (_stricmp(fullA, fullB) == 0); + } + } +#else + { + char* scratch = work + (WOLFCLU_PATH_BUF_SZ * 2); + + if (!wolfCLU_ResolveParentPath(pathA, fullA, WOLFCLU_PATH_BUF_SZ, + scratch) || + !wolfCLU_ResolveParentPath(pathB, fullB, + WOLFCLU_PATH_BUF_SZ, scratch)) { + /* Couldn't resolve one side's parent directory; fail closed + * rather than assume the paths are distinct. */ + ret = 1; + } + else { + ret = (XSTRCMP(fullA, fullB) == 0); + } + } +#endif + + XFREE(work, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + return ret; +} + +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, int isSecret) +{ + return isSecret ? 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 +2472,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 +2547,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 +2572,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 +2585,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 +2620,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 +2632,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 +2641,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/x509/clu_request_setup.c b/src/x509/clu_request_setup.c index 6cea40c0..47264800 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()); diff --git a/tests/dgst/dgst-test.py b/tests/dgst/dgst-test.py index 456224b5..a9a1953d 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", @@ -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..c0f8d520 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,46 @@ 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") + + 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") + def test_small_file(self): small = "enc_small.txt" enc = "enc_small.txt.enc" @@ -180,6 +219,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 +376,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 +392,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 +457,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 +517,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 +531,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 +675,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 +689,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 +946,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..0729b4f2 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,7 +178,7 @@ 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_signature_size(self): @@ -207,7 +219,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): @@ -327,6 +339,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 +589,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 +605,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 +619,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 +786,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/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..65d8884b --- /dev/null +++ b/tests/tools/tools_unit_test.c @@ -0,0 +1,623 @@ +/* tools_unit_test.c */ + +#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; + +#define CHECK(cond, msg) \ + do { \ + 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"); + 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); + } + } +#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); +} +#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"); + + /* 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"); +} + + +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; + } + + testReadFileToBuffer(); + testPathsRefEqual(); +#ifndef _WIN32 + testOpenOutAndKeyFile(); + testKeyFileRefusals(); + testOpenExistingSecureFile(); +#endif + testSecureFileBios(); + 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); + } + + return fail ? 1 : (skipped ? 77 : 0); +} + +#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..50b7ed3c 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,6 @@ def test_override_extensions_md_days_cert_keyfile(self): self.assertEqual(r.returncode, 0, r.stderr) - class TestCAKeyMismatch(unittest.TestCase): """ca with mismatched key should fail.""" @@ -427,6 +432,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 +553,7 @@ def test_rand_file_changes(self): +@unittest.skipIf(no_filesystem(), "filesystem support disabled") class TestCAPolicy(unittest.TestCase): """Policy section enforcement.""" @@ -646,6 +653,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 +737,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 47766c72..8ac5c846 100644 --- a/wolfclu/clu_header_main.h +++ b/wolfclu/clu_header_main.h @@ -118,9 +118,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 @@ -442,8 +442,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); @@ -613,6 +613,111 @@ 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 opens through + * wolfCLU_OpenExistingSecureFile() so key material cannot be read from a + * symlinked path. Use this for every private key read. + */ +int wolfCLU_ReadKeyFileToBuffer(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 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 path for writing non-secret output, with default permissions. + */ +FILE* wolfCLU_OpenOutFile(const char* path); + +/** + * @brief Check if two path strings name (or might name) the same file. + * Returns 0 only when they are provably distinct; an inconclusive + * comparison (for example a path whose parent directory cannot be + * canonicalized) fails closed and reports 1. + */ +int wolfCLU_PathsRefEqual(const char* pathA, const char* pathB); + +/** + * @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 + * isSecret. + */ +WOLFSSL_BIO* wolfCLU_OpenOutOrKeyFileBio(const char* path, int isSecret); + +#endif /* !WOLFCLU_NO_FILESYSTEM */ + /** * @brief example client */ @@ -665,7 +770,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);