Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a27998a
F-10431: map non-ENOENT store read-open failures to I/O error
danielinux Sep 17, 2026
f999b6c
F-10457: reject store directories that are not private
danielinux Sep 17, 2026
551bcea
F-8726: return commit/close status from wolfPSA_Store_Close
danielinux Sep 17, 2026
3eaf694
test: ignore new regression test binaries
danielinux Sep 17, 2026
b6d7d31
F-11571: reject unconsumed steps in HKDF-Expand validation
danielinux Sep 17, 2026
94b14b0
F-11572: reject repeated single-use steps in all KDF validators
danielinux Sep 17, 2026
42cd2d8
F-11573: enforce PSK-TO-MS PSK size limit and 16-bit premaster bound
danielinux Sep 17, 2026
e34d843
F-10453: zeroize KDF output on mid-stream error
danielinux Sep 17, 2026
112b95d
F-13856: reject zero-length key import
danielinux Sep 17, 2026
54a4249
F-13859: permit cross-lifetime psa_copy_key
danielinux Sep 17, 2026
1e717da
test: add make unit-run and gcov coverage (make cov)
danielinux Sep 17, 2026
6553cdf
F-13878: require a constant-time AES backend
danielinux Sep 17, 2026
a62cf8e
Fix store/KDF defects found in independent review
danielinux Sep 17, 2026
dbef108
build-config-matrix: define WC_AES_BITSLICED, drop aes-ecb lane
danielinux Sep 17, 2026
b2df06a
psa_ecc: allow all-zero ECDSA digest, enforce as a wolfPSA requirement
danielinux Sep 17, 2026
f8c93dd
psa_copy_key: reject cross-lifetime copy (revert F-13859)
danielinux Sep 17, 2026
ba7c4ce
psa_import_key: validate ECC bits/curve match
danielinux Sep 17, 2026
503f7e6
zephyr: enable WC_AES_BITSLICED consttime AES
danielinux Sep 17, 2026
d6059f8
psa_import_key: validate ECC public-key encoding; scope config guards
danielinux Sep 17, 2026
8b23bed
zephyr: fix psa_tls_coexist consttime AES selection
danielinux Sep 18, 2026
fe5fd99
harden config guards; family-specific X448/Ed448 key lengths
danielinux Sep 18, 2026
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
2 changes: 0 additions & 2 deletions .github/workflows/build-config-matrix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,6 @@ jobs:
modifiers: "-WOLFSSL_AES_CFB"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deleting the aes-ecb lane removes a configuration we used to support rather than fixing it. wolfCrypt hard-errors on WC_AES_BITSLICED without HAVE_AES_ECB, and this PR requires WC_AES_BITSLICED whenever AES is present, so "AES on, HAVE_AES_ECB off" is now impossible either way round.

If we want it back, HAVE_AES_ECB comes from the BASELINE list in build-test/build-variant.sh:59, so WC_AES_BITSLICED needs to move into that list for the -FLAG modifier to strip it. Otherwise let's state in user_settings.h and CHANGELOG.md that HAVE_AES_ECB is now a hard requirement. Worth adding a NO_AES lane too - the guard's own !defined(NO_AES) escape hatch is currently untested.

Suggested change
modifiers: "-WOLFSSL_AES_CFB"
- name: aes-ecb
modifiers: "-HAVE_AES_ECB -WC_AES_BITSLICED"

- name: aes-ofb
modifiers: "-WOLFSSL_AES_OFB"
- name: aes-ecb
modifiers: "-HAVE_AES_ECB"
- name: des-ecb
modifiers: "-WOLFSSL_DES_ECB"
- name: cmac
Expand Down
20 changes: 19 additions & 1 deletion .github/workflows/test-psa-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ jobs:
psa_ecc_ecdh_curve_test
psa_xof_output_wrap_test
psa_kdf_length_check_test
psa_kdf_expand_context_test
psa_kdf_repeat_step_test
psa_kdf_psk_to_ms_size_test
psa_mldsa_det_sign_test
psa_mldsa_any_hash_test
psa_ecc_curve_caps_test
Expand All @@ -125,7 +128,13 @@ jobs:
psa_pqc_export_seed_test
psa_key_declared_bits_test
psa_import_key_probe_test
psa_store_commit_test
psa_store_read_open_test
psa_store_dir_validation_test
psa_devid_cryptocb_test
psa_kdf_zeroize_output_test
psa_import_zero_length_test
psa_copy_key_cross_lifetime_test

- name: Run PSA API tests
env:
Expand Down Expand Up @@ -155,6 +164,9 @@ jobs:
psa_ecc_ecdh_curve_test \
psa_xof_output_wrap_test \
psa_kdf_length_check_test \
psa_kdf_expand_context_test \
psa_kdf_repeat_step_test \
psa_kdf_psk_to_ms_size_test \
psa_mldsa_det_sign_test \
psa_mldsa_any_hash_test \
psa_ecc_curve_caps_test \
Expand All @@ -173,7 +185,13 @@ jobs:
psa_pqc_export_seed_test \
psa_key_declared_bits_test \
psa_import_key_probe_test \
psa_devid_cryptocb_test; do
psa_store_commit_test \
psa_store_read_open_test \
psa_store_dir_validation_test \
psa_devid_cryptocb_test \
psa_kdf_zeroize_output_test \
psa_import_zero_length_test \
psa_copy_key_cross_lifetime_test; do
echo "=== $t ==="
rm -rf test/.store
./test/$t
Expand Down
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
build
# gcov coverage artifacts (make cov)
*.gcda
*.gcno
src/.deps
libwolfpsa.a
test-psa-api/psa-arch-tests
Expand Down Expand Up @@ -67,3 +70,12 @@ test/psa_devid_cryptocb_test
test/build-probe
test/build-cryptocb
test/build-cryptocb-asan
test/psa_store_commit_test
test/psa_store_dir_validation_test
test/psa_store_read_open_test
test/psa_kdf_expand_context_test
test/psa_kdf_repeat_step_test
test/psa_kdf_psk_to_ms_size_test
test/psa_kdf_zeroize_output_test
test/psa_import_zero_length_test
test/psa_copy_key_cross_lifetime_test
116 changes: 115 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,18 @@ endif
CFLAGS += $(DEBUG_FLAGS) $(SANITIZE_FLAGS)
LDFLAGS += $(SANITIZE_FLAGS)

.PHONY: all clean psa-objects
# gcov coverage (make cov): instrument the library and the unit tests, run
# the tests, and emit an HTML report of which lines of src/*.c they cover.
# The unit tests link the instrumented shared library, so the runtime .gcda
# files land next to the PIC objects in build/obj.pic.
COV_DIR := build/coverage
OPEN_CMD := $(shell command -v open >/dev/null 2>&1 && echo open || echo xdg-open)
ifeq ($(COV),1)
CFLAGS += --coverage
LDFLAGS += --coverage
endif

.PHONY: all clean psa-objects unit-run run-tests cov covclean

all: $(LIBNAME) $(SHLIBNAME)

Expand Down Expand Up @@ -135,6 +146,109 @@ $(OBJDIR_PIC)/wolfcrypt_%.o: $(WOLFSSL_PATH)/wolfcrypt/src/%.c
@mkdir -p $(OBJDIR_PIC)
$(CC) $(CPPFLAGS) $(DEPFLAGS) $(CFLAGS) -fPIC -c $< -o $@

# The unit tests, in the order CI runs them (test-psa-api.yml). The servers
# (psa_tls_client, psa_tls_server), the benchmark, and psa_crypto_init_test
# (driven separately) are not unit tests and are not listed.
UNIT_TESTS := psa_api_test \
psa_aead_multipart_test \
psa_copy_key_narrowing_test \
psa_ecc_bit_inference_test \
psa_des3_stack_scrub_test \
psa_ecc_curve_id_test \
psa_random_size_test \
psa_rsa_pss_interop_test \
psa_mldsa_test \
psa_mlkem_test \
psa_xof_test \
psa_key_wrap_test \
psa_sign_context_test \
psa_lms_xmss_verify_test \
psa_ascon_xchacha_test \
psa_sp800_108_test \
psa_14_misc_test \
psa_xof_input_wrap_test \
psa_pbkdf2_cmac_test \
psa_kdf_input_key_test \
psa_ecc_verify_curve_test \
psa_ecc_ecdh_curve_test \
psa_xof_output_wrap_test \
psa_kdf_length_check_test \
psa_kdf_expand_context_test \
psa_kdf_repeat_step_test \
psa_kdf_psk_to_ms_size_test \
psa_mldsa_det_sign_test \
psa_mldsa_any_hash_test \
psa_ecc_curve_caps_test \
psa_xof_no_backend_test \
psa_ecc_sig_len_test \
psa_xof_set_context_test \
psa_cipher_inplace_test \
psa_cipher_overlap_test \
psa_des3_pkcs7_test \
psa_eddsa_mont_export_test \
psa_eddsa_mont_gen_test \
psa_pure_eddsa_context_test \
psa_sign_hash_eddsa_test \
psa_key_infer_bits_test \
psa_cipher_oneshot_len_test \
psa_pqc_export_seed_test \
psa_key_declared_bits_test \
psa_import_key_probe_test \
psa_store_commit_test \
psa_store_read_open_test \
psa_store_dir_validation_test \
psa_devid_cryptocb_test \
psa_kdf_zeroize_output_test \
psa_import_zero_length_test \
psa_copy_key_cross_lifetime_test

# Run the unit test loop from the repo root (psa_rsa_pss_interop_test reads
# its certificate relative to the root). Assumes the tests are already built.
run-tests:
@for t in $(UNIT_TESTS); do \
echo "=== $$t ==="; \
rm -rf test/.store; \
./test/$$t || exit 1; \
done

# Build the library and the unit tests, then run every unit test.
unit-run: all
@$(MAKE) -C test $(UNIT_TESTS)
@$(MAKE) run-tests

# Build everything with gcov instrumentation, run the unit tests, and emit an
# HTML coverage report for src/*.c (the bundled wolfCrypt sources are
# excluded by the -f filter). The instrumented objects are dropped once the
# report exists: a later non-coverage build would otherwise reuse them and
# fail to link (undefined __gcov_init). The report itself is kept.
cov:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make cov strands the tree when a test fails. run-tests does ./test/$$t || exit 1, so the first failure aborts the recipe before gcovr runs and before the instrumented objects are removed. build/obj and build/obj.pic keep their --coverage objects, the next plain make sees them newer than their sources and links them without --coverage, and you get undefined _gcov* symbols. make covclean does not help - it only removes .gcda/.gcno. The comment at 221-223 names this hazard but nothing guards against it.

Could we record the failure, still generate the report, drop the instrumented objects, then re-raise at the end? Three smaller ones while you are in here: gcovr has no preflight and is documented nowhere in the repo, so a missing one costs a full clean rebuild plus 52 binaries before it fails; --gcov-ignore-errors=all can hide a broken run and is worth narrowing; and the OPEN_CMD macOS branch at line 110 is unreachable because the guard only fires on X11/Wayland. test/Makefile clean also leaves behind the gcov artifacts it created under test/psa_server.

Suggested change
cov:
@$(MAKE) run-tests || touch $(BUILD_DIR)/.cov-tests-failed

@$(MAKE) clean
@$(MAKE) -C test clean
@$(MAKE) all COV=1
@$(MAKE) -C test $(UNIT_TESTS) COV=1
@$(MAKE) run-tests
@mkdir -p $(COV_DIR)
@echo "[COV] gcovr html"
@gcovr -r . -f '^src/.*\.c$$' \
--gcov-ignore-errors=all \
--html-medium-threshold 60 \
--html-high-threshold 80 \
--html-details -o $(COV_DIR)/index.html
@echo "[COV] report: $(COV_DIR)/index.html"
@echo "[COV] dropping instrumented objects"
@rm -rf $(OBJDIR) $(OBJDIR_PIC) $(LIBNAME) $(SHLIBNAME)
@$(MAKE) -C test clean
@if [ -n "$$DISPLAY" ] || [ -n "$$WAYLAND_DISPLAY" ]; then \
$(OPEN_CMD) $(COV_DIR)/index.html || true; \
fi

# Remove gcov artifacts and the coverage report.
covclean:
rm -f $(OBJDIR)/*.gcda $(OBJDIR)/*.gcno \
$(OBJDIR_PIC)/*.gcda $(OBJDIR_PIC)/*.gcno
rm -f test/psa_server/*.gcda test/psa_server/*.gcno
rm -rf $(COV_DIR)

clean:
rm -rf $(BUILD_DIR) $(LIBNAME) $(SHLIBNAME)

Expand Down
9 changes: 9 additions & 0 deletions build-test/user_settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,14 @@
#define SINGLE_THREADED
#define WOLFSSL_PSA_ENGINE
#define NO_DSA
/* Constant-time AES backend (F-13878): the default software AES uses

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These comment blocks say "psa_aead.c fails the build if neither it nor WOLFSSL_AESNI is selected", but the guard tests only !defined(WC_AES_BITSLICED) - a WOLFSSL_AESNI build without it still fails. The same wording is in build-test/user_settings.h, zephyr/user_settings_example.h, zephyr/tests/psa_tls_coexist/user_settings.h and its file header, and prj.conf. Someone will read it, set WOLFSSL_AESNI, and hit the same #error.

Two other things in the same text: the default path is not unmitigated - wolfCrypt already prefetches every table cache line before each block unless WC_NO_CACHE_RESISTANT is set - and WC_AES_BITSLICED is not the only constant-time option; WOLFSSL_AES_TOUCH_LINES keeps Aes at 416 bytes instead of 123,296.

Separately, these are long for what they are. A 12-line theory-of-operation block above the #error and a 4-line justification above a single #define in four settings files is more than a config header should carry - one line naming the constraint, and the rest in the commit message. Same for zephyr/README.md:76-97, which still documents the config this PR makes unbuildable and still says HAVE_HASHDRBG is the only hard requirement.

Suggested change
/* Constant-time AES backend (F-13878): the default software AES uses
#error "wolfPSA requires WC_AES_BITSLICED. WOLFSSL_AESNI is NOT an accepted substitute: wolfCrypt falls back to the non-constant-time T-table AesSetKey_C() path when AES-NI is unavailable at runtime."

* secret-indexed T-table loads, a cache-timing channel. WC_AES_BITSLICED is
* the portable consttime core; psa_aead.c fails the build if neither it nor
* WOLFSSL_AESNI is selected. */
#define WC_AES_BITSLICED
/* psa_sign_hash()/psa_verify_hash() must accept an all-zero digest (PSA
* treats the hash as opaque bytes); wolfCrypt rejects it by default, so opt
* out. psa_ecc.c #errors when HAVE_ECC is on and this is undefined. */
#define WC_ALLOW_ECC_ZERO_HASH

#endif /* WOLFSSL_USER_SETTINGS_H */
15 changes: 15 additions & 0 deletions src/psa_aead.c
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,21 @@

#include <wolfssl/wolfcrypt/settings.h>

/* The AEAD/CMAC/KDF paths process secret-derived AES keys. Require the
* constant-time software backend (WC_AES_BITSLICED): the default software
* fallback uses secret-indexed T-table loads (a cache-timing channel), and
* WOLFSSL_AESNI alone is not sufficient because wolfCrypt falls back to
* AesSetKey_C() (the same non-constant-time T-table path) when runtime
* AES-NI is unavailable. WC_AES_BITSLICED is the consttime fallback that
* covers both the no-AES-NI and the AES-NI-unavailable cases. Fail the build
* if it is not selected, but only when the PSA engine and an AES path are
* actually active: this file is otherwise inert (no PSA engine) or its AES
* code is excluded (NO_AES), and compiling this inactive source must not
* fail. */
#if defined(WOLFSSL_PSA_ENGINE) && !defined(NO_AES) && !defined(WC_AES_BITSLICED)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This #error locks out every hardware AES backend. Both the T-table core and the bitsliced core live inside #ifdef NEED_AES_TABLES in wolfcrypt/src/aes.c, which WOLFSSL_ARMASM, WOLFSSL_ESP32_CRYPT, FREESCALE_LTC, WOLFSSL_SILABS_SE_ACCEL, WOLFSSL_PSOC6_CRYPTO, WOLFSSL_AFALG, WOLFSSL_DEVCRYPTO_AES, WOLFSSL_RISCV_ASM and WOLF_CRYPTO_CB_ONLY_AES never define. Those builds have no secret-indexed T-table at all, so there is no channel here to close, but they cannot build without defining WC_AES_BITSLICED.

Defining it there costs them for nothing: bs_key in aes.h is gated on WC_AES_BITSLICED alone, not on NEED_AES_TABLES. Under WOLFSSL_ARMASM sizeof(Aes) goes 944 -> 123,824 while aes.c emits zero bitsliced functions. Can we make the guard accept any of the acceptable answers - a hardware backend, WOLFSSL_AES_TOUCH_LINES (constant cache-line footprint, Aes stays 416 bytes), or WC_AES_BITSLICED - and add an opt-out macro? On a cacheless M0/M3/M4 there is no cache-timing channel in the first place.

Suggested change
#if defined(WOLFSSL_PSA_ENGINE) && !defined(NO_AES) && !defined(WC_AES_BITSLICED)
#if defined(WOLFSSL_PSA_ENGINE) && !defined(NO_AES) && !defined(WC_AES_BITSLICED) \
&& !defined(WOLFSSL_ARMASM) && !defined(WOLFSSL_ESP32_CRYPT) \
&& !defined(WOLFSSL_SILABS_SE_ACCEL) && !defined(FREESCALE_LTC) \
&& !defined(WOLFSSL_PSOC6_CRYPTO) && !defined(WOLFSSL_AFALG) \
&& !defined(WOLFSSL_DEVCRYPTO_AES) && !defined(WOLFSSL_RISCV_ASM) \
&& !defined(WOLF_CRYPTO_CB_ONLY_AES) \
&& !defined(WOLFPSA_ALLOW_NON_CONSTTIME_AES)

#error "wolfPSA needs the consttime AES backend (WC_AES_BITSLICED); WOLFSSL_AESNI alone leaves a non-constant-time runtime fallback"
#endif

#if defined(WOLFSSL_PSA_ENGINE)

#include <psa/crypto.h>
Expand Down
17 changes: 17 additions & 0 deletions src/psa_ecc.c
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,23 @@

#include <wolfssl/wolfcrypt/settings.h>

/* psa_sign_hash()/psa_verify_hash() must accept an all-zero digest: PSA
* treats the hash argument as opaque bytes and ECDSA over e = 0 is
* well-defined. wolfCrypt rejects an all-zero digest by default (a guard
* against uninitialized buffers), which would surface as
* PSA_ERROR_INVALID_ARGUMENT for input the spec requires us to accept.
* WC_ALLOW_ECC_ZERO_HASH opts out of that rejection.
*
* The requirement is on the PSA ECDSA path itself, not on how the wolfCrypt
* config is supplied: any build that compiles the PSA ECC path (PSA engine +
* HAVE_ECC) must define WC_ALLOW_ECC_ZERO_HASH, whether the config comes from
* a WOLFSSL_SETTINGS_FILE, a WOLFSSL_USER_SETTINGS file, or the configure
* system. Builds without the PSA ECC path are unaffected. */
#if defined(WOLFSSL_PSA_ENGINE) && defined(HAVE_ECC) \

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both new guards enforce a project-wide policy from a single translation unit. WC_ALLOW_ECC_ZERO_HASH is consumed only when ecc.c is compiled, and psa_ecc.c never reads it - so in a split build against a prebuilt libwolfssl (which is what test/Makefile's rebuild-wolfssl-psa produces) defining it in wolfPSA's own compile satisfies the #error without changing ecc.c at all, and psa_sign_hash on an all-zero digest still returns ECC_BAD_ARG_E. The guard then reports "configured correctly" for a build that is not.

The AES guard has the same shape: psa_cipher.c, psa_key_derivation.c, psa_key_wrap.c and psa_mac.c all use wc_Aes*/wc_Cmac* and carry no guard. It works today only because the Makefile and zephyr/CMakeLists.txt both glob src/*.c. Could both move into a shared header included by every TU after settings.h, and could the ECC one either key off a symbol wolfCrypt derives from settings.h or be replaced by handling the all-zero digest in psa_ecc.c directly?

&& !defined(WC_ALLOW_ECC_ZERO_HASH)
#error "wolfPSA needs WC_ALLOW_ECC_ZERO_HASH (psa_sign_hash/psa_verify_hash must accept an all-zero digest)"
#endif

#if defined(WOLFSSL_PSA_ENGINE) && defined(HAVE_ECC)

#include <psa/crypto.h>
Expand Down
Loading
Loading