Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions src/crypto/clu_crypto_setup.c
Original file line number Diff line number Diff line change
Expand Up @@ -533,10 +533,15 @@ int wolfCLU_setup(int argc, char** argv, char action)
}
/* if no pwdKey is provided */
else {
/* Pass the pwdKey buffer capacity, NOT &keySize:
* wolfCLU_GetStdinPassword writes the entered length back through
* this pointer, and keySize (the algorithm key size in bits) is
* still needed for key derivation and cleanup below. */
word32 pwdBufSz = (word32)(keySize + block);
WOLFCLU_LOG(WOLFCLU_L0,
"No -pwd flag set, please enter a password to use for"
" encrypting.");
ret = wolfCLU_GetStdinPassword(pwdKey, (word32*)&keySize);
ret = wolfCLU_GetStdinPassword(pwdKey, &pwdBufSz);
pwdKeyChk = 1;
}
}
Expand Down Expand Up @@ -649,10 +654,12 @@ int wolfCLU_setup(int argc, char** argv, char action)
}
/* clear and free data — zero the full allocation, not just the
* keyBytes actually used, so any future code path that writes past
* the cipher key length doesn't leak material across XFREE. */
XMEMSET(key, 0, keySize);
XMEMSET(pwdKey, 0, keySize + block);
XMEMSET(iv, 0, block);
* the cipher key length doesn't leak material across XFREE.
* ForceZero (not XMEMSET) so the compiler can't drop the wipe of these
* soon-to-be-freed key buffers. */
wolfCLU_ForceZero(key, keySize);
wolfCLU_ForceZero(pwdKey, keySize + block);
wolfCLU_ForceZero(iv, block);
wolfCLU_freeBins(pwdKey, iv, key, NULL, NULL);

if (mode != NULL)
Expand Down
5 changes: 4 additions & 1 deletion src/sign-verify/clu_verify.c
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,7 @@ int wolfCLU_verify_signature_ecc(byte* sig, int sigSz, byte* hash, int hashSz,
}
else {
wolfCLU_LogError("Invalid Signature.");
ret = WOLFCLU_FATAL_ERROR;
}
}

Expand Down Expand Up @@ -651,6 +652,7 @@ int wolfCLU_verify_signature_ed25519(byte* sig, int sigSz,
}
else {
wolfCLU_LogError("Invalid Signature.");
ret = WOLFCLU_FATAL_ERROR;
}
}

Expand Down Expand Up @@ -795,14 +797,15 @@ int wolfCLU_verify_signature_dilithium(byte* sig, int sigSz, byte* msg,
}
else {
wolfCLU_LogError("Invalid Signature.");
ret = WOLFCLU_FATAL_ERROR;
}
wc_dilithium_free(key);

#ifdef WOLFSSL_SMALL_STACK
XFREE(key, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER);
#endif

return WOLFCLU_SUCCESS;
return (ret >= 0) ? WOLFCLU_SUCCESS : ret;
#else
(void)sig;
(void)sigSz;
Expand Down
1 change: 1 addition & 0 deletions src/x509/clu_request_setup.c
Original file line number Diff line number Diff line change
Expand Up @@ -1028,6 +1028,7 @@ int wolfCLU_requestSetup(int argc, char** argv)
}
else {
wolfCLU_LogError("verify failed");
ret = WOLFCLU_FATAL_ERROR;
}
}
}
Expand Down
193 changes: 193 additions & 0 deletions tests/encrypt/enc-test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,25 @@

import filecmp
import os
import re
import shutil
import subprocess
import sys
import time
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

# The interactive password prompt only reads from stdin when stdin is a real
# terminal (wolfCLU_GetStdinPassword -> tcgetattr fails on a pipe), so driving
# it requires a pseudo-terminal. The pty module is POSIX-only.
try:
import pty as _pty
HAVE_PTY = True
except ImportError:
HAVE_PTY = False


def run_enc(*args, password=""):
"""Run wolfssl enc with a -k password argument appended."""
Expand Down Expand Up @@ -818,5 +829,187 @@ def test_rand_hex_to_inkey_workflow(self):
"rand-hex -> -inkey workflow did not round-trip")


@unittest.skipUnless(HAVE_PTY, "pty not available (non-POSIX)")
class EncStdinPasswordTest(unittest.TestCase):
"""Interactive stdin-password path of `encrypt` (F-5970).

Running `encrypt` without -pwd/-pass/-key prompts for a password on the
terminal via wolfCLU_GetStdinPassword, which writes the typed length back
through its size pointer. The caller passed &keySize (the algorithm key
size in bits), so keySize was overwritten by the password length. With
-pbkdf2 the EVP derivation length is (keySize+7)/8, collapsing the derived
key to a couple of bytes. These tests drive the prompt over a pty.
"""

# >= 14 chars to satisfy the FIPS HMAC minimum (HMAC_FIPS_MIN_KEY) for the
# PBKDF2 path, while still truncating the bit-size keySize far below a real
# cipher key, so the buggy derivation collapses to a few key bytes.
PASSWORD = "correcthorsebatterystaple"
PLAINTEXT = b"F-5970 interactive password regression payload\n"
# AES-256 key length in bytes.
FULL_KEY_BYTES = 32

@classmethod
def setUpClass(cls):
config_log = os.path.join(".", "config.log")
if os.path.isfile(config_log):
with open(config_log) as f:
if "disable-filesystem" in f.read():
raise unittest.SkipTest("filesystem support disabled")

def _cleanup(self, *files):
for f in files:
self.addCleanup(lambda p=f: os.remove(p)
if os.path.exists(p) else None)

def _write_plaintext(self, path):
with open(path, "wb") as f:
f.write(self.PLAINTEXT)

def _encrypt_with_pty_password(self, password, *args, timeout=30):
"""Run `wolfssl encrypt <args>` and type `password` at the prompt over
a pseudo-terminal. Returns (exit_code, combined_output)."""
import select
import signal

# fgets() reads a line, so the password must be newline-terminated or
# the child blocks forever waiting for end-of-line.
line = (password + "\n").encode()
argv = [WOLFSSL_BIN, "encrypt"] + list(args)
pid, fd = _pty.fork()
if pid == 0:
# Child: become the encrypt process with the pty as its stdin.
try:
os.execvpe(argv[0], argv, os.environ)
except Exception:
pass
os._exit(127)

output = b""
wrote = False
deadline = time.time() + timeout
try:
while time.time() < deadline:
r, _, _ = select.select([fd], [], [], 0.5)
if fd in r:
try:
data = os.read(fd, 1024)
except OSError:
break # slave closed (child exited)
if not data:
break
output += data
if not wrote and b"Input Password" in output:
os.write(fd, line)
wrote = True
elif not wrote:
# Prompt may not have matched yet; send it anyway.
os.write(fd, line)
wrote = True
finally:
try:
os.close(fd)
except OSError:
pass

# Reap with a bounded wait so a stuck child cannot hang the suite.
end = time.time() + 5
status = None
while time.time() < end:
wpid, st = os.waitpid(pid, os.WNOHANG)
if wpid == pid:
status = st
break
time.sleep(0.05)
if status is None:
try:
os.kill(pid, signal.SIGKILL)
except OSError:
pass
_, status = os.waitpid(pid, 0)

if os.WIFEXITED(status):
code = os.WEXITSTATUS(status)
elif os.WIFSIGNALED(status):
code = -os.WTERMSIG(status)
else:
code = -1
return code, output.decode(errors="replace")

def test_pbkdf2_full_key_derivation(self):
"""A typed password with -pbkdf2 must derive the full cipher key.

Before the fix keySize was overwritten by the password length, so the
`-p` debug print reported a few key bytes instead of 32."""
plain = "f5970_keylen_in.txt"
cipher = "f5970_keylen.bin"
self._cleanup(plain, cipher)
self._write_plaintext(plain)

code, out = self._encrypt_with_pty_password(
self.PASSWORD, "aes-cbc-256", "-pbkdf2", "-p",
"-in", plain, "-out", cipher)
self.assertEqual(code, 0, "encrypt failed: " + out)

m = re.search(r"key\s*\[(\d+)\]", out)
self.assertIsNotNone(m, "no key length in debug output: " + out)
self.assertEqual(int(m.group(1)), self.FULL_KEY_BYTES,
"AES-256 key derived as {} bytes, expected {} "
"(keySize was clobbered)".format(
m.group(1), self.FULL_KEY_BYTES))

def test_pbkdf2_stdin_decrypts_with_pass(self):
"""A file encrypted with a typed password + -pbkdf2 must decrypt with
the same password supplied via -pass (interoperability, F-5970)."""
plain = "f5970_interop_in.txt"
cipher = "f5970_interop.bin"
dec = "f5970_interop_out.txt"
self._cleanup(plain, cipher, dec)
self._write_plaintext(plain)

code, out = self._encrypt_with_pty_password(
self.PASSWORD, "aes-cbc-256", "-pbkdf2",
"-in", plain, "-out", cipher)
self.assertEqual(code, 0, "encrypt failed: " + out)

r = subprocess.run(
[WOLFSSL_BIN, "decrypt", "aes-cbc-256", "-pbkdf2",
"-pass", "pass:" + self.PASSWORD, "-in", cipher, "-out", dec],
capture_output=True, text=True, stdin=subprocess.DEVNULL,
timeout=60)
self.assertEqual(r.returncode, 0,
"decrypt of pbkdf2 stdin-password file failed: "
+ r.stderr)
with open(dec, "rb") as f:
self.assertEqual(f.read(), self.PLAINTEXT,
"decrypted plaintext mismatch")

def test_default_kdf_stdin_decrypts_with_pass(self):
"""Regression guard: the default (BytesToKey) path already interops
because the key length comes from the cipher; the fix must keep it
working."""
plain = "f5970_def_in.txt"
cipher = "f5970_def.bin"
dec = "f5970_def_out.txt"
self._cleanup(plain, cipher, dec)
self._write_plaintext(plain)

code, out = self._encrypt_with_pty_password(
self.PASSWORD, "aes-cbc-256", "-in", plain, "-out", cipher)
self.assertEqual(code, 0, "encrypt failed: " + out)

r = subprocess.run(
[WOLFSSL_BIN, "decrypt", "aes-cbc-256",
"-pass", "pass:" + self.PASSWORD, "-in", cipher, "-out", dec],
capture_output=True, text=True, stdin=subprocess.DEVNULL,
timeout=60)
self.assertEqual(r.returncode, 0,
"decrypt of default stdin-password file failed: "
+ r.stderr)
with open(dec, "rb") as f:
self.assertEqual(f.read(), self.PLAINTEXT,
"decrypted plaintext mismatch")


if __name__ == "__main__":
test_main()
47 changes: 47 additions & 0 deletions tests/genkey_sign_ver/genkey-sign-ver-test.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,36 @@ def _track(self, *files):
for f in files:
_TEMP_FILES.append(f)

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)."""
priv, pub = self._genkey(algo, keybase, fmt, extra_genkey_args,
use_output_flag=use_output_flag)
self._sign(algo, priv, fmt, sig_file)

wrong_msg = keybase + "-wrong-msg.txt"
self._track(wrong_msg)
with open(wrong_msg, "w") as f:
f.write("Totally different data that was never signed\n")

r = run_wolfssl(f"-{algo}", "-verify", "-inkey", pub,
"-inform", fmt, "-sigfile", sig_file,
"-in", wrong_msg, "-pubin")
self.assertNotEqual(r.returncode, 0,
"{} verify of a signature over different data "
"should fail".format(algo))
self.assertGreaterEqual(r.returncode, 0,
"{} bad verify crashed with signal "
"{}".format(algo, r.returncode))

def _genkey(self, algo, keybase, fmt, extra_args=None,
use_output_flag=False):
args = ["-genkey", algo]
Expand Down Expand Up @@ -135,6 +165,10 @@ def test_ed25519_pem(self):
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)."""
self._gen_sign_badverify("ed25519", "edkey-bad", "ed-bad.sig", "der")

def test_ed25519_signature_size(self):
"""ED25519 signatures must be exactly 64 bytes."""
priv, pub = self._genkey("ed25519", "edkey-sztest", "der",
Expand Down Expand Up @@ -172,6 +206,10 @@ def test_ecc_der(self):
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)."""
self._gen_sign_badverify("ecc", "ecckey-bad", "ecc-bad.sig", "der")

def test_ecc_der_key_size_and_roundtrip(self):
"""Regression: ECC DER private key must be reasonably sized, and the
full sign/verify round-trip must succeed on the generated keypair."""
Expand Down Expand Up @@ -309,6 +347,15 @@ def test_dilithium_pem(self):
extra_genkey_args=["-level", str(level)],
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)."""
for level in [2, 3, 5]:
with self.subTest(level=level):
self._gen_sign_badverify(
"dilithium", "mldsakey-bad", "mldsa-bad.sig", "der",
extra_genkey_args=["-level", str(level)],
use_output_flag=True)

def test_output_pub_only(self):
pub = "mldsakey_pub.pub"
priv = "mldsakey_pub.priv"
Expand Down
Loading
Loading