diff --git a/src/hash/clu_hash.c b/src/hash/clu_hash.c index 0bc80ed7..1d83ce01 100644 --- a/src/hash/clu_hash.c +++ b/src/hash/clu_hash.c @@ -27,9 +27,9 @@ /* * hashing function - * If bioIn is null then read 8192 max bytes from stdin - * If bioOut is null then print to stdout - * + * Stream stdin in MAX_IO_CHUNK_SZ blocks. On fallback to base64 enc/dec: + * If bioIn is null then read 8192 max bytes from stdin. If bioOut is null then + * print to stdout */ int wolfCLU_hash(WOLFSSL_BIO* bioIn, WOLFSSL_BIO* bioOut, const char* alg, int size) @@ -37,147 +37,281 @@ int wolfCLU_hash(WOLFSSL_BIO* bioIn, WOLFSSL_BIO* bioOut, const char* alg, #ifdef HAVE_BLAKE2 Blake2b hash; /* blake2b declaration */ #endif - byte* input; /* input buffer */ - byte* output; /* output buffer */ - - int i = 0; /* loop variable */ + byte chunk[MAX_IO_CHUNK_SZ]; + byte* input = NULL; + byte* output = NULL; int ret = WOLFCLU_SUCCESS; - int inputSz = MAX_STDINSZ; + int outSz; + int handled = 0; + enum wc_HashType hashType = WC_HASH_TYPE_NONE; WOLFSSL_BIO* tmp; if (bioIn == NULL) { tmp = wolfSSL_BIO_new(wolfSSL_BIO_s_file()); - if (tmp != NULL) - wolfSSL_BIO_set_fp(tmp, stdin, BIO_NOCLOSE); + if (tmp == NULL) { + return MEMORY_E; + } + wolfSSL_BIO_set_fp(tmp, stdin, BIO_NOCLOSE); } else { - /* get data size using raw FILE pointer and seek */ - XFILE f; tmp = bioIn; - if (wolfSSL_BIO_get_fp(tmp, &f) != WOLFSSL_SUCCESS) { - wolfCLU_LogError("Unable to get raw file pointer"); - ret = WOLFCLU_FATAL_ERROR; - } - - if (ret == WOLFCLU_SUCCESS && XFSEEK(f, 0, XSEEK_END) != 0) { - wolfCLU_LogError("Unable to seek end of file"); - ret = WOLFCLU_FATAL_ERROR; - } - - if (ret == WOLFCLU_SUCCESS) { - inputSz = (word32)XFTELL(f); - wolfSSL_BIO_reset(tmp); - } } - input = (byte*)XMALLOC(inputSz, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - if (input == NULL) { + /* Output buffer size: digest size for hash algorithms (default is + * WC_MAX_DIGEST_SIZE), or caller-provided size for base64. */ + outSz = (size == 0) ? WC_MAX_DIGEST_SIZE : size; + output = (byte*)XMALLOC(outSz, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + if (output == NULL) { if (bioIn == NULL) wolfSSL_BIO_free(tmp); return MEMORY_E; } - inputSz = wolfSSL_BIO_read(tmp, input, inputSz); - if (bioIn == NULL) - wolfSSL_BIO_free(tmp); + XMEMSET(output, 0, outSz); - /* if size not provided then use input length to find max possible size */ - if (size == 0) { - #ifndef NO_CODING - if (Base64_Encode(input, inputSz, NULL, (word32*)&size) != - LENGTH_ONLY_E) { - wolfCLU_freeBins(input, NULL, NULL, NULL, NULL); - return BAD_FUNC_ARG; - } - #endif - size = (size < WC_MAX_DIGEST_SIZE) ? WC_MAX_DIGEST_SIZE : size; - } + /* Chunked reads fed to Init/Update/Final. No upfront file-size + * determination, so files larger than UINT32_MAX are handled correctly. */ - output = XMALLOC(size, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - if (output == NULL) { - wolfCLU_freeBins(input, NULL, NULL, NULL, NULL); - return MEMORY_E; - } - XMEMSET(output, 0, size); - - /* hashes using accepted algorithm */ #ifndef NO_MD5 - if (ret == WOLFCLU_SUCCESS && XSTRCMP(alg, "md5") == 0) { - ret = wc_Md5Hash(input, inputSz, output); - } + if (XSTRCMP(alg, "md5") == 0) + hashType = WC_HASH_TYPE_MD5; +#endif +#ifndef NO_SHA + if (XSTRCMP(alg, "sha") == 0) + hashType = WC_HASH_TYPE_SHA; #endif #ifndef NO_SHA256 - if (ret == WOLFCLU_SUCCESS && XSTRCMP(alg, "sha256") == 0) { - ret = wc_Sha256Hash(input, inputSz, output); - } + if (XSTRCMP(alg, "sha256") == 0) + hashType = WC_HASH_TYPE_SHA256; #endif #ifdef WOLFSSL_SHA384 - if (ret == WOLFCLU_SUCCESS && XSTRCMP(alg, "sha384") == 0) { - ret = wc_Sha384Hash(input, inputSz, output); - } + if (XSTRCMP(alg, "sha384") == 0) + hashType = WC_HASH_TYPE_SHA384; #endif #ifdef WOLFSSL_SHA512 - if (ret == WOLFCLU_SUCCESS && XSTRCMP(alg, "sha512") == 0) { - ret = wc_Sha512Hash(input, inputSz, output); - } + if (XSTRCMP(alg, "sha512") == 0) + hashType = WC_HASH_TYPE_SHA512; #endif -#ifndef NO_SHA - if (ret == WOLFCLU_SUCCESS && XSTRCMP(alg, "sha") == 0) { - ret = wc_ShaHash(input, inputSz, output); + + if (hashType != WC_HASH_TYPE_NONE) { + int digestSz = wc_HashGetDigestSize(hashType); + int bytesRead; + int hashInit = 0; + wc_HashAlg hashAlg; + + if (digestSz <= 0 || digestSz > outSz) { + wolfCLU_LogError("Bad digest size for selected hash"); + ret = WOLFCLU_FATAL_ERROR; + } + if (ret == WOLFCLU_SUCCESS) { + if (wc_HashInit(&hashAlg, hashType) != 0) { + wolfCLU_LogError("Unable to initialize hash"); + ret = WOLFCLU_FATAL_ERROR; + } + else { + hashInit = 1; + } + } + while (ret == WOLFCLU_SUCCESS) { + bytesRead = wolfSSL_BIO_read(tmp, 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; + } + } + if (ret == WOLFCLU_SUCCESS) { + if (wc_HashFinal(&hashAlg, hashType, output) != 0) { + wolfCLU_LogError("Hash finalization failed"); + ret = WOLFCLU_FATAL_ERROR; + } + else { + outSz = digestSz; + } + } + if (hashInit) { + wc_HashFree(&hashAlg, hashType); + } + handled = 1; } -#endif + #ifdef HAVE_BLAKE2 - if (ret == WOLFCLU_SUCCESS && XSTRCMP(alg, "blake2b") == 0) { - ret = wc_InitBlake2b(&hash, size); - if (ret != 0) return ret; - ret = wc_Blake2bUpdate(&hash, input, inputSz); - if (ret != 0) return ret; - ret = wc_Blake2bFinal(&hash, output, size); - if (ret != 0) return ret; + if (!handled && ret == WOLFCLU_SUCCESS && XSTRCMP(alg, "blake2b") == 0) { + int bytesRead; + if (wc_InitBlake2b(&hash, outSz) != 0) { + wolfCLU_LogError("Unable to initialize blake2b"); + ret = WOLFCLU_FATAL_ERROR; + } + while (ret == WOLFCLU_SUCCESS) { + bytesRead = wolfSSL_BIO_read(tmp, chunk, sizeof(chunk)); + if (bytesRead < 0) { + wolfCLU_LogError("Error reading data"); + ret = WOLFCLU_FATAL_ERROR; + break; + } + else if (bytesRead == 0) { + break; + } + if (wc_Blake2bUpdate(&hash, chunk, (word32)bytesRead) != 0) { + wolfCLU_LogError("Blake2b update failed"); + ret = WOLFCLU_FATAL_ERROR; + } + } + if (ret == WOLFCLU_SUCCESS) { + if (wc_Blake2bFinal(&hash, output, outSz) != 0) { + wolfCLU_LogError("Blake2b finalization failed"); + ret = WOLFCLU_FATAL_ERROR; + } + } + handled = 1; } #endif #ifndef NO_CODING + /* Buffered fall-back for base64 enc/dec (not a hash, so streaming + * Init/Update/Final doesn't apply). size is taken from XFTELL + * but bounded to INT_MAX */ + if (!handled && ret == WOLFCLU_SUCCESS) { + long fileLen = MAX_STDINSZ; + int inputSz = MAX_STDINSZ; + XFILE f; + + if (bioIn != NULL) { + if (wolfSSL_BIO_get_fp(tmp, &f) != WOLFSSL_SUCCESS) { + wolfCLU_LogError("Unable to get raw file pointer"); + ret = WOLFCLU_FATAL_ERROR; + } + if (ret == WOLFCLU_SUCCESS && XFSEEK(f, 0, XSEEK_END) != 0) { + wolfCLU_LogError("Unable to seek end of file"); + ret = WOLFCLU_FATAL_ERROR; + } + if (ret == WOLFCLU_SUCCESS) { + fileLen = XFTELL(f); + wolfSSL_BIO_reset(tmp); + if (fileLen < 0 || fileLen > INT_MAX) { + wolfCLU_LogError("Input too large for base64 buffer"); + ret = WOLFCLU_FATAL_ERROR; + } + else { + inputSz = (int)fileLen; + } + } + } + + if (ret == WOLFCLU_SUCCESS) { + input = (byte*)XMALLOC(inputSz, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + if (input == NULL) { + ret = MEMORY_E; + } + } + if (ret == WOLFCLU_SUCCESS) { + inputSz = wolfSSL_BIO_read(tmp, input, inputSz); + if (inputSz < 0) { + wolfCLU_LogError("Error reading data"); + ret = WOLFCLU_FATAL_ERROR; + } + } + #ifdef WOLFSSL_BASE64_ENCODE - if (ret == WOLFCLU_SUCCESS && XSTRCMP(alg, "base64enc") == 0) { - ret = Base64_Encode(input, inputSz, output, (word32*)&size); - } + if (ret == WOLFCLU_SUCCESS && XSTRCMP(alg, "base64enc") == 0) { + if (size == 0) { + if (Base64_Encode(input, inputSz, NULL, (word32*)&outSz) + != LENGTH_ONLY_E) { + ret = BAD_FUNC_ARG; + } + else { + XFREE(output, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + output = (byte*)XMALLOC(outSz, HEAP_HINT, + DYNAMIC_TYPE_TMP_BUFFER); + if (output == NULL) { + ret = MEMORY_E; + } + else { + XMEMSET(output, 0, outSz); + } + } + } + if (ret == WOLFCLU_SUCCESS) { + if (Base64_Encode(input, inputSz, output, (word32*)&outSz) + != 0) { + wolfCLU_LogError("Base64 encode failed"); + ret = WOLFCLU_FATAL_ERROR; + } + handled = 1; + } + } #endif /* WOLFSSL_BASE64_ENCODE */ - if (ret == WOLFCLU_SUCCESS && XSTRCMP(alg, "base64dec") == 0) { - ret = Base64_Decode(input, inputSz, output, (word32*)&size); + if (ret == WOLFCLU_SUCCESS && XSTRCMP(alg, "base64dec") == 0) { + if (size == 0) { + XFREE(output, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + outSz = inputSz; + output = (byte*)XMALLOC(outSz, HEAP_HINT, + DYNAMIC_TYPE_TMP_BUFFER); + if (output == NULL) { + ret = MEMORY_E; + } + else { + XMEMSET(output, 0, outSz); + } + } + if (ret == WOLFCLU_SUCCESS) { + if (Base64_Decode(input, inputSz, output, (word32*)&outSz) + != 0) { + wolfCLU_LogError("Base64 decode failed"); + ret = WOLFCLU_FATAL_ERROR; + } + handled = 1; + } + } } #endif /* !NO_CODING */ - if (ret == 0) { + if (bioIn == NULL) { + wolfSSL_BIO_free(tmp); + } + + if (ret == WOLFCLU_SUCCESS && handled) { if (bioOut != NULL) { - if (wolfSSL_BIO_write(bioOut, output, size) == size) { - ret = WOLFCLU_SUCCESS; - } - else { + if (wolfSSL_BIO_write(bioOut, output, outSz) != outSz) { ret = WOLFCLU_FATAL_ERROR; } } else { + int i; /* write hashed output to terminal */ tmp = wolfSSL_BIO_new(wolfSSL_BIO_s_file()); - if (tmp != NULL) { + if (tmp == NULL) { + ret = MEMORY_E; + } + else { wolfSSL_BIO_set_fp(tmp, stdout, BIO_NOCLOSE); - - for (i = 0; i < size; i++) + for (i = 0; i < outSz; i++) { wolfSSL_BIO_printf(tmp, "%02x", output[i]); + } wolfSSL_BIO_printf(tmp, "\n"); wolfSSL_BIO_free(tmp); - ret = WOLFCLU_SUCCESS; - } - else { - ret = MEMORY_E; } } } + else if (!handled && ret == WOLFCLU_SUCCESS) { + wolfCLU_LogError("Unrecognized algorithm: %s", alg); + ret = WOLFCLU_FATAL_ERROR; + } - /* closes the opened files and frees the memory */ - XMEMSET(input, 0, inputSz); - XMEMSET(output, 0, size); - wolfCLU_freeBins(input, output, NULL, NULL, NULL); + if (input != NULL) { + XFREE(input, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + } + if (output != NULL) { + XMEMSET(output, 0, outSz); + XFREE(output, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + } return ret; } diff --git a/src/sign-verify/clu_dgst_setup.c b/src/sign-verify/clu_dgst_setup.c index 40cfd21e..a1bf7d75 100644 --- a/src/sign-verify/clu_dgst_setup.c +++ b/src/sign-verify/clu_dgst_setup.c @@ -169,10 +169,10 @@ int wolfCLU_dgst_setup(int argc, char** argv) WOLFSSL_EVP_PKEY *pkey = NULL; int ret = WOLFCLU_SUCCESS; byte* sig = NULL; - char* data = NULL; char* sigFile = NULL; void* key = NULL; - word32 dataSz = 0; + byte digest[MAX_DER_DIGEST_SZ]; + word32 digestSz = 0; word32 sigSz = 0; int keySz = 0; int option; @@ -277,71 +277,74 @@ int wolfCLU_dgst_setup(int argc, char** argv) } } + /* Stream the data file through a hash to produce a digest, then pass + * the digest to wc_Signature{Generate,Verify}Hash below. */ if (ret == WOLFCLU_SUCCESS) { - XFILE f; - - /* get data size using raw FILE pointer and seek */ - if (wolfSSL_BIO_get_fp(dataBio, &f) != WOLFSSL_SUCCESS) { - wolfCLU_LogError("Unable to get raw file pointer"); - ret = WOLFCLU_FATAL_ERROR; - } - - if (ret == WOLFCLU_SUCCESS && XFSEEK(f, 0, XSEEK_END) != 0) { - wolfCLU_LogError("Unable to seek end of file"); + int dsz = wc_HashGetDigestSize(hashType); + int chunkBytes; + int hashInit = 0; + wc_HashAlg hashAlg; + byte chunk[MAX_IO_CHUNK_SZ]; + + if (dsz <= 0 || dsz > WC_MAX_DIGEST_SIZE) { + wolfCLU_LogError("Bad digest size for selected hash"); ret = WOLFCLU_FATAL_ERROR; } - if (ret == WOLFCLU_SUCCESS) { - dataSz = (word32)XFTELL(f); - wolfSSL_BIO_reset(dataBio); + if (wc_HashInit(&hashAlg, hashType) != 0) { + wolfCLU_LogError("Unable to initialize hash"); + ret = WOLFCLU_FATAL_ERROR; + } + else { + hashInit = 1; + } } - - if (signing == 0) { - sigBio = wolfSSL_BIO_new_file(sigFile, "rb"); - if (sigBio == NULL) { - wolfCLU_LogError("Unable to read signature file %s", - sigFile); + while (ret == WOLFCLU_SUCCESS) { + chunkBytes = wolfSSL_BIO_read(dataBio, chunk, sizeof(chunk)); + if (chunkBytes < 0) { + wolfCLU_LogError("Error reading data"); ret = WOLFCLU_FATAL_ERROR; + break; } - - if (ret == WOLFCLU_SUCCESS) { - ret = wolfSSL_BIO_get_len(sigBio); - if (ret <= 0) { - wolfCLU_LogError("Unable to get signature size"); - ret = WOLFCLU_FATAL_ERROR; - } - else { - sigSz = (word32)ret; - ret = WOLFCLU_SUCCESS; - } + else if (chunkBytes == 0) { + break; + } + if (wc_HashUpdate(&hashAlg, hashType, chunk, (word32)chunkBytes) + != 0) { + wolfCLU_LogError("Hash update failed"); + ret = WOLFCLU_FATAL_ERROR; } } - - if (dataSz <= 0 || (sigSz <= 0 && signing == 0)) { - wolfCLU_LogError("No signature or data"); - ret = WOLFCLU_FATAL_ERROR; + if (ret == WOLFCLU_SUCCESS) { + if (wc_HashFinal(&hashAlg, hashType, digest) != 0) { + wolfCLU_LogError("Hash finalization failed"); + ret = WOLFCLU_FATAL_ERROR; + } + else { + digestSz = (word32)dsz; + } + } + if (hashInit) { + wc_HashFree(&hashAlg, hashType); } } - /* create buffers and fill them */ - if (ret == WOLFCLU_SUCCESS) { - data = (char*)XMALLOC(dataSz, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - if (data == NULL) { - ret = MEMORY_E; + if (ret == WOLFCLU_SUCCESS && signing == 0) { + sigBio = wolfSSL_BIO_new_file(sigFile, "rb"); + if (sigBio == NULL) { + wolfCLU_LogError("Unable to read signature file %s", sigFile); + ret = WOLFCLU_FATAL_ERROR; } - else { - word32 totalRead = 0; - - /* read in 4k at a time because file could be larger than int type - * restriction on size input for wolfSSL_BIO_read */ - while (totalRead < dataSz) { - int sz = min(dataSz - totalRead, 4096); - if (wolfSSL_BIO_read(dataBio, data + totalRead, sz) != sz) { - wolfCLU_LogError("Error reading data"); - ret = WOLFCLU_FATAL_ERROR; - break; - } - totalRead += sz; + + if (ret == WOLFCLU_SUCCESS) { + ret = wolfSSL_BIO_get_len(sigBio); + if (ret <= 0) { + wolfCLU_LogError("Unable to get signature size"); + ret = WOLFCLU_FATAL_ERROR; + } + else { + sigSz = (word32)ret; + ret = WOLFCLU_SUCCESS; } } } @@ -405,9 +408,33 @@ int wolfCLU_dgst_setup(int argc, char** argv) } } + /* For RSA with PKCS#1 v1.5 encoding, wc_Signature{Generate,Verify}Hash + * expect the digest already wrapped in DER. The non-Hash variants did + * this internally. ECC signs the raw digest, so no wrap. */ +#ifndef NO_RSA + if (ret == WOLFCLU_SUCCESS && sigType == WC_SIGNATURE_TYPE_RSA_W_ENC) { + int oid = wc_HashGetOID(hashType); + word32 enc; + if (oid < 0) { + wolfCLU_LogError("Unable to get hash OID for DER encoding"); + ret = WOLFCLU_FATAL_ERROR; + } + else { + enc = wc_EncodeSignature(digest, digest, digestSz, oid); + if (enc == 0) { + wolfCLU_LogError("Unable to DER-encode digest"); + ret = WOLFCLU_FATAL_ERROR; + } + else { + digestSz = enc; + } + } + } +#endif + /* if not signing then do verification */ if (ret == WOLFCLU_SUCCESS && signing == 0) { - if (wc_SignatureVerify(hashType, sigType, (const byte*)data, dataSz, + if (wc_SignatureVerifyHash(hashType, sigType, digest, digestSz, (const byte*)sig, sigSz, key, keySz) == 0) { WOLFCLU_LOG(WOLFCLU_L0, "Verify OK"); } @@ -447,8 +474,8 @@ int wolfCLU_dgst_setup(int argc, char** argv) } if (ret == WOLFCLU_SUCCESS && - wc_SignatureGenerate(hashType, sigType, (const byte*)data, - dataSz, sig, &sigSz, key, keySz, &rng) != 0) { + wc_SignatureGenerateHash(hashType, sigType, digest, digestSz, + sig, &sigSz, key, keySz, &rng) != 0) { wolfCLU_LogError("Error getting signature"); ret = WOLFCLU_FATAL_ERROR; } @@ -492,8 +519,6 @@ int wolfCLU_dgst_setup(int argc, char** argv) } } - if (data != NULL) - XFREE(data, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); if (sig != NULL) XFREE(sig, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); diff --git a/tests/dgst/dgst-test.py b/tests/dgst/dgst-test.py index 1cef0602..ed6fd1e6 100644 --- a/tests/dgst/dgst-test.py +++ b/tests/dgst/dgst-test.py @@ -3,7 +3,9 @@ import filecmp import os +import shutil import sys +import tempfile import unittest sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) @@ -188,6 +190,107 @@ def test_enc_dec_large_file(self): "Decryption of large file failed") +class LargeFileDgstTest(unittest.TestCase): + """A signature over a >4 GiB file must NOT verify a tampered copy. + + Guards against truncating the file size to word32 in clu_dgst_setup.c, + which caused only the first 4 GiB to be hashed before sign/verify. + + Defaults to sha256 + RSA. Override with: + WOLFCLU_LARGE_DGST_ALG= (md5, sha, sha256, sha384, sha512) + WOLFCLU_LARGE_DGST_KEY= + """ + + LARGE_FILE_SIZE = 4_831_838_208 # 4.5 GiB, well above UINT32_MAX + CANDIDATE_ALGS = ["md5", "sha", "sha256", "sha384", "sha512"] + DEFAULT_ALG = "sha256" + KEY_PAIRS = { + "rsa": ("server-key.pem", "server-keyPub.pem"), + "ecc": ("ecc-key.pem", "ecc-keyPub.pem"), + } + DEFAULT_KEY = "rsa" + + @classmethod + def _probe_supported(cls, algs): + probe_input = os.path.join(CERTS_DIR, "ca-cert.pem") + supported = [] + for alg in algs: + r = run_wolfssl(alg, probe_input) + if r.returncode == 0: + supported.append(alg) + return supported + + @classmethod + def setUpClass(cls): + if not os.path.isdir(CERTS_DIR): + raise unittest.SkipTest("certs directory not found") + + requested_alg = os.environ.get("WOLFCLU_LARGE_DGST_ALG", + cls.DEFAULT_ALG) + if requested_alg == "all": + cls.algs = cls._probe_supported(cls.CANDIDATE_ALGS) + else: + cls.algs = cls._probe_supported([requested_alg]) + if not cls.algs: + raise unittest.SkipTest( + "no supported hash algorithm for " + "WOLFCLU_LARGE_DGST_ALG={}".format(requested_alg)) + + requested_key = os.environ.get("WOLFCLU_LARGE_DGST_KEY", + cls.DEFAULT_KEY) + if requested_key == "all": + cls.key_kinds = list(cls.KEY_PAIRS.keys()) + elif requested_key in cls.KEY_PAIRS: + cls.key_kinds = [requested_key] + else: + raise unittest.SkipTest( + "unknown WOLFCLU_LARGE_DGST_KEY={}".format(requested_key)) + + cls._tmpdir = tempfile.mkdtemp(prefix="wolfclu-large-dgst-") + cls.original = os.path.join(cls._tmpdir, "original.bin") + cls.tampered = os.path.join(cls._tmpdir, "tampered.bin") + try: + for p in (cls.original, cls.tampered): + with open(p, "wb") as f: + f.truncate(cls.LARGE_FILE_SIZE) + with open(cls.tampered, "r+b") as f: + f.seek(-1, os.SEEK_END) + f.write(b"X") + except OSError as e: + shutil.rmtree(cls._tmpdir, ignore_errors=True) + raise unittest.SkipTest("could not create sparse files: {}".format(e)) + + @classmethod + def tearDownClass(cls): + shutil.rmtree(getattr(cls, "_tmpdir", ""), ignore_errors=True) + + def test_tampered_last_byte_fails_verify(self): + for alg in self.algs: + for key_kind in self.key_kinds: + priv_name, pub_name = self.KEY_PAIRS[key_kind] + priv_key = os.path.join(CERTS_DIR, priv_name) + pub_key = os.path.join(CERTS_DIR, pub_name) + sig_file = os.path.join( + self._tmpdir, "{}-{}.sig".format(alg, key_kind)) + with self.subTest(alg=alg, key=key_kind): + r = run_wolfssl( + "dgst", "-" + alg, "-sign", priv_key, + "-out", sig_file, self.original, timeout=1800) + self.assertEqual(r.returncode, 0, r.stderr) + + r = run_wolfssl( + "dgst", "-" + alg, "-verify", pub_key, + "-signature", sig_file, self.original, + timeout=1800) + self.assertEqual(r.returncode, 0, r.stderr) + + r = run_wolfssl( + "dgst", "-" + alg, "-verify", pub_key, + "-signature", sig_file, self.tampered, + timeout=1800) + self.assertNotEqual(r.returncode, 0) + + class DgstSignVerifyRoundtripTest(unittest.TestCase): @classmethod diff --git a/tests/hash/hash-test.py b/tests/hash/hash-test.py index ee17dc6b..78dd4a10 100644 --- a/tests/hash/hash-test.py +++ b/tests/hash/hash-test.py @@ -2,7 +2,9 @@ """Hash tests for wolfCLU.""" import os +import shutil import sys +import tempfile import unittest sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) @@ -88,6 +90,73 @@ def test_sha512(self): self.assertEqual(r.stdout.strip(), _read_expected("sha512-expect.hex")) +class LargeFileHashTest(unittest.TestCase): + """Two >4 GiB files differing only in the last byte must hash differently. + + Guards against truncating the file size to word32 in clu_hash.c, which + caused files past the 4 GiB boundary to collide. + + Defaults to sha256. Set WOLFCLU_LARGE_HASH_ALG= to test a single + different algorithm, or WOLFCLU_LARGE_HASH_ALG=all to test every + supported algorithm. + """ + + LARGE_FILE_SIZE = 4_831_838_208 # 4.5 GiB, well above UINT32_MAX + CANDIDATE_ALGS = ["md5", "sha", "sha256", "sha384", "sha512"] + DEFAULT_ALG = "sha256" + + @classmethod + def _probe_supported(cls, algs): + supported = [] + for alg in algs: + r = run_wolfssl(alg, CERT_FILE) + if r.returncode == 0: + supported.append(alg) + return supported + + @classmethod + def setUpClass(cls): + if not os.path.isdir(CERTS_DIR): + raise unittest.SkipTest("certs directory not found") + + requested = os.environ.get("WOLFCLU_LARGE_HASH_ALG", cls.DEFAULT_ALG) + if requested == "all": + cls.algs = cls._probe_supported(cls.CANDIDATE_ALGS) + else: + cls.algs = cls._probe_supported([requested]) + if not cls.algs: + raise unittest.SkipTest( + "no supported hash algorithm for " + "WOLFCLU_LARGE_HASH_ALG={}".format(requested)) + + cls._tmpdir = tempfile.mkdtemp(prefix="wolfclu-large-hash-") + cls.original = os.path.join(cls._tmpdir, "original.bin") + cls.tampered = os.path.join(cls._tmpdir, "tampered.bin") + try: + for p in (cls.original, cls.tampered): + with open(p, "wb") as f: + f.truncate(cls.LARGE_FILE_SIZE) + with open(cls.tampered, "r+b") as f: + f.seek(-1, os.SEEK_END) + f.write(b"X") + except OSError as e: + shutil.rmtree(cls._tmpdir, ignore_errors=True) + raise unittest.SkipTest("could not create sparse files: {}".format(e)) + + @classmethod + def tearDownClass(cls): + shutil.rmtree(getattr(cls, "_tmpdir", ""), ignore_errors=True) + + def test_tampered_last_byte_changes_hash(self): + for alg in self.algs: + with self.subTest(alg=alg): + r1 = run_wolfssl(alg, self.original, timeout=1800) + r2 = run_wolfssl(alg, self.tampered, timeout=1800) + self.assertEqual(r1.returncode, 0, r1.stderr) + self.assertEqual(r2.returncode, 0, r2.stderr) + self.assertNotEqual(r1.stdout.strip(), r2.stdout.strip()) + + class HashArgErrorTest(unittest.TestCase): """Argument-handling regression tests.""" diff --git a/wolfclu/clu_header_main.h b/wolfclu/clu_header_main.h index f80ac610..caf6c9f0 100644 --- a/wolfclu/clu_header_main.h +++ b/wolfclu/clu_header_main.h @@ -124,6 +124,7 @@ extern "C" { #define MAX_TERM_WIDTH 80 #define MAX_THREADS 64 #define MAX_STDINSZ 8192 +#define MAX_IO_CHUNK_SZ 4096 /* I/O chunk size for streaming reads */ #ifndef MAX_FILENAME_SZ #define MAX_FILENAME_SZ 256 #endif