diff --git a/.github/workflows/cose-hpke.yml b/.github/workflows/cose-hpke.yml new file mode 100644 index 0000000..922324d --- /dev/null +++ b/.github/workflows/cose-hpke.yml @@ -0,0 +1,109 @@ +name: Experimental COSE-HPKE + +on: + push: + branches: [ 'main', 'release/**' ] + pull_request: + branches: [ '**' ] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + discover: + uses: ./.github/workflows/_resolve-wolfssl.yml + + cose-hpke: + name: HPKE P0 (${{ matrix.wolfssl-ref }}) + needs: discover + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + wolfssl-ref: ${{ fromJson(needs.discover.outputs.refs) }} + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y autoconf automake libtool + + - name: Build HPKE-enabled wolfSSL + run: | + git clone --depth 1 --branch "${{ matrix.wolfssl-ref }}" \ + https://github.com/wolfSSL/wolfssl.git "$HOME/wolfssl" + cd "$HOME/wolfssl" + ./autogen.sh + CFLAGS="-fsanitize=address -fno-omit-frame-pointer -g" \ + LDFLAGS="-fsanitize=address" \ + ./configure --enable-ecc --enable-ed25519 --enable-ed448 \ + --enable-curve25519 --enable-curve448 \ + --enable-ecccustcurves=all \ + --enable-aesgcm --enable-aesccm \ + --enable-sha384 --enable-sha512 \ + --enable-keygen --enable-hkdf --enable-aeskeywrap \ + --enable-chacha --enable-poly1305 --enable-mldsa \ + --enable-rsapss --enable-hpke \ + --prefix="$HOME/wolfssl-install" + make -j"$(nproc)" + make install + + - name: Run experimental HPKE unit tests + run: | + export WOLFSSL_DIR="$HOME/wolfssl-install" + export LD_LIBRARY_PATH="$WOLFSSL_DIR/lib" + HPKE_FLAGS="-DWOLFCOSE_EXPERIMENTAL \ + -DWOLFCOSE_ENABLE_HPKE_0_ENCRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_DECRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT" + WOLFCOSE_CFLAGS="-std=c99 -DHAVE_ANONYMOUS_INLINE_AGGREGATES=1 \ + -Og -g -fsanitize=address -fno-omit-frame-pointer \ + -Wall -Wextra -Wpedantic -Wshadow -Wconversion -Wvla \ + -Werror=vla -DHAVE_ECC_KOBLITZ -I./include \ + -I$WOLFSSL_DIR/include $HPKE_FLAGS" + make clean + make test CFLAGS="$WOLFCOSE_CFLAGS" \ + LDFLAGS="-fsanitize=address -L$WOLFSSL_DIR/lib -lwolfssl" + + - name: Run experimental HPKE example + run: | + export WOLFSSL_DIR="$HOME/wolfssl-install" + export LD_LIBRARY_PATH="$WOLFSSL_DIR/lib" + WOLFCOSE_CFLAGS="-std=c99 -DHAVE_ANONYMOUS_INLINE_AGGREGATES=1 \ + -Og -g -fsanitize=address -fno-omit-frame-pointer \ + -Wall -Wextra -Wpedantic -Wshadow -Wconversion -Wvla \ + -Werror=vla -I./include -I$WOLFSSL_DIR/include" + make clean + make hpke-demo CFLAGS="$WOLFCOSE_CFLAGS" \ + LDFLAGS="-fsanitize=address -L$WOLFSSL_DIR/lib -lwolfssl" + + - name: Run experimental HPKE strict C99 check + run: | + export WOLFSSL_DIR="$HOME/wolfssl-install" + make c99-hpke-check WOLFSSL_INC="$WOLFSSL_DIR/include" + + - name: Run experimental HPKE CLI commands + run: | + export WOLFSSL_DIR="$HOME/wolfssl-install" + export LD_LIBRARY_PATH="$WOLFSSL_DIR/lib" + HPKE_FLAGS="-DWOLFCOSE_EXPERIMENTAL \ + -DWOLFCOSE_ENABLE_HPKE_0_ENCRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_DECRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT" + WOLFCOSE_CFLAGS="-std=c99 -DHAVE_ANONYMOUS_INLINE_AGGREGATES=1 \ + -Og -g -fsanitize=address -fno-omit-frame-pointer \ + -Wall -Wextra -Wpedantic -Wshadow -Wconversion -Wvla \ + -Werror=vla -I./include -I$WOLFSSL_DIR/include $HPKE_FLAGS" + make clean + EXPECT_HPKE=true EXPECT_PQC=true make cmdline-test \ + CFLAGS="$WOLFCOSE_CFLAGS" \ + LDFLAGS="-fsanitize=address -L$WOLFSSL_DIR/lib -lwolfssl" diff --git a/.github/workflows/misra-2012.yml b/.github/workflows/misra-2012.yml index 79b8914..5a6614f 100644 --- a/.github/workflows/misra-2012.yml +++ b/.github/workflows/misra-2012.yml @@ -43,7 +43,7 @@ jobs: uses: actions/cache@v4 with: path: ~/wolfssl-install - key: wolfssl-ubuntu-24.04-v5-lms-${{ env.WOLFSSL_REF }} + key: wolfssl-ubuntu-24.04-v6-lms-hpke-${{ env.WOLFSSL_REF }} - name: Build wolfSSL if: steps.cache-wolfssl.outputs.cache-hit != 'true' @@ -59,7 +59,7 @@ jobs: --enable-curve25519 --enable-aesgcm --enable-aesccm \ --enable-sha384 --enable-sha512 --enable-keygen \ --enable-rsapss --enable-chacha --enable-poly1305 \ - --enable-mldsa --enable-lms \ + --enable-mldsa --enable-lms --enable-hpke \ --prefix=$HOME/wolfssl-install make -j$(nproc) make install @@ -82,6 +82,11 @@ jobs: # a full production build does. Only true user inputs are supplied. cc -std=c99 -Wall -Wextra -Wpedantic -Werror \ -DWOLFCOSE_FLOAT -DWOLFCOSE_ENABLE_EXT_SIGN \ + -DWOLFCOSE_EXPERIMENTAL \ + -DWOLFCOSE_ENABLE_HPKE_0_ENCRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_DECRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT \ -I include -I "$HOME/wolfssl-install/include" \ -fsyntax-only tests/misra_consumer.c @@ -108,8 +113,14 @@ jobs: -DWOLFSSL_AES_DIRECT \ -DHAVE_HKDF \ -DHAVE_AES_CBC \ + -DHAVE_HPKE \ -DWOLFCOSE_FLOAT \ -DWOLFCOSE_ENABLE_EXT_SIGN \ + -DWOLFCOSE_EXPERIMENTAL \ + -DWOLFCOSE_ENABLE_HPKE_0_ENCRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_DECRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT \ -I include -I src -I $HOME/wolfssl-install/include \ src/*.c \ tests/misra_consumer.c \ diff --git a/Makefile b/Makefile index 6d2be2a..8fdb75a 100644 --- a/Makefile +++ b/Makefile @@ -99,6 +99,7 @@ TOOL_BIN = tools/wolfcose_tool DEMO_SRC = examples/lifecycle_demo.c DEMO_BIN = examples/lifecycle_demo ENC_DEMO = examples/encrypt0_demo +HPKE_DEMO = examples/hpke_demo MAC_DEMO = examples/mac0_demo SIGN1_DEMO = examples/sign1_demo LEANV_DEMO = examples/sign1_verify_lean @@ -121,7 +122,7 @@ SCEN_IOTFLEET = examples/scenarios/iot_fleet_config SCEN_SENSOR = examples/scenarios/sensor_attestation SCEN_BROADCAST = examples/scenarios/group_broadcast_mac -.PHONY: all shared test pkg-config-test ecdsa-policy-test rsapss-policy-test countersign-config-test zero-alloc-check zeroize-test ecc-import-policy-test ext-sign-test ext-sign-demo ext-sign-force-failure coverage tool tool-test cmdline-test demo demos lean-verify mldsa-demo mldsa-verify lms-demo lms-verify comprehensive scenarios interop-tcose tcose-upstream interop-go-cose interop-python-cwt interop-rust-coset c99-check c99-check-lms experimental-check clean FORCE +.PHONY: all shared test pkg-config-test ecdsa-policy-test rsapss-policy-test countersign-config-test zero-alloc-check zeroize-test ecc-import-policy-test ext-sign-test ext-sign-demo ext-sign-force-failure coverage tool tool-test cmdline-test demo demos hpke-demo lean-verify mldsa-demo mldsa-verify lms-demo lms-verify comprehensive scenarios interop-tcose tcose-upstream interop-go-cose interop-python-cwt interop-rust-coset c99-check c99-check-lms c99-hpke-check experimental-check clean FORCE # --- Core library --- all: $(LIB_A) @@ -148,7 +149,14 @@ src/%.o: src/%.c src/wolfcose_internal.h include/wolfcose/wolfcose.h $(BUILD_CON # under test. It must exercise the local ECDSA Sign1 signing path even when the # caller is testing a no-ECDSA or verify-only configuration. ECDSA_POLICY_OPTS ?= -include wolfssl/options.h -ECDSA_POLICY_BASE_FLAGS = $(CFLAGS) -x c -fsyntax-only -Wno-error \ +WOLFCOSE_POLICY_DISABLE_HPKE_FLAGS = -UWOLFCOSE_ENABLE_HPKE_0 \ + -UWOLFCOSE_ENABLE_HPKE_0_KE \ + -UWOLFCOSE_ENABLE_HPKE_0_ENCRYPT \ + -UWOLFCOSE_ENABLE_HPKE_0_DECRYPT \ + -UWOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT \ + -UWOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT +ECDSA_POLICY_BASE_FLAGS = $(CFLAGS) $(WOLFCOSE_POLICY_DISABLE_HPKE_FLAGS) \ + -x c -fsyntax-only -Wno-error \ $(ECDSA_POLICY_OPTS) -DHAVE_ECC \ -UWOLFCOSE_ENABLE_DETERMINISTIC_ECDSA \ -UWOLFCOSE_NO_ES256 -UWOLFCOSE_NO_SIGN1 \ @@ -157,7 +165,8 @@ ECDSA_POLICY_BASE_FLAGS = $(CFLAGS) -x c -fsyntax-only -Wno-error \ -UWOLFCOSE_LEAN_MLDSA \ -UWOLFCOSE_LEAN_VERIFY_LMS \ -UWOLFCOSE_LEAN_LMS -ECDSA_POLICY_NO_SUPPORT_FLAGS = $(CFLAGS) -x c -fsyntax-only -Wno-error \ +ECDSA_POLICY_NO_SUPPORT_FLAGS = $(CFLAGS) $(WOLFCOSE_POLICY_DISABLE_HPKE_FLAGS) \ + -x c -fsyntax-only -Wno-error \ -DWOLFSSL_NO_OPTIONS_H -DHAVE_ECC \ -UWOLFSSL_ECDSA_DETERMINISTIC_K \ -UWOLFSSL_ECDSA_DETERMINISTIC_K_VARIANT \ @@ -272,18 +281,21 @@ ecdsa-policy-test: echo "PASS: optional ECDSA nonce policy enforced" rsapss-policy-test: - $(CC) $(CFLAGS) -Werror=unused-function -fsyntax-only \ + $(CC) $(CFLAGS) $(WOLFCOSE_POLICY_DISABLE_HPKE_FLAGS) \ + -Werror=unused-function -fsyntax-only \ -DWOLFCOSE_NO_SIGN1 -DWOLFCOSE_NO_SIGN $(SRC) - $(CC) $(CFLAGS) -x c -fsyntax-only -DWOLFSSL_NO_OPTIONS_H \ + $(CC) $(CFLAGS) $(WOLFCOSE_POLICY_DISABLE_HPKE_FLAGS) \ + -x c -fsyntax-only -DWOLFSSL_NO_OPTIONS_H \ -DWC_RSA_PSS -DWOLFCOSE_NO_KEY_ENCODE \ -DWOLFCOSE_ENABLE_RSAPSS $(SRC) - $(CC) $(CFLAGS) -x c -fsyntax-only -DWOLFSSL_NO_OPTIONS_H \ + $(CC) $(CFLAGS) $(WOLFCOSE_POLICY_DISABLE_HPKE_FLAGS) \ + -x c -fsyntax-only -DWOLFSSL_NO_OPTIONS_H \ -DWC_RSA_PSS -DWOLFCOSE_LEAN_VERIFY \ -DWOLFCOSE_ENABLE_RSAPSS $(SRC) @set -e; \ log_file=$$(mktemp "$${TMPDIR:-/tmp}/wolfcose-rsapss.XXXXXX"); \ trap 'rm -f "$$log_file"' 0 1 2 3 15; \ - if $(CC) $(CFLAGS) -x c -fsyntax-only -DWOLFSSL_NO_OPTIONS_H \ + if $(CC) $(CFLAGS) $(WOLFCOSE_POLICY_DISABLE_HPKE_FLAGS) -x c -fsyntax-only -DWOLFSSL_NO_OPTIONS_H \ -UHAVE_ECC -UWOLFSSL_EXPORT_INT \ -DWC_RSA_PSS -DWOLFCOSE_ENABLE_RSAPSS \ -DWOLFSSL_RSA_VERIFY_ONLY -DWOLFCOSE_LEAN_VERIFY \ @@ -293,7 +305,7 @@ rsapss-policy-test: fi; \ grep -q "RSA-PSS key validation requires WOLFSSL_EXPORT_INT" \ "$$log_file" - $(CC) $(CFLAGS) -x c -fsyntax-only -DWOLFSSL_NO_OPTIONS_H \ + $(CC) $(CFLAGS) $(WOLFCOSE_POLICY_DISABLE_HPKE_FLAGS) -x c -fsyntax-only -DWOLFSSL_NO_OPTIONS_H \ -UHAVE_ECC -UWOLFSSL_EXPORT_INT \ -DWC_RSA_PSS -DWOLFCOSE_ENABLE_RSAPSS \ -DWOLFSSL_RSA_VERIFY_ONLY -DWOLFSSL_EXPORT_INT \ @@ -302,7 +314,7 @@ rsapss-policy-test: log_file=$$(mktemp "$${TMPDIR:-/tmp}/wolfcose-rsapss.XXXXXX"); \ trap 'rm -f "$$log_file"' 0 1 2 3 15; \ for backend in WOLF_CRYPTO_CB WOLFSSL_MICROCHIP_TA100; do \ - if $(CC) $(CFLAGS) -x c -fsyntax-only -DWOLFSSL_NO_OPTIONS_H \ + if $(CC) $(CFLAGS) $(WOLFCOSE_POLICY_DISABLE_HPKE_FLAGS) -x c -fsyntax-only -DWOLFSSL_NO_OPTIONS_H \ -UHAVE_ECC -UWOLFSSL_EXPORT_INT \ -DWC_RSA_PSS -DWOLFCOSE_ENABLE_RSAPSS \ -DWOLFSSL_RSA_VERIFY_ONLY -D$$backend \ @@ -459,6 +471,19 @@ demos: $(LIB_A) ./$(MAC_DEMO) ./$(SIGN1_DEMO) +# --- Experimental COSE-HPKE example --- +# The library is compiled directly because HPKE is intentionally opt-in and +# the default libwolfcose.a does not contain the experimental paths. +hpke-demo: + $(CC) $(CFLAGS) -DWOLFCOSE_EXPERIMENTAL \ + -DWOLFCOSE_ENABLE_HPKE_0_ENCRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_DECRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT \ + -o $(HPKE_DEMO) $(HPKE_DEMO).c $(SRC) $(LDFLAGS) $(LDLIBS) + @echo "=== Running experimental COSE-HPKE example ===" + ./$(HPKE_DEMO) + # --- Lean verify-only example (WOLFCOSE_LEAN_VERIFY) --- # Compiles the wolfCOSE sources directly with the lean macro instead of the full # prebuilt library, so the example exercises the minimal verify-only profile. @@ -653,7 +678,7 @@ C99_FLAGS = -std=c99 -pedantic-errors -Werror -Wall -Wextra -Wshadow -Wconversio -Wvla -DHAVE_ANONYMOUS_INLINE_AGGREGATES=1 \ -I./include $(C99_WOLFSSL_CFLAGS) $(EXTRA_CFLAGS) C99_SRC = $(SRC) $(TEST_SRC) $(TOOL_SRC) $(DEMO_SRC) \ - $(ENC_DEMO).c $(MAC_DEMO).c $(SIGN1_DEMO).c \ + $(ENC_DEMO).c $(HPKE_DEMO).c $(MAC_DEMO).c $(SIGN1_DEMO).c \ $(COMP_SIGN).c $(COMP_ENCRYPT).c $(COMP_MAC).c $(COMP_ERRORS).c \ $(SCEN_FIRMWARE).c $(SCEN_MULTIPARTY).c $(SCEN_IOTFLEET).c \ $(SCEN_SENSOR).c $(SCEN_BROADCAST).c $(EXTSIGN_DEMO).c \ @@ -663,6 +688,30 @@ C99_SRC = $(SRC) $(TEST_SRC) $(TOOL_SRC) $(DEMO_SRC) \ # conditionally-compiled translation unit, not just the default subset. C99_CONFIGS = "" "-DWOLFCOSE_FLOAT" "-DWOLFCOSE_ENABLE_EXT_SIGN" \ "-DWOLFCOSE_ENABLE_EXT_SIGN -DWOLFCOSE_NO_EDDSA -DWOLFCOSE_NO_ED448" +HPKE_C99_CONFIG = -DWOLFCOSE_EXPERIMENTAL -DWOLFCOSE_BUILD_TOOL \ + -DWOLFCOSE_ENABLE_HPKE_0_ENCRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_DECRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT +HPKE_C99_CONFIGS = \ + "-DWOLFCOSE_EXPERIMENTAL -DWOLFCOSE_BUILD_TOOL -DWOLFCOSE_ENABLE_HPKE_0" \ + "-DWOLFCOSE_EXPERIMENTAL -DWOLFCOSE_BUILD_TOOL -DWOLFCOSE_ENABLE_HPKE_0_KE" \ + "-DWOLFCOSE_EXPERIMENTAL -DWOLFCOSE_BUILD_TOOL -DWOLFCOSE_ENABLE_HPKE_0_ENCRYPT" \ + "-DWOLFCOSE_EXPERIMENTAL -DWOLFCOSE_BUILD_TOOL -DWOLFCOSE_ENABLE_HPKE_0_DECRYPT" \ + "-DWOLFCOSE_EXPERIMENTAL -DWOLFCOSE_BUILD_TOOL -DWOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT" \ + "-DWOLFCOSE_EXPERIMENTAL -DWOLFCOSE_BUILD_TOOL -DWOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT" \ + "$(HPKE_C99_CONFIG)" \ + "$(HPKE_C99_CONFIG) -DNO_ECC256 -DHAVE_ALL_CURVES -DHAVE_ECC_KOBLITZ" +HPKE_C99_INVALID_CONFIGS = "-DNO_ECC_SECP" "-DECC_MIN_KEY_SZ=384" \ + "-DNO_AES_128" \ + "-DWOLFSSL_USER_SETTINGS -Itests/config/hpke_prereqs -DWOLFCOSE_TEST_NO_HKDF" \ + "-DWOLFSSL_USER_SETTINGS -Itests/config/hpke_prereqs -DWOLFCOSE_TEST_NO_ECC_DHE" +# Prefer the explicitly selected HPKE backend over an unrelated host install. +# Treat backend headers as system headers so this gate judges gated wolfCOSE +# syntax, including when the selected wolfSSL version uses C11 extensions. +# Re-add the common local prefix late because macOS Clang otherwise searches it +# before a caller-selected -isystem directory. +HPKE_C99_FLAGS = $(C99_FLAGS) -Wno-unused-function -isystem /usr/local/include c99-check: @for cfg in $(C99_CONFIGS); do \ @@ -704,6 +753,14 @@ EXP_FLAGS = -std=c99 -pedantic-errors -I./include $(C99_WOLFSSL_CFLAGS) \ # Include settings.h plus one declaration so the stub is a valid C99 TU. EXP_TU = printf '\#include \nint wolfcose_experimental_gate_check;\n' +EXP_HPKE_ENABLES = \ + WOLFCOSE_ENABLE_HPKE_0 \ + WOLFCOSE_ENABLE_HPKE_0_KE \ + WOLFCOSE_ENABLE_HPKE_0_ENCRYPT \ + WOLFCOSE_ENABLE_HPKE_0_DECRYPT \ + WOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT \ + WOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT + experimental-check: @echo " EXP feature without acknowledgement (expect error)" @if $(EXP_TU) | \ @@ -720,14 +777,48 @@ experimental-check: @$(EXP_TU) | \ $(CC) $(EXP_FLAGS) -DWOLFCOSE_ENABLE_EXPERIMENTAL_EXAMPLE \ -DWOLFCOSE_EXPERIMENTAL -fsyntax-only -x c - + @for enable in $(EXP_HPKE_ENABLES); do \ + echo " EXP COSE-HPKE $$enable without acknowledgement (expect error)"; \ + if $(EXP_TU) | $(CC) $(EXP_FLAGS) -D$$enable \ + -fsyntax-only -x c - 2>experimental-check.err; then \ + echo "FAIL: COSE-HPKE $$enable compiled without WOLFCOSE_EXPERIMENTAL"; \ + rm -f experimental-check.err; exit 1; \ + fi; \ + if ! grep -q WOLFCOSE_EXPERIMENTAL experimental-check.err; then \ + echo "FAIL: COSE-HPKE $$enable gate error did not mention WOLFCOSE_EXPERIMENTAL"; \ + cat experimental-check.err; rm -f experimental-check.err; exit 1; \ + fi; \ + rm -f experimental-check.err; \ + done @echo " EXP normal build (expect pass, zero experimental code)" @$(EXP_TU) | \ $(CC) $(EXP_FLAGS) -fsyntax-only -x c - @echo "PASS: WOLFCOSE_EXPERIMENTAL gate enforced" +# Requires WOLFSSL_INC to name an HPKE-enabled wolfSSL installation. Keep this +# separate from c99-check so the normal strict gate remains backend-neutral. +c99-hpke-check: + @for cfg in $(HPKE_C99_CONFIGS); do \ + for f in $(C99_SRC); do \ + echo " C99 HPKE $$cfg $$f"; \ + $(CC) $(HPKE_C99_FLAGS) $$cfg -fsyntax-only $$f || exit 1; \ + done; \ + done + @for cfg in $(HPKE_C99_INVALID_CONFIGS); do \ + echo " C99 HPKE missing suite prerequisite $$cfg (expect error)"; \ + if $(EXP_TU) | $(CC) $(HPKE_C99_FLAGS) $(HPKE_C99_CONFIG) $$cfg \ + -fsyntax-only -x c - 2>experimental-check.err; then \ + echo "FAIL: HPKE compiled without a required suite primitive"; \ + rm -f experimental-check.err; exit 1; \ + fi; \ + grep -q "HPKE-0 requires" experimental-check.err || exit 1; \ + done + @rm -f experimental-check.err + @echo "PASS: all experimental HPKE sources conform to ISO C99 (-pedantic-errors)" + # --- Cleanup --- clean: - rm -f $(OBJ) $(TEST_BIN) $(TOOL_BIN) $(DEMO_BIN) $(ENC_DEMO) $(MAC_DEMO) \ + rm -f $(OBJ) $(TEST_BIN) $(TOOL_BIN) $(DEMO_BIN) $(ENC_DEMO) $(HPKE_DEMO) $(MAC_DEMO) \ $(EXTSIGN_DEMO) $(SIGN1_DEMO) $(MLDSA_DEMO) $(MLDSAV_DEMO) $(LMS_DEMO) $(LMSV_DEMO) \ $(COMP_SIGN) $(COMP_ENCRYPT) $(COMP_MAC) $(COMP_ERRORS) \ $(SCEN_FIRMWARE) $(SCEN_MULTIPARTY) $(SCEN_IOTFLEET) $(SCEN_SENSOR) $(SCEN_BROADCAST) \ diff --git a/docs/Experimental.md b/docs/Experimental.md new file mode 100644 index 0000000..b59514c --- /dev/null +++ b/docs/Experimental.md @@ -0,0 +1,102 @@ +# Experimental Features + +This page tracks wolfCOSE features that intentionally remain outside the stable +default build while their standards are still in development. + +> **Status snapshot:** 2 September 2026. Update this page when the IETF draft, +> its IANA registrations, or wolfCOSE's implementation status changes. + +## Customer Summary + +Experimental code is disabled by default. Enabling a feature requires both the +master acknowledgement, `WOLFCOSE_EXPERIMENTAL`, and the feature's individual +`WOLFCOSE_ENABLE_*` macro. This makes draft adoption an explicit build-time +decision and keeps ordinary deployments free of draft wire formats and APIs. + +Experimental features may change or be removed before their associated RFC is +published. Customers who evaluate one should pin the wolfCOSE revision used for +testing and should not promise long-term wire compatibility until the feature +graduates. + +## COSE-HPKE + +### Standards Status + +| Item | Current status | +|------|----------------| +| Specification | [draft-ietf-cose-hpke-26](https://datatracker.ietf.org/doc/draft-ietf-cose-hpke/26/) | +| IETF state | Active COSE working-group Internet-Draft | +| Intended RFC status | Proposed Standard | +| Published | 4 July 2026 | +| Draft expiration | 5 January 2027 | +| Stable RFC | Not published | + +Draft expiration is not an RFC publication estimate. The IETF may revise, +replace, or advance the document at any time. Track the +[current Datatracker record](https://datatracker.ietf.org/doc/draft-ietf-cose-hpke/) +for the authoritative state. + +### wolfCOSE Status + +| Area | Status | Scope | +|------|--------|-------| +| Master acknowledgement | Complete | `WOLFCOSE_EXPERIMENTAL` is required but enables no code by itself. | +| Fine-grained build controls | Complete | Separate send and receive macros exist for integrated HPKE-0 and multi-recipient HPKE-0-KE. | +| Integrated encryption | Complete for P0 | `COSE_Encrypt0` HPKE-0 encrypt and decrypt. | +| Multi-recipient encryption | Complete for P0 | `COSE_Encrypt` HPKE-0-KE encrypt and decrypt. | +| Command-line and example coverage | Complete for P0 | HPKE commands, self-tests, and `examples/hpke_demo.c` use the same experimental gate. | +| Continuous integration | Complete for P0 | Unit, example, CLI, strict C99, malformed-input, and curve-validation coverage run in the experimental HPKE workflow. | +| Stable wire and API commitment | Pending | Deferred until the final RFC and IANA values are published. | + +### Implemented P0 Scope + +The current implementation is deliberately narrow and auditable: + +- HPKE base mode only. +- DHKEM(P-256, HKDF-SHA256), HKDF-SHA256, and AES-128-GCM only. +- One-recipient integrated `COSE_Encrypt0` HPKE-0. +- Multi-recipient `COSE_Encrypt` HPKE-0-KE with an independently protected CEK + for each recipient. +- External AAD bound through the normal COSE encryption structure. + +The following are not implemented in P0: alternate KEM, KDF, or AEAD suites; +X25519; PSK mode; authenticated HPKE mode; and an application-controlled HPKE +`info` value. These omissions are intentional, not hidden runtime options. + +### Build Selection + +Build wolfSSL with HPKE, ECC P-256, AES-128-GCM, SHA-256, and key generation, +then select only the wolfCOSE directions the product needs: + +```bash +# Integrated Encrypt0, send and receive. +make EXTRA_CFLAGS="-DWOLFCOSE_EXPERIMENTAL -DWOLFCOSE_ENABLE_HPKE_0" + +# Receive-only integrated provisioning target. +make EXTRA_CFLAGS="-DWOLFCOSE_EXPERIMENTAL \ + -DWOLFCOSE_ENABLE_HPKE_0_DECRYPT" + +# Multi-recipient sender in a lean build. +make EXTRA_CFLAGS="-DWOLFCOSE_LEAN -DWOLFCOSE_EXPERIMENTAL \ + -DWOLFCOSE_ENABLE_ENCRYPT -DWOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT" +``` + +The `make hpke-demo` target supplies the acknowledgement and all four P0 +operation macros itself. See [Configuration Macros](Macros.md#cose-hpke-experimental) +for every enable and compile-out macro. + +### Path to Stable Support + +| Milestone | Status | Customer planning guidance | +|-----------|--------|----------------------------| +| New draft revisions | Ongoing | wolfCOSE will assess each revision for wire, identifier, and API changes. | +| Final RFC and IANA registrations | Pending IETF publication | No public RFC publication date is available. Do not use the draft expiration as a delivery date. | +| wolfCOSE compatibility update | Pending final RFC | Planned after the final RFC and registrations are available. | +| Graduation from experimental | Pending validation | The target is a compatible wolfCOSE release after final-RFC validation. The release date cannot be committed before the IETF publication date is known. | + +When COSE-HPKE graduates, wolfCOSE will publish migration notes, update the +implemented identifiers and wire handling as needed, remove the +`WOLFCOSE_EXPERIMENTAL` requirement, and document the stable API and +compatibility policy. Until then, use a COSE signature or MAC with HPKE when +the sender must be authenticated, and treat HPKE-encoded artifacts as +experimental interoperability data. diff --git a/docs/Getting-Started.md b/docs/Getting-Started.md index 044c6c0..d3b4ac3 100644 --- a/docs/Getting-Started.md +++ b/docs/Getting-Started.md @@ -68,6 +68,7 @@ You can enable only the algorithms you need: | ChaCha20-Poly1305 | `--enable-chacha --enable-poly1305` | | ECDH-ES key agreement | `--enable-ecc --enable-hkdf` | | AES Key Wrap | `--enable-aeskeywrap` (wolfSSL 5.9.0+) | +| Experimental COSE-HPKE P0 | `--enable-hpke --enable-ecc --enable-aesgcm --enable-keygen` | | RSA-PSS signing | `--enable-rsapss --enable-keygen` | | Private RSA `COSE_Key` decoding | `--enable-rsapss` (wolfSSL 5.9.0+) | | Private RSA `COSE_Key` serialization | `--enable-rsapss --enable-keygen` (wolfSSL 5.9.2+) | @@ -126,6 +127,8 @@ paths without a wolfSSL installation. | `make tool-test` | Round-trip self-test for all 17 algorithms | | `make demo` | Build and run lifecycle demo (11 algorithms) | | `make demos` | Build and run all basic demos | +| `make hpke-demo` | Build and run the opt-in experimental COSE-HPKE P0 demo | +| `make c99-hpke-check` | Strict C99 syntax check for all experimental HPKE paths (HPKE-enabled wolfSSL required) | | `make comprehensive` | Build and run comprehensive algorithm tests (~240 tests) | | `make scenarios` | Build and run real-world scenario examples | | `make coverage` | Run tests with gcov coverage | @@ -254,6 +257,73 @@ int main(void) } ``` +## Experimental COSE-HPKE P0 + +COSE-HPKE tracks an active Internet-Draft, so it is disabled in every build, +including a normal non-lean build. It currently implements the P0 subset: +HPKE base mode with DHKEM(P-256, HKDF-SHA256), HKDF-SHA256, and AES-128-GCM. +See [Configuration Macros](Macros.md#cose-hpke-experimental) for the complete +operation and compile-out gates, and [Experimental Features](Experimental.md) +for the draft status and graduation plan. + +Build wolfSSL with HPKE support, then enable the exact send and receive paths +your application needs. `WOLFCOSE_EXPERIMENTAL` is required with every HPKE +enable macro. The standalone example supplies that acknowledgement, enables all +four paths, and +demonstrates both one-recipient `COSE_Encrypt0` and two-recipient +`COSE_Encrypt` key encryption: + +```bash +cd wolfssl +./configure --enable-cryptonly --enable-hpke --enable-ecc --enable-aesgcm \ + --enable-keygen +make + +cd ../wolfCOSE +make hpke-demo \ + EXTRA_CFLAGS="-I/path/to/wolfssl" \ + LDFLAGS="-L/path/to/wolfssl -lwolfssl" +``` + +The command-line tool is compiled with the same operation gates. `keygen -p` +exports a public-only COSE_Key; keep the corresponding `-o` private key on the +recipient, and use new, distinct non-symlink destinations for `-o` and `-p`. +On POSIX builds, HPKE key generation refuses to replace an existing +destination. Normalized, case-equivalent, and symlink aliases are rejected +before either key is written. On non-POSIX builds, `-p` is rejected rather +than weakening those key-output safeguards. The direct commands are +for HPKE-0, while the `hpke-ke-*` commands +use one independently HPKE-protected CEK for every recipient: + +```bash +# Build the tool with WOLFCOSE_EXPERIMENTAL and the four +# WOLFCOSE_ENABLE_HPKE_0_* operation macros. +./tools/wolfcose_tool keygen -a HPKE-0 \ + -o recipient.private.cbor -p recipient.public.cbor +./tools/wolfcose_tool hpke0-enc -k recipient.public.cbor \ + -i config.bin -o config.hpke.cbor +./tools/wolfcose_tool hpke0-dec -k recipient.private.cbor \ + -i config.hpke.cbor -o config.out + +./tools/wolfcose_tool keygen -a HPKE-0-KE \ + -o recipient-a.private.cbor -p recipient-a.public.cbor +./tools/wolfcose_tool keygen -a HPKE-0-KE \ + -o recipient-b.private.cbor -p recipient-b.public.cbor +./tools/wolfcose_tool hpke-ke-enc -a A128GCM \ + -k recipient-a.public.cbor -k recipient-b.public.cbor \ + -i config.bin -o config.multi.cbor +./tools/wolfcose_tool hpke-ke-dec -k recipient-b.private.cbor -r 1 \ + -i config.multi.cbor -o config.out +``` + +`-r` is a zero-based recipient index. The tool limits its HPKE-0-KE command +paths to `WOLFCOSE_TOOL_MAX_HPKE_RECIPIENTS` recipient keys and indices, four +by default, which can be reduced for constrained integrations. Its output +buffers reserve framing for the fixed P-256 HPKE envelope, so an input at the +configured `WOLFCOSE_TOOL_MAX_MSG` limit remains usable. HPKE base mode +authenticates the recipient, not the sender. Sign or MAC the resulting message +when sender authentication is required. + ## Quick Start: Post-Quantum Signing (ML-DSA) ```c diff --git a/docs/Home.md b/docs/Home.md index b61bb55..ffbecdb 100644 --- a/docs/Home.md +++ b/docs/Home.md @@ -33,6 +33,7 @@ It uses [wolfSSL](https://www.wolfssl.com/) as the cryptographic backend and is | [[Algorithms]] | Complete list of supported algorithms with COSE IDs | | [[API Reference]] | Full API documentation for all functions | | [[Macros]] | Configuration macros and compile-time options | +| [[Experimental]] | Draft feature status, supported scope, and graduation plan | | [[Footprint]] | Size and speed numbers, desktop and on-device | | [[Testing]] | Unit tests, coverage, and failure injection | | [[Project Structure]] | Source code layout and file descriptions | diff --git a/docs/MISRA-Compliance.md b/docs/MISRA-Compliance.md index 1b89fe5..54855df 100644 --- a/docs/MISRA-Compliance.md +++ b/docs/MISRA-Compliance.md @@ -15,7 +15,7 @@ Verified via cppcheck's MISRA addon (`--addon=misra`) with all wolfCOSE algorith **Workflow**: `.github/workflows/misra-2012.yml` -All wolfCOSE and wolfSSL feature macros are explicitly defined so cppcheck checks the full code path rather than enumerating wolfSSL's hundreds of platform `#ifdef` configurations. See [[Macros]] for the complete list. +All wolfCOSE and wolfSSL feature macros are explicitly defined so cppcheck checks the full code path rather than enumerating wolfSSL's hundreds of platform `#ifdef` configurations. This includes all four default-off experimental COSE-HPKE operation gates, `WOLFCOSE_EXPERIMENTAL`, and wolfSSL HPKE support, so draft code is analyzed rather than silently excluded. See [[Macros]] for the complete list. ### MISRA C:2023 @@ -84,6 +84,8 @@ tracked conditional preprocessing directive. |----|------|----------|--------------------------| | D-11.5-001 | 11.5 | `wolfCose_ForceZero` | Converting the caller's object pointer to a volatile character pointer is the standard C mechanism for securely erasing its object representation. Character access is alignment-safe and the function allocates no memory. `make zeroize-test`, `make zero-alloc-check`, and `make c99-check` validate the implementation. | | D-19.2-001 | 19.2 | `WOLFCOSE_KEY.key` | The public, discriminated union preserves the established ABI and embedded-memory footprint. `kty` and `attachedType` govern member access. Replacing it with a structure would break ABI and increase RAM. The full tests exercise the supported members, and CI anchors the deviation to the exact union boundaries. | +| D-11.8-002 | 11.8 | `wolfCose_Hpke0Seal` plaintext input | The pinned wolfSSL `wc_HpkeSealBase()` interface accepts a mutable plaintext pointer even though its implementation only consumes plaintext while writing to its distinct ciphertext output. The wrapper keeps the wolfCOSE public input const-qualified and avoids a payload-sized copy in constrained builds. The deviation is anchored to the one interoperability cast and covered by the HPKE unit and sanitizer CI lanes. | +| D-11.8-003 | 11.8 | `wolfCose_Hpke0Open` ciphertext input | The pinned wolfSSL `wc_HpkeOpenBase()` interface accepts a mutable ciphertext pointer even though its implementation only consumes ciphertext while writing to its distinct plaintext output. The wrapper keeps the wolfCOSE public input const-qualified and avoids a ciphertext-sized copy in constrained builds. The deviation is anchored to the one interoperability cast and covered by the HPKE unit and sanitizer CI lanes. | `scripts/misra-deviations.json` identifies only these source locations. The classifier also pins a hash of the public union declaration. It fails if an diff --git a/docs/Macros.md b/docs/Macros.md index b56ab72..968d483 100644 --- a/docs/Macros.md +++ b/docs/Macros.md @@ -2,7 +2,8 @@ wolfCOSE has two configuration modes. The default is an opt-out full build: every algorithm wolfSSL provides is enabled, and you strip what you don't need with `WOLFCOSE_NO_*` defines. Alternatively, `WOLFCOSE_LEAN` switches to an opt-in core build and you add extensions with `WOLFCOSE_ENABLE_*`. See [Lean Configuration Layer](#lean-configuration-layer). -Draft, pre-RFC features are held behind a separate acknowledgement, `WOLFCOSE_EXPERIMENTAL`; see [Experimental Features](#experimental-features). +Draft, pre-RFC features are held behind a separate acknowledgement, +`WOLFCOSE_EXPERIMENTAL`; see [Experimental Features](Experimental.md). ## Experimental Features @@ -38,6 +39,11 @@ cc -DWOLFCOSE_ENABLE_EXPERIMENTAL_EXAMPLE ... **Graduation policy.** When a draft is published as an RFC, its `WOLFCOSE_EXPERIMENTAL` requirement is removed in a focused follow-up and the feature becomes an ordinary gate (default-on full build, `WOLFCOSE_ENABLE_` under `WOLFCOSE_LEAN`, `WOLFCOSE_NO_` to strip), following the [Algorithm Gates](#algorithm-gates) convention. +COSE-HPKE is currently an experimental feature. It stays off in every build and +requires the master acknowledgement plus the relevant operation gate. See +[Experimental Features](Experimental.md) for its current draft status, scope, +and graduation plan. + ## Lean Configuration Layer Defining `WOLFCOSE_LEAN` keeps only the core — `COSE_Sign1`/`Encrypt0`/`Mac0` with ES256, AES-GCM, and HMAC-SHA256 — and turns every other algorithm into an opt-in. This is the recommended starting point for constrained targets. @@ -49,6 +55,9 @@ Defining `WOLFCOSE_LEAN` keeps only the core — `COSE_Sign1`/`Encrypt0`/`Mac0` Extension names for `WOLFCOSE_ENABLE_`: `ES384`, `ES512`, `EDDSA`, `ED448`, `RSAPSS`, `MLDSA`, `LMS`, `HMAC384`, `HMAC512`, `AESCCM`, `CHACHA20`, `AESMAC`, `AESWRAP`, `ECDH_ES`, `SIGN` (multi-signer), `ENCRYPT` (multi-recipient), `MAC` (multi-recipient), and `COUNTERSIGN` (RFC 9338 countersignatures). +COSE-HPKE uses its own explicit gates rather than this generic extension rule, +so it never becomes enabled merely because wolfSSL provides HPKE. + An extension is compiled in when it is explicitly enabled (`WOLFCOSE_ENABLE_`), or — in a non-lean build — when wolfSSL provides the primitive and it is not opted out with `WOLFCOSE_NO_`. Enabling an extension wolfSSL cannot provide is a compile error. The resolved state is exposed internally as read-only `WOLFCOSE_HAVE_` gates (e.g. `WOLFCOSE_HAVE_MLDSA`); sources, tests, and examples compile against those, so you set `WOLFCOSE_ENABLE_*`/`WOLFCOSE_NO_*`, not `WOLFCOSE_HAVE_*`. ## Algorithm Gates @@ -232,6 +241,106 @@ Resolved internally as read-only `WOLFCOSE_KEY_WRAP`, `WOLFCOSE_ECDH`, and `WOLF --- +## COSE-HPKE (experimental) + +COSE-HPKE implements the P0 subset of +[draft-ietf-cose-hpke-26](https://datatracker.ietf.org/doc/draft-ietf-cose-hpke/). +It is deliberately disabled by default in both full and lean builds because the +COSE binding and its IANA values are still an Internet-Draft. HPKE itself is +standardized by RFC 9180, but the COSE profile is not yet an RFC. + +P0 fixes every cryptographic choice: `DHKEM(P-256, HKDF-SHA256)`, +`HKDF-SHA256`, and `AES-128-GCM`, in HPKE base mode only. There are no hidden +P-384, P-521, X25519, PSK, authenticated, or alternate-AEAD paths to enable. +That keeps each enabled wire operation auditable and lets targets compile out +all unsupported suites. + +P0 is intended for confidential provisioning, configuration, and credential +delivery. It can encrypt a payload directly to one recipient, or encrypt the +content once and independently protect its content-encryption key for multiple +recipients. Base mode provides recipient-only confidentiality; pair the result +with a COSE signature or MAC when the sender must be authenticated. + +| Define | Description | Default | +|--------|-------------|---------| +| `WOLFCOSE_EXPERIMENTAL` | Required acknowledgement for every COSE-HPKE enable; alone enables no HPKE code | off | +| `WOLFCOSE_ENABLE_HPKE_0` | Enable both send and receive for single-recipient integrated `COSE_Encrypt0` HPKE-0 | off | +| `WOLFCOSE_ENABLE_HPKE_0_ENCRYPT` | Enable only integrated Encrypt0 send | off | +| `WOLFCOSE_ENABLE_HPKE_0_DECRYPT` | Enable only integrated Encrypt0 receive | off | +| `WOLFCOSE_ENABLE_HPKE_0_KE` | Enable both send and receive for multi-recipient `COSE_Encrypt` HPKE-0-KE | off | +| `WOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT` | Enable only multi-recipient HPKE key-encryption send | off | +| `WOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT` | Enable only multi-recipient HPKE key-encryption receive | off | +| `WOLFCOSE_NO_HPKE_0` | Prohibit all HPKE-0 enables; conflicts with any HPKE-0 enable | off | +| `WOLFCOSE_NO_HPKE_0_ENCRYPT` / `WOLFCOSE_NO_HPKE_0_DECRYPT` | Compile out the corresponding integrated Encrypt0 direction | off | +| `WOLFCOSE_NO_HPKE_0_KE_ENCRYPT` / `WOLFCOSE_NO_HPKE_0_KE_DECRYPT` | Compile out the corresponding multi-recipient key-encryption direction | off | + +Every HPKE `ENABLE_*` macro requires `WOLFCOSE_EXPERIMENTAL`; selecting an HPKE +operation without the acknowledgement is a compile error. The convenience +gates respect their per-direction `NO_*` gates. Defining an individual +`ENABLE_*` and its matching `NO_*` is a compile error; defining +`WOLFCOSE_NO_HPKE_0` with any enable is also a compile error. + +An enabled HPKE operation requires wolfSSL `HAVE_HPKE`, `HAVE_ECC` with P-256, +SHA-256, and `HAVE_AESGCM`. Integrated mode also needs the matching +`COSE_Encrypt0` direction. Key-encryption mode additionally needs the matching +`COSE_Encrypt` direction and recipient support; under `WOLFCOSE_LEAN`, add +`WOLFCOSE_ENABLE_ENCRYPT` before enabling an HPKE-0-KE operation. + +Build the wolfSSL backend with HPKE, ECC P-256, AES-GCM, SHA-256, and key +generation. For example, a cryptography-only backend can use: + +```bash +./configure --enable-cryptonly --enable-hpke --enable-ecc --enable-aesgcm \ + --enable-keygen +``` + +The public identifiers are provisional draft values: `HPKE-0` is algorithm 35, +`HPKE-0-KE` is algorithm 46, and `ek` is header label -4. Applications must +treat them as draft values and update when the COSE-HPKE RFC or IANA registry +changes. P0 accepts only base mode: `psk_id`, a PSK, and external HPKE `info` +are intentionally not exposed. Application external AAD is still bound through +the normal COSE `Enc_structure` API argument. + +For integrated mode, call `wc_CoseHpkeEncrypt0_Encrypt()` and +`wc_CoseHpkeEncrypt0_Decrypt()`. It encrypts payloads directly for one +recipient. For key-encryption mode, use `wc_CoseEncrypt_Encrypt()` and +`wc_CoseEncrypt_Decrypt()` with every `WOLFCOSE_RECIPIENT.algId` set to +`WOLFCOSE_ALG_HPKE_0_KE`; wolfCOSE generates one CEK, encrypts the content +once, and HPKE-wraps that CEK separately for every recipient. HPKE base mode +does not authenticate the sender, so add a COSE signature or MAC when sender +authentication is required. + +Draft P0 permits the HPKE `alg` header parameter to be absent. The integrated +decrypt API is already pinned to HPKE-0; for key-encryption decrypt, set the +selected `WOLFCOSE_RECIPIENT.algId` to `WOLFCOSE_ALG_HPKE_0_KE` so wolfCOSE can +pin the omitted value safely. When an integrated or recipient HPKE `alg` is +present, it must be in the protected header. wolfCOSE rejects an unprotected +HPKE `alg` rather than accepting an unauthenticated key-management choice. For +HPKE-0-KE, the outer `COSE_Encrypt` content algorithm may be unprotected: the +HPKE Recipient_structure binds it as `next_layer_alg`, so a modification makes +HPKE CEK recovery fail. P0 requires the 65-byte P-256 `ek` in the unprotected +header, rejects it in the protected header, and rejects `psk_id` because PSK +mode is not implemented. + +```bash +# Single-recipient integrated HPKE: send and receive. +make EXTRA_CFLAGS="-DWOLFCOSE_EXPERIMENTAL -DWOLFCOSE_ENABLE_HPKE_0" + +# Receive-only provisioning target: no HPKE sender code. +make EXTRA_CFLAGS="-DWOLFCOSE_EXPERIMENTAL -DWOLFCOSE_ENABLE_HPKE_0_DECRYPT" + +# Multi-recipient provisioning server, including the lean COSE_Encrypt gate. +make EXTRA_CFLAGS="-DWOLFCOSE_LEAN -DWOLFCOSE_EXPERIMENTAL -DWOLFCOSE_ENABLE_ENCRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT" +``` + +Draft -26's published Section 5.2 key-encryption sample encodes a 16-byte +`A128GCM` IV. RFC 9053 fixes the COSE AES-GCM nonce at 12 bytes, so wolfCOSE +rejects that malformed sample and requires a 12-byte IV for all COSE content +encryption. + +--- + ## Delegated Signing | Define | Description | Default | @@ -458,6 +567,7 @@ wolfCOSE requires these wolfSSL features for full functionality: | `WOLFSSL_SHA512` | SHA-512 for ES512, HMAC-512 | | `HAVE_AES_KEYWRAP` | AES Key Wrap distribution | | `HAVE_HKDF` | ECDH-ES key derivation | +| `HAVE_HPKE` + `HAVE_ECC` P-256 + SHA-256 + `HAVE_AESGCM` | Experimental COSE-HPKE P0 | --- diff --git a/docs/Testing.md b/docs/Testing.md index a8ffabd..e8b8bcc 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -24,6 +24,56 @@ make tool-test Round-trip self-tests for all 17 supported CLI algorithms. Each algorithm is tested with key generation, operation, and verification. +### Experimental COSE-HPKE Tests + +COSE-HPKE P0 is off by default and therefore has a dedicated opt-in test path. +Build against a wolfSSL configured with `--enable-hpke --enable-ecc +--enable-aesgcm --enable-keygen`, then acknowledge experimental draft support +and enable the four operation macros: + +```bash +HPKE_FLAGS="-DWOLFCOSE_EXPERIMENTAL \ + -DWOLFCOSE_ENABLE_HPKE_0_ENCRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_DECRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT" + +make test EXTRA_CFLAGS="$HPKE_FLAGS" +make hpke-demo EXTRA_CFLAGS="$HPKE_FLAGS" +make c99-hpke-check WOLFSSL_INC=/path/to/hpke-enabled-wolfssl/include +EXPECT_HPKE=true make cmdline-test EXTRA_CFLAGS="$HPKE_FLAGS" +``` + +The command-line test performs public/private key export, rejects identical, +normalized-alias, case-alias (where the filesystem supports it), and +symlink-alias key destinations, preserves existing POSIX destinations, and +rejects a failed paired key export without leaving a private-key file. It +rejects a plaintext one byte above the configured maximum for both +constructions, exercises HPKE-0 integrated +encryption, two-recipient HPKE-0-KE encryption and decryption at both +recipient indices, maximum-message round trips for both constructions, and a +focused `test -a` round trip for each construction. The dedicated strict C99 +target compiles every HPKE-gated library, test, tool, and example path under +each one-way operation gate, both convenience gates, the complete P0 +configuration, and wolfSSL's `NO_ECC256` plus `HAVE_ALL_CURVES` configuration. +It also rejects HPKE when `NO_ECC_SECP` or an `ECC_MIN_KEY_SZ` above 256 makes +P-256 unavailable, or when `NO_AES_128` removes the fixed suite's AEAD key +size. +Every enabled configuration also supplies `WOLFCOSE_EXPERIMENTAL`; the regular +`make experimental-check` target verifies that an HPKE operation selected +without that acknowledgement is rejected at compile time. +The unit suite validates draft messages that omit the optional HPKE `alg` +(including a second selected recipient), rejects an HPKE `alg` in an +unprotected header, accepts an unprotected HPKE-0-KE content algorithm only +when its authenticated Recipient_structure binds it, and rejects a modified +content algorithm. The CI backend enables Koblitz curves to prove that a +32-byte secp256k1 key cannot masquerade as P-256 at the HPKE API boundary. It +also covers missing, duplicate, wrong-type, wrong-length, and wrongly placed +`ek`, prohibited `psk_id`, detached ciphertext, and cleared outputs on failed +decrypts and encrypts. +GitHub Actions runs the same coverage in +[Experimental COSE-HPKE](../.github/workflows/cose-hpke.yml). + ### Comprehensive Algorithm Tests ```bash @@ -221,6 +271,8 @@ wolfCOSE runs the following CI checks on every push and pull request: 3. **Comprehensive Tests**: ~240 algorithm combination tests 4. **Scenario Examples**: Real-world workflow tests 5. **Tool Tests**: CLI round-trip tests (17 algorithms) +6. **Experimental COSE-HPKE**: Opt-in P0 unit tests, example, CLI commands, + and both construction-specific self-tests The Interop workflow intentionally keeps t_cose, go-cose, python-cwt, Rust coset, and the complete upstream t_cose suite in one matrix job per wolfSSL diff --git a/docs/_Sidebar.md b/docs/_Sidebar.md index 6683ebe..6979fb2 100644 --- a/docs/_Sidebar.md +++ b/docs/_Sidebar.md @@ -6,6 +6,7 @@ - [[Algorithms]] - [[API Reference]] - [[Macros]] +- [[Experimental]] - [[Footprint]] - [[Testing]] - [[MISRA Compliance]] diff --git a/examples/hpke_demo.c b/examples/hpke_demo.c new file mode 100644 index 0000000..fcc5a3d --- /dev/null +++ b/examples/hpke_demo.c @@ -0,0 +1,221 @@ +/* hpke_demo.c + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfCOSE. + * + * wolfCOSE is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfCOSE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +/* Experimental COSE-HPKE P0 demonstration. + * + * The draft binding requires WOLFCOSE_EXPERIMENTAL plus its operation gates. + * This program demonstrates the two currently supported P0 constructions: + * - HPKE-0: one-recipient COSE_Encrypt0 integrated encryption + * - HPKE-0-KE: COSE_Encrypt with one HPKE-protected CEK per recipient + */ + +#include +#include + +#include +#include +#include + +#if defined(WOLFCOSE_HPKE_0_ENCRYPT) && \ + defined(WOLFCOSE_HPKE_0_DECRYPT) && \ + defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) && \ + defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + +#define HPKE_DEMO_RECIPIENTS 2u + +static int demo_hpke_encrypt0(void) +{ + static const uint8_t kid[] = "recipient-a"; + static const uint8_t payload[] = "HPKE-0 integrated encryption"; + WC_RNG rng; + ecc_key recipient; + WOLFCOSE_KEY recipientKey; + WOLFCOSE_HDR hdr; + uint8_t scratch[WOLFCOSE_MAX_SCRATCH_SZ]; + uint8_t cose[512]; + uint8_t plaintext[sizeof(payload)]; + size_t coseLen = 0u; + size_t plaintextLen = 0u; + int rngInit = 0; + int eccInit = 0; + int ret; + + printf("--- HPKE-0 COSE_Encrypt0 ---\n"); + + ret = wc_InitRng(&rng); + if (ret == 0) { + rngInit = 1; + ret = wc_ecc_init(&recipient); + } + if (ret == 0) { + eccInit = 1; + ret = wc_ecc_make_key(&rng, 32, &recipient); + } + if (ret == 0) { + wc_CoseKey_Init(&recipientKey); + ret = wc_CoseKey_SetEcc(&recipientKey, WOLFCOSE_CRV_P256, + &recipient); + recipientKey.alg = WOLFCOSE_ALG_HPKE_0; + recipientKey.hasPrivate = 0u; + } + if (ret == 0) { + ret = wc_CoseHpkeEncrypt0_Encrypt(&recipientKey, kid, + sizeof(kid) - 1u, payload, sizeof(payload) - 1u, + NULL, 0u, NULL, NULL, 0u, scratch, sizeof(scratch), cose, + sizeof(cose), &coseLen, &rng); + } + if (ret == 0) { + recipientKey.hasPrivate = 1u; + ret = wc_CoseHpkeEncrypt0_Decrypt(&recipientKey, cose, coseLen, + NULL, 0u, NULL, 0u, scratch, sizeof(scratch), &hdr, plaintext, + sizeof(plaintext), &plaintextLen); + } + if ((ret == 0) && + ((hdr.alg != WOLFCOSE_ALG_HPKE_0) || + (plaintextLen != (sizeof(payload) - 1u)) || + (memcmp(plaintext, payload, plaintextLen) != 0))) { + ret = -1; + } + + if (eccInit != 0) { + wc_ecc_free(&recipient); + } + if (rngInit != 0) { + wc_FreeRng(&rng); + } + + printf(" %s (%zu byte COSE_Encrypt0)\n", ret == 0 ? "PASS" : "FAIL", + coseLen); + return ret; +} + +static int demo_hpke_key_encryption(void) +{ + static const uint8_t kid[HPKE_DEMO_RECIPIENTS][12] = { + "recipient-a", "recipient-b" + }; + static const uint8_t payload[] = "HPKE-0-KE multi-recipient encryption"; + WC_RNG rng; + ecc_key recipientEcc[HPKE_DEMO_RECIPIENTS]; + WOLFCOSE_KEY recipientKey[HPKE_DEMO_RECIPIENTS]; + WOLFCOSE_RECIPIENT recipients[HPKE_DEMO_RECIPIENTS]; + WOLFCOSE_HDR hdr; + uint8_t iv[12]; + uint8_t scratch[WOLFCOSE_MAX_SCRATCH_SZ]; + uint8_t cose[1024]; + uint8_t plaintext[sizeof(payload)]; + size_t coseLen = 0u; + size_t plaintextLen = 0u; + size_t i; + size_t eccCount = 0u; + int rngInit = 0; + int ret; + + printf("--- HPKE-0-KE COSE_Encrypt ---\n"); + (void)memset(recipients, 0, sizeof(recipients)); + + ret = wc_InitRng(&rng); + if (ret == 0) { + rngInit = 1; + } + for (i = 0u; (ret == 0) && (i < HPKE_DEMO_RECIPIENTS); i++) { + ret = wc_ecc_init(&recipientEcc[i]); + if (ret == 0) { + eccCount++; + ret = wc_ecc_make_key(&rng, 32, &recipientEcc[i]); + } + if (ret == 0) { + wc_CoseKey_Init(&recipientKey[i]); + ret = wc_CoseKey_SetEcc(&recipientKey[i], WOLFCOSE_CRV_P256, + &recipientEcc[i]); + recipientKey[i].alg = WOLFCOSE_ALG_HPKE_0_KE; + recipientKey[i].hasPrivate = 0u; + } + if (ret == 0) { + recipients[i].algId = WOLFCOSE_ALG_HPKE_0_KE; + recipients[i].key = &recipientKey[i]; + recipients[i].kid = kid[i]; + recipients[i].kidLen = sizeof(kid[i]) - 1u; + } + } + if (ret == 0) { + ret = wc_RNG_GenerateBlock(&rng, iv, (word32)sizeof(iv)); + } + if (ret == 0) { + ret = wc_CoseEncrypt_Encrypt(recipients, HPKE_DEMO_RECIPIENTS, + WOLFCOSE_ALG_A128GCM, iv, sizeof(iv), payload, + sizeof(payload) - 1u, NULL, 0u, NULL, 0u, scratch, + sizeof(scratch), cose, sizeof(cose), &coseLen, &rng); + } + if (ret == 0) { + for (i = 0u; i < HPKE_DEMO_RECIPIENTS; i++) { + recipientKey[i].hasPrivate = 1u; + } + } + for (i = 0u; (ret == 0) && (i < HPKE_DEMO_RECIPIENTS); i++) { + plaintextLen = 0u; + ret = wc_CoseEncrypt_Decrypt(&recipients[i], i, cose, coseLen, + NULL, 0u, NULL, 0u, scratch, sizeof(scratch), &hdr, plaintext, + sizeof(plaintext), &plaintextLen); + if ((ret == 0) && + ((hdr.alg != WOLFCOSE_ALG_A128GCM) || + (plaintextLen != (sizeof(payload) - 1u)) || + (memcmp(plaintext, payload, plaintextLen) != 0))) { + ret = -1; + } + } + + while (eccCount > 0u) { + eccCount--; + wc_ecc_free(&recipientEcc[eccCount]); + } + if (rngInit != 0) { + wc_FreeRng(&rng); + } + + printf(" %s (%zu byte COSE_Encrypt, %u recipients)\n", + ret == 0 ? "PASS" : "FAIL", coseLen, + (unsigned int)HPKE_DEMO_RECIPIENTS); + return ret; +} + +int main(void) +{ + int ret; + + printf("Experimental COSE-HPKE P0 example\n\n"); + ret = demo_hpke_encrypt0(); + if (ret == 0) { + ret = demo_hpke_key_encryption(); + } + return ret == 0 ? 0 : 1; +} + +#else + +int main(void) +{ + fprintf(stderr, + "This example requires WOLFCOSE_EXPERIMENTAL and the four " + "WOLFCOSE_ENABLE_HPKE_0_* macros.\n"); + return 1; +} + +#endif diff --git a/include/wolfcose/settings.h b/include/wolfcose/settings.h index 548a509..0bfdb16 100644 --- a/include/wolfcose/settings.h +++ b/include/wolfcose/settings.h @@ -623,6 +623,111 @@ extern "C" { #endif #endif +/* ----- COSE-HPKE (draft-ietf-cose-hpke-26) ----- + * + * HPKE is deliberately opt-in in every build. The P0 implementation supports + * only the draft's HPKE-0 ciphersuite: DHKEM(P-256, HKDF-SHA256), + * HKDF-SHA256, and AES-128-GCM in base mode. Each wire operation has a + * separate enable gate so send-only and receive-only targets do not carry the + * other direction. The two convenience switches enable both directions for + * their respective COSE construction. + */ +#if (defined(WOLFCOSE_ENABLE_HPKE_0) || \ + defined(WOLFCOSE_ENABLE_HPKE_0_KE) || \ + defined(WOLFCOSE_ENABLE_HPKE_0_ENCRYPT) || \ + defined(WOLFCOSE_ENABLE_HPKE_0_DECRYPT) || \ + defined(WOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT) || \ + defined(WOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT)) && \ + !defined(WOLFCOSE_EXPERIMENTAL) + #error "COSE-HPKE selects experimental draft code (spec not yet a finalized RFC); also define WOLFCOSE_EXPERIMENTAL to acknowledge" +#endif + +#if defined(WOLFCOSE_ENABLE_HPKE_0) + #if defined(WOLFCOSE_NO_HPKE_0) + #error "WOLFCOSE_ENABLE_HPKE_0 conflicts with WOLFCOSE_NO_HPKE_0" + #endif + #if !defined(WOLFCOSE_NO_HPKE_0_ENCRYPT) + #define WOLFCOSE_ENABLE_HPKE_0_ENCRYPT + #endif + #if !defined(WOLFCOSE_NO_HPKE_0_DECRYPT) + #define WOLFCOSE_ENABLE_HPKE_0_DECRYPT + #endif +#endif + +#if defined(WOLFCOSE_ENABLE_HPKE_0_KE) + #if defined(WOLFCOSE_NO_HPKE_0) + #error "WOLFCOSE_ENABLE_HPKE_0_KE conflicts with WOLFCOSE_NO_HPKE_0" + #endif + #if !defined(WOLFCOSE_NO_HPKE_0_KE_ENCRYPT) + #define WOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT + #endif + #if !defined(WOLFCOSE_NO_HPKE_0_KE_DECRYPT) + #define WOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT + #endif +#endif + +#if defined(WOLFCOSE_NO_HPKE_0) && \ + (defined(WOLFCOSE_ENABLE_HPKE_0_ENCRYPT) || \ + defined(WOLFCOSE_ENABLE_HPKE_0_DECRYPT) || \ + defined(WOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT) || \ + defined(WOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT)) + #error "WOLFCOSE_NO_HPKE_0 conflicts with an HPKE-0 operation enable" +#endif + +#if (defined(WOLFCOSE_ENABLE_HPKE_0_ENCRYPT) && \ + defined(WOLFCOSE_NO_HPKE_0_ENCRYPT)) || \ + (defined(WOLFCOSE_ENABLE_HPKE_0_DECRYPT) && \ + defined(WOLFCOSE_NO_HPKE_0_DECRYPT)) || \ + (defined(WOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT) && \ + defined(WOLFCOSE_NO_HPKE_0_KE_ENCRYPT)) || \ + (defined(WOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT) && \ + defined(WOLFCOSE_NO_HPKE_0_KE_DECRYPT)) + #error "An HPKE-0 operation cannot be both enabled and disabled" +#endif + +#if defined(WOLFCOSE_ENABLE_HPKE_0_ENCRYPT) || \ + defined(WOLFCOSE_ENABLE_HPKE_0_DECRYPT) || \ + defined(WOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT) || \ + defined(WOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT) + #if !defined(HAVE_HPKE) || !defined(HAVE_ECC) || \ + !defined(HAVE_HKDF) || !defined(HAVE_ECC_DHE) || \ + !defined(HAVE_AESGCM) || !defined(WOLFSSL_AES_128) || \ + defined(NO_SHA256) || defined(NO_ECC_SECP) || \ + (defined(NO_ECC256) && !defined(HAVE_ALL_CURVES)) || \ + (defined(ECC_MIN_KEY_SZ) && (ECC_MIN_KEY_SZ > 256)) + #error "HPKE-0 requires HPKE, ECC P-256 ECDH, HKDF-SHA256, SHA-256, AES-GCM, and AES-128" + #endif + #define WOLFCOSE_HAVE_HPKE_0 +#endif + +#if defined(WOLFCOSE_ENABLE_HPKE_0_ENCRYPT) + #if !defined(WOLFCOSE_ENCRYPT0_ENCRYPT) + #error "WOLFCOSE_ENABLE_HPKE_0_ENCRYPT requires COSE_Encrypt0 encrypt" + #endif + #define WOLFCOSE_HPKE_0_ENCRYPT +#endif + +#if defined(WOLFCOSE_ENABLE_HPKE_0_DECRYPT) + #if !defined(WOLFCOSE_ENCRYPT0_DECRYPT) + #error "WOLFCOSE_ENABLE_HPKE_0_DECRYPT requires COSE_Encrypt0 decrypt" + #endif + #define WOLFCOSE_HPKE_0_DECRYPT +#endif + +#if defined(WOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT) + #if !defined(WOLFCOSE_ENCRYPT_ENCRYPT) || !defined(WOLFCOSE_RECIPIENTS) + #error "WOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT requires COSE_Encrypt recipients and encrypt" + #endif + #define WOLFCOSE_HPKE_0_KE_ENCRYPT +#endif + +#if defined(WOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT) + #if !defined(WOLFCOSE_ENCRYPT_DECRYPT) || !defined(WOLFCOSE_RECIPIENTS) + #error "WOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT requires COSE_Encrypt recipients and decrypt" + #endif + #define WOLFCOSE_HPKE_0_KE_DECRYPT +#endif + /* ----- COSE_Key serialization — core ----- */ #ifndef WOLFCOSE_NO_KEY_ENCODE #define WOLFCOSE_KEY_ENCODE diff --git a/include/wolfcose/wolfcose.h b/include/wolfcose/wolfcose.h index f77435c..39d2110 100644 --- a/include/wolfcose/wolfcose.h +++ b/include/wolfcose/wolfcose.h @@ -156,6 +156,10 @@ extern "C" { #define WOLFCOSE_HDR_COUNTERSIGNATURE_V2 11 #define WOLFCOSE_HDR_COUNTERSIGNATURE0_V2 12 #define WOLFCOSE_HDR_EPHEMERAL_KEY (-1) /* Ephemeral COSE_Key for ECDH */ +#if defined(WOLFCOSE_HAVE_HPKE_0) + /* draft-ietf-cose-hpke-26, provisional label -4. */ + #define WOLFCOSE_HDR_HPKE_EK (-4) /* HPKE encapsulated key */ +#endif /* * Security considerations for the algorithms below: @@ -233,6 +237,12 @@ extern "C" { #define WOLFCOSE_ALG_ECDH_ES_A192KW (-30) /* ECDH-ES + A192KW */ #define WOLFCOSE_ALG_ECDH_ES_A256KW (-31) /* ECDH-ES + A256KW */ +#if defined(WOLFCOSE_HAVE_HPKE_0) +/* draft-ietf-cose-hpke-26 provisional COSE algorithm identifiers. */ +#define WOLFCOSE_ALG_HPKE_0 35 /* P-256, HKDF-SHA256, AES-128-GCM */ +#define WOLFCOSE_ALG_HPKE_0_KE 46 /* HPKE-0 key encryption */ +#endif + #define WOLFCOSE_ALG_ML_DSA_44 (-48) /* ML-DSA Level 2 */ #define WOLFCOSE_ALG_ML_DSA_65 (-49) /* ML-DSA Level 3 */ #define WOLFCOSE_ALG_ML_DSA_87 (-50) /* ML-DSA Level 5 */ @@ -431,7 +441,7 @@ typedef struct WOLFCOSE_KEY { */ typedef struct WOLFCOSE_RECIPIENT { int32_t algId; /**< Key distribution algorithm; direct mode requires explicit WOLFCOSE_ALG_DIRECT (-6) on both encrypt and MAC create (-3..-31, -6) */ - WOLFCOSE_KEY* key; /**< Caller-owned key (KEK for wrap, recipient pubkey for ECDH) */ + WOLFCOSE_KEY* key; /**< Caller-owned key (KEK for wrap; recipient public/private key for ECDH or HPKE) */ const uint8_t* kid; /**< Key ID for recipient lookup */ size_t kidLen; /**< Key ID length */ } WOLFCOSE_RECIPIENT; @@ -1351,6 +1361,85 @@ WOLFCOSE_API int wc_CoseEncrypt0_Decrypt(const WOLFCOSE_KEY* key, uint8_t* plaintext, size_t plaintextSz, size_t* plaintextLen); #endif /* WOLFCOSE_ENCRYPT0_DECRYPT */ +/* ----- COSE-HPKE Integrated Encryption (draft-ietf-cose-hpke-26) ----- */ + +#if defined(WOLFCOSE_HPKE_0_ENCRYPT) +/** + * \brief Encrypt a payload with COSE HPKE-0 integrated encryption. + * + * Uses only HPKE base mode with DHKEM(P-256, HKDF-SHA256), HKDF-SHA256, + * and AES-128-GCM. The recipient key must be an EC2 P-256 public key and may + * be pinned to WOLFCOSE_ALG_HPKE_0. The encapsulated key is written as the + * draft's provisional ek header parameter (-4). HPKE info is empty and PSK + * mode is not supported by this P0 API. + * + * \param recipientKey Recipient EC2 P-256 public key. + * \param kid Optional recipient key identifier. + * \param kidLen Recipient key identifier length. + * \param payload Plaintext payload (non-NULL, may be empty). + * \param payloadLen Plaintext length. + * \param detachedPayload Detached ciphertext destination (NULL if attached). + * \param detachedSz Detached ciphertext destination size. + * \param detachedLen Output: detached ciphertext length. + * \param extAad External additional authenticated data (NULL if none). + * \param extAadLen External additional authenticated data length. + * \param scratch Working buffer for Enc_structure. + * \param scratchSz Working buffer size. + * \param out Output COSE_Encrypt0 buffer. + * \param outSz Output buffer size. + * \param outLen Output: bytes written; set to zero on failure. + * \param rng Initialized random number generator. + * \return WOLFCOSE_SUCCESS or a negative error code. + */ +WOLFCOSE_API int wc_CoseHpkeEncrypt0_Encrypt( + const WOLFCOSE_KEY* recipientKey, + const uint8_t* kid, size_t kidLen, + const uint8_t* payload, size_t payloadLen, + uint8_t* detachedPayload, size_t detachedSz, size_t* detachedLen, + const uint8_t* extAad, size_t extAadLen, + uint8_t* scratch, size_t scratchSz, + uint8_t* out, size_t outSz, size_t* outLen, + WC_RNG* rng); +#endif /* WOLFCOSE_HPKE_0_ENCRYPT */ + +#if defined(WOLFCOSE_HPKE_0_DECRYPT) +/** + * \brief Decrypt a COSE HPKE-0 integrated encryption message. + * + * The recipient key must be an EC2 P-256 private key and may be pinned to + * WOLFCOSE_ALG_HPKE_0. Only HPKE base mode with empty HPKE info is accepted; + * PSK mode is rejected. + * + * \param recipientKey Recipient EC2 P-256 private key. + * \param in Input COSE_Encrypt0 message. + * \param inSz Input message size. + * \param detachedCt Detached ciphertext (NULL if attached). Required + * when the message's ciphertext item is CBOR null. + * \param detachedCtLen Detached ciphertext length. + * \param extAad External additional authenticated data (NULL if none). + * \param extAadLen External additional authenticated data length. + * \param scratch Working buffer for Enc_structure reconstruction. + * \param scratchSz Working buffer size. + * \param hdr Output: parsed COSE headers; only protected fields + * are authenticated. + * \param plaintext Output buffer for decrypted plaintext. + * \param plaintextSz Plaintext buffer size. + * \param plaintextLen Output: plaintext length. + * \return WOLFCOSE_SUCCESS or a negative error code, including + * WOLFCOSE_E_DETACHED_PAYLOAD when ciphertext is detached but + * detachedCt is NULL. On failure, hdr and plaintext are cleared and + * plaintextLen is set to zero when it is non-NULL. + */ +WOLFCOSE_API int wc_CoseHpkeEncrypt0_Decrypt( + const WOLFCOSE_KEY* recipientKey, + const uint8_t* in, size_t inSz, + const uint8_t* detachedCt, size_t detachedCtLen, + const uint8_t* extAad, size_t extAadLen, + uint8_t* scratch, size_t scratchSz, + WOLFCOSE_HDR* hdr, + uint8_t* plaintext, size_t plaintextSz, size_t* plaintextLen); +#endif /* WOLFCOSE_HPKE_0_DECRYPT */ + /* ----- COSE_Mac0 API (RFC 9052 Section 6.2) ----- */ #if defined(WOLFCOSE_MAC0_CREATE) && (defined(WOLFCOSE_HAVE_HMAC) || defined(WOLFCOSE_HAVE_AESMAC)) @@ -1507,10 +1596,11 @@ WOLFCOSE_API int wc_CoseSign_Verify(const WOLFCOSE_KEY* verifyKey, * COSE_Encrypt = [Headers, ciphertext, recipients : [+ COSE_recipient]] * * Depending on build configuration, recipient key management supports direct, - * AES Key Wrap, and ECDH-ES direct key agreement. Direct mode uses a - * pre-shared content encryption key (CEK). AES Key Wrap generates one CEK and - * wraps it separately for each recipient. ECDH-ES derives the CEK for one - * recipient and puts the ephemeral public key in the unprotected header. + * AES Key Wrap, ECDH-ES direct key agreement, and HPKE-0 key encryption. + * Direct mode uses a pre-shared content encryption key (CEK). AES Key Wrap + * and HPKE generate one CEK and protect it separately for each recipient. + * ECDH-ES derives the CEK for one recipient and puts the ephemeral public key + * in the unprotected header. * * \param recipients Array of WOLFCOSE_RECIPIENT with keys. * \param recipientCount Number of recipients (must be >= 1). @@ -1531,8 +1621,8 @@ WOLFCOSE_API int wc_CoseSign_Verify(const WOLFCOSE_KEY* verifyKey, * \param out Output buffer. * \param outSz Output buffer size. * \param outLen Output: bytes written to out. - * \param rng Initialized WC_RNG for AES Key Wrap or ECDH-ES; - * may be NULL for direct mode. + * \param rng Initialized WC_RNG for AES Key Wrap, ECDH-ES, or + * HPKE; may be NULL for direct mode. * \return WOLFCOSE_SUCCESS or negative error code. */ WOLFCOSE_API int wc_CoseEncrypt_Encrypt(const WOLFCOSE_RECIPIENT* recipients, @@ -1551,13 +1641,14 @@ WOLFCOSE_API int wc_CoseEncrypt_Encrypt(const WOLFCOSE_RECIPIENT* recipients, /** * \brief Decrypt a COSE_Encrypt message. * - * Decrypts using the key from the specified recipient entry. - * Every on-wire recipient must declare its key-management algorithm, all - * top-level sibling recipients must use an RFC-compatible key-distribution - * mode, and a direct recipient's ciphertext item must be an empty bstr or - * null. Selecting a recipient that contains nested recipients is unsupported. - * An unprotected body algorithm is accepted only when recipient->key->alg - * pins the same value. + * Decrypts using the key from the specified recipient entry. Every non-HPKE + * on-wire recipient must declare its key-management algorithm, all top-level + * sibling recipients must use an RFC-compatible key-distribution mode, and a + * direct recipient's ciphertext item must be an empty bstr or null. Selecting + * a recipient that contains nested recipients is unsupported. An HPKE-0-KE + * recipient may omit its algorithm when recipient->algId pins HPKE-0-KE. An + * unprotected body algorithm is accepted when a direct key pins it or an + * HPKE-0-KE Recipient_structure cryptographically binds it. * * \param recipient WOLFCOSE_RECIPIENT with decryption key. * \param recipientIndex 0-based index of recipient to use. diff --git a/scripts/cmdline-test.sh b/scripts/cmdline-test.sh index ac69889..ae2a5de 100755 --- a/scripts/cmdline-test.sh +++ b/scripts/cmdline-test.sh @@ -33,6 +33,12 @@ skip() { SKIP=$((SKIP+1)); printf ' SKIP %s (%s)\n' "$1" "${2:-not built}"; } # When an algorithm is expected to be built, a keygen failure is a FAIL, not a # silent skip. ML-DSA is only expected when the wolfSSL leg enabled PQC. EXPECT_PQC="${EXPECT_PQC:-false}" +# HPKE is experimental and disabled by default. The dedicated CI lane sets +# this so a missing opt-in path cannot be silently skipped. +EXPECT_HPKE="${EXPECT_HPKE:-false}" +# The default matches WOLFCOSE_TOOL_MAX_MSG. Set this when validating a tool +# compiled with a non-default message limit. +HPKE_TOOL_MAX_MSG="${HPKE_TOOL_MAX_MSG:-8192}" # keygen $alg $out $optional(0|1): on failure, skip if optional else FAIL. keygen_or() { if "$TOOL" keygen -a "$1" -o "$2" >/dev/null 2>&1; then return 0; fi @@ -40,6 +46,13 @@ keygen_or() { return 1 } +# HPKE keygen also writes the public recipient key used for encryption. +hpke_keygen_or() { + if "$TOOL" keygen -a "$1" -o "$2" -p "$3" >/dev/null 2>&1; then return 0; fi + if [ "$EXPECT_HPKE" = "true" ]; then bad "$1 keygen"; else skip "$1"; fi + return 1 +} + # Names exactly as wolfcose_tool's parser accepts them. SIGN_ALGS="ES256 EdDSA Ed448 ML-DSA-44 ML-DSA-65 ML-DSA-87" ENC_ALGS="A128GCM A192GCM A256GCM ChaCha20 AES-CCM" @@ -163,6 +176,193 @@ for A in $ENC_ALGS; do fi done +echo "== Experimental COSE-HPKE: keygen -> encrypt -> decrypt -> self-test ==" +H0_PRIV="$WORK/hpke0.private"; H0_PUB="$WORK/hpke0.public" +H0_COSE="$WORK/hpke0.cose"; H0_OUT="$WORK/hpke0.out" +if hpke_keygen_or "HPKE-0" "$H0_PRIV" "$H0_PUB"; then + H0_SAME="$WORK/hpke0.same" + if "$TOOL" keygen -a "HPKE-0" -o "$H0_SAME" -p "$H0_SAME" \ + >/dev/null 2>&1; then + bad "HPKE-0 keygen rejects identical private/public paths" + else + ok "HPKE-0 keygen rejects identical private/public paths" + fi + H0_ALIAS="$WORK/hpke0.alias" + if "$TOOL" keygen -a "HPKE-0" -o "$H0_ALIAS" -p "$WORK/./hpke0.alias" \ + >/dev/null 2>&1; then + bad "HPKE-0 keygen rejects normalized private/public paths" + else + ok "HPKE-0 keygen rejects normalized private/public paths" + fi + H0_CASE_LOWER="$WORK/hpke0.case" + H0_CASE_UPPER="$WORK/HPKE0.CASE" + if touch "$H0_CASE_LOWER"; then + if [ -e "$H0_CASE_UPPER" ]; then + rm -f "$H0_CASE_LOWER" + if "$TOOL" keygen -a "HPKE-0" -o "$H0_CASE_LOWER" \ + -p "$H0_CASE_UPPER" >/dev/null 2>&1 || \ + [ -e "$H0_CASE_LOWER" ]; then + bad "HPKE-0 keygen rejects case-alias private/public paths" + else + ok "HPKE-0 keygen rejects case-alias private/public paths" + fi + else + rm -f "$H0_CASE_LOWER" + skip "HPKE-0 keygen case-alias paths" "case-sensitive filesystem" + fi + else + bad "HPKE-0 keygen case-alias test setup" + fi + H0_SYMLINK_TARGET="$WORK/hpke0.symlink-target" + H0_SYMLINK="$WORK/hpke0.symlink" + if ln -s "$H0_SYMLINK_TARGET" "$H0_SYMLINK" && \ + "$TOOL" keygen -a "HPKE-0" -o "$H0_SYMLINK_TARGET" -p "$H0_SYMLINK" \ + >/dev/null 2>&1; then + bad "HPKE-0 keygen rejects symlink private/public paths" + elif [ -e "$H0_SYMLINK_TARGET" ]; then + bad "HPKE-0 keygen leaves symlink target untouched" + else + ok "HPKE-0 keygen rejects symlink private/public paths" + fi + H0_EXISTING_PRIV="$WORK/hpke0-existing.private" + H0_EXISTING_COPY="$WORK/hpke0-existing.copy" + H0_NEW_PUB="$WORK/hpke0-new.public" + if printf 'existing HPKE private key\n' > "$H0_EXISTING_PRIV" && \ + cp "$H0_EXISTING_PRIV" "$H0_EXISTING_COPY"; then + if "$TOOL" keygen -a "HPKE-0" -o "$H0_EXISTING_PRIV" \ + -p "$H0_NEW_PUB" >/dev/null 2>&1; then + bad "HPKE-0 keygen rejects existing output paths" + elif cmp -s "$H0_EXISTING_PRIV" "$H0_EXISTING_COPY" && \ + [ ! -e "$H0_NEW_PUB" ]; then + ok "HPKE-0 keygen preserves existing output paths" + else + bad "HPKE-0 keygen preserves existing output paths" + fi + else + bad "HPKE-0 existing output test setup" + fi + H0_FAILED_PRIV="$WORK/hpke0-failed.private" + H0_BLOCKED_PUB="$WORK/hpke0-public-directory" + if mkdir "$H0_BLOCKED_PUB"; then + if "$TOOL" keygen -a "HPKE-0" -o "$H0_FAILED_PRIV" \ + -p "$H0_BLOCKED_PUB" >/dev/null 2>&1; then + bad "HPKE-0 keygen rejects unusable public output" + elif [ -e "$H0_FAILED_PRIV" ]; then + bad "HPKE-0 keygen leaves no partial private output" + else + ok "HPKE-0 keygen leaves no partial private output" + fi + else + bad "HPKE-0 unusable public output test setup" + fi + if "$TOOL" hpke0-enc -k "$H0_PUB" -i "$IN" -o "$H0_COSE" >/dev/null 2>&1 && \ + "$TOOL" hpke0-dec -k "$H0_PRIV" -i "$H0_COSE" -o "$H0_OUT" >/dev/null 2>&1 && \ + cmp -s "$IN" "$H0_OUT"; then + ok "HPKE-0 enc/dec" + else + bad "HPKE-0 enc/dec" + fi + if "$TOOL" test -a "HPKE-0" >/dev/null 2>&1; then + ok "HPKE-0 self-test" + else + bad "HPKE-0 self-test" + fi + H0_MAX_IN="$WORK/hpke0-max.bin"; H0_MAX_COSE="$WORK/hpke0-max.cose" + H0_MAX_OUT="$WORK/hpke0-max.out" + if dd if=/dev/zero of="$H0_MAX_IN" bs="$HPKE_TOOL_MAX_MSG" count=1 \ + >/dev/null 2>&1 && \ + "$TOOL" hpke0-enc -k "$H0_PUB" -i "$H0_MAX_IN" -o "$H0_MAX_COSE" \ + >/dev/null 2>&1 && \ + "$TOOL" hpke0-dec -k "$H0_PRIV" -i "$H0_MAX_COSE" -o "$H0_MAX_OUT" \ + >/dev/null 2>&1 && \ + "$TOOL" info -i "$H0_MAX_COSE" >/dev/null 2>&1 && \ + cmp -s "$H0_MAX_IN" "$H0_MAX_OUT"; then + ok "HPKE-0 maximum message enc/dec" + else + bad "HPKE-0 maximum message enc/dec" + fi + H0_OVERSIZE_IN="$WORK/hpke0-oversize.bin" + H0_OVERSIZE_COSE="$WORK/hpke0-oversize.cose" + if dd if=/dev/zero of="$H0_OVERSIZE_IN" bs="$HPKE_TOOL_MAX_MSG" count=1 \ + >/dev/null 2>&1 && printf '\0' >> "$H0_OVERSIZE_IN"; then + if "$TOOL" hpke0-enc -k "$H0_PUB" -i "$H0_OVERSIZE_IN" \ + -o "$H0_OVERSIZE_COSE" >/dev/null 2>&1; then + bad "HPKE-0 rejects oversized plaintext" + else + ok "HPKE-0 rejects oversized plaintext" + fi + else + bad "HPKE-0 oversized plaintext test setup" + fi +else + skip "HPKE-0 command and self-test" +fi + +HKE0_PRIV="$WORK/hpke-ke-0.private"; HKE0_PUB="$WORK/hpke-ke-0.public" +HKE1_PRIV="$WORK/hpke-ke-1.private"; HKE1_PUB="$WORK/hpke-ke-1.public" +HKE_COSE="$WORK/hpke-ke.cose"; HKE0_OUT="$WORK/hpke-ke-0.out" +HKE1_OUT="$WORK/hpke-ke-1.out" +if hpke_keygen_or "HPKE-0-KE" "$HKE0_PRIV" "$HKE0_PUB" && \ + hpke_keygen_or "HPKE-0-KE" "$HKE1_PRIV" "$HKE1_PUB"; then + if "$TOOL" hpke-ke-enc -a A128GCM -k "$HKE0_PUB" -k "$HKE1_PUB" \ + -i "$IN" -o "$HKE_COSE" >/dev/null 2>&1 && \ + "$TOOL" hpke-ke-dec -k "$HKE0_PRIV" -r 0 -i "$HKE_COSE" \ + -o "$HKE0_OUT" >/dev/null 2>&1 && \ + "$TOOL" hpke-ke-dec -k "$HKE1_PRIV" -r 1 -i "$HKE_COSE" \ + -o "$HKE1_OUT" >/dev/null 2>&1 && \ + cmp -s "$IN" "$HKE0_OUT" && cmp -s "$IN" "$HKE1_OUT"; then + ok "HPKE-0-KE multi-recipient enc/dec" + else + bad "HPKE-0-KE multi-recipient enc/dec" + fi + if "$TOOL" test -a "HPKE-0-KE" >/dev/null 2>&1; then + ok "HPKE-0-KE self-test" + else + bad "HPKE-0-KE self-test" + fi + if "$TOOL" info -r 0 -i "$HKE_COSE" >/dev/null 2>&1; then + bad "HPKE-0-KE recipient index is command-specific" + else + ok "HPKE-0-KE recipient index is command-specific" + fi + "$TOOL" hpke-ke-enc -a A128GCM -k "$WORK/missing-hpke-key" \ + -i "$IN" -o "$HKE_COSE" >/dev/null 2>&1 + if [ "$?" -eq 3 ]; then + ok "HPKE-0-KE preserves key input errors" + else + bad "HPKE-0-KE preserves key input errors" + fi + HKE_MAX_IN="$WORK/hpke-ke-max.bin"; HKE_MAX_COSE="$WORK/hpke-ke-max.cose" + HKE_MAX_OUT="$WORK/hpke-ke-max.out" + if dd if=/dev/zero of="$HKE_MAX_IN" bs="$HPKE_TOOL_MAX_MSG" count=1 \ + >/dev/null 2>&1 && \ + "$TOOL" hpke-ke-enc -a A128GCM -k "$HKE0_PUB" -k "$HKE1_PUB" \ + -i "$HKE_MAX_IN" -o "$HKE_MAX_COSE" >/dev/null 2>&1 && \ + "$TOOL" hpke-ke-dec -k "$HKE0_PRIV" -r 0 -i "$HKE_MAX_COSE" \ + -o "$HKE_MAX_OUT" >/dev/null 2>&1 && \ + cmp -s "$HKE_MAX_IN" "$HKE_MAX_OUT"; then + ok "HPKE-0-KE maximum message enc/dec" + else + bad "HPKE-0-KE maximum message enc/dec" + fi + HKE_OVERSIZE_IN="$WORK/hpke-ke-oversize.bin" + HKE_OVERSIZE_COSE="$WORK/hpke-ke-oversize.cose" + if dd if=/dev/zero of="$HKE_OVERSIZE_IN" bs="$HPKE_TOOL_MAX_MSG" count=1 \ + >/dev/null 2>&1 && printf '\0' >> "$HKE_OVERSIZE_IN"; then + if "$TOOL" hpke-ke-enc -a A128GCM -k "$HKE0_PUB" -k "$HKE1_PUB" \ + -i "$HKE_OVERSIZE_IN" -o "$HKE_OVERSIZE_COSE" \ + >/dev/null 2>&1; then + bad "HPKE-0-KE rejects oversized plaintext" + else + ok "HPKE-0-KE rejects oversized plaintext" + fi + else + bad "HPKE-0-KE oversized plaintext test setup" + fi +else + skip "HPKE-0-KE command and self-test" +fi + echo "== MAC: keygen -> mac -> macverify ==" for A in $MAC_ALGS; do K="$WORK/mac.key"; C="$WORK/mac.cose" diff --git a/scripts/misra-deviations.json b/scripts/misra-deviations.json index 4b2f66f..63c2478 100644 --- a/scripts/misra-deviations.json +++ b/scripts/misra-deviations.json @@ -22,6 +22,26 @@ ], "expected": 2, "range_sha256": "5e1a847f33318c6a6d785966753c616a955f2fcde24099f40d0c0c370dcc606e" + }, + { + "id": "D-11.8-002", + "rule": "11.8", + "path": "src/wolfcose_recipient.c", + "symbol": "wolfCose_Hpke0Seal", + "anchors": [ + " (byte*)plaintext, (word32)plaintextLen, ciphertext);" + ], + "expected": 1 + }, + { + "id": "D-11.8-003", + "rule": "11.8", + "path": "src/wolfcose_recipient.c", + "symbol": "wolfCose_Hpke0Open", + "anchors": [ + " (byte*)ciphertext, (word32)ciphertextBodyLen, plaintext);" + ], + "expected": 1 } ] } diff --git a/src/wolfcose_encrypt.c b/src/wolfcose_encrypt.c index 6e38f3b..400d278 100644 --- a/src/wolfcose_encrypt.c +++ b/src/wolfcose_encrypt.c @@ -145,6 +145,19 @@ int wc_CoseEncrypt_Encrypt(const WOLFCOSE_RECIPIENT* recipients, size_t wrappedCekLen = 0; int useKeyWrap = 0; #endif +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) + uint8_t cekHpke[32]; /* Random CEK wrapped for every recipient */ + uint8_t hpkeEnc[WOLFCOSE_HPKE_0_ENC_SZ]; + size_t hpkeRecipientInfoLen = 0u; + WOLFCOSE_HPKE_0_SEAL_CTX hpkeSealCtx; + int useHpke = 0; +#endif + +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) + (void)XMEMSET(cekHpke, 0, sizeof(cekHpke)); + (void)XMEMSET(hpkeEnc, 0, sizeof(hpkeEnc)); + (void)XMEMSET(&hpkeSealCtx, 0, sizeof(hpkeSealCtx)); +#endif /* Parameter validation */ if ((recipients == NULL) || (recipientCount == 0u) || @@ -218,7 +231,35 @@ int wc_CoseEncrypt_Encrypt(const WOLFCOSE_RECIPIENT* recipients, } } - /* Validate first recipient and determine key mode */ + /* Validate first recipient and determine key mode. */ +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) + if ((ret == WOLFCOSE_SUCCESS) && + (recipients[0].algId == WOLFCOSE_ALG_HPKE_0_KE)) { + if (rng == NULL) { + ret = WOLFCOSE_E_INVALID_ARG; + } + for (i = 0; (ret == WOLFCOSE_SUCCESS) && (i < recipientCount); i++) { + if (recipients[i].algId != WOLFCOSE_ALG_HPKE_0_KE) { + ret = WOLFCOSE_E_COSE_BAD_ALG; + } + else { + ret = wolfCose_Hpke0ValidateKey(recipients[i].key, + WOLFCOSE_ALG_HPKE_0_KE, 0); + } + } + if (ret == WOLFCOSE_SUCCESS) { + int rngRet = wc_RNG_GenerateBlock(rng, cekHpke, (word32)keyLen); + if (rngRet != 0) { + ret = WOLFCOSE_E_CRYPTO; + } + } + if (ret == WOLFCOSE_SUCCESS) { + useHpke = 1; + encKey = cekHpke; + } + } + else +#endif #if defined(WOLFCOSE_ECDH_ES_DIRECT) && defined(HAVE_ECC) && defined(HAVE_HKDF) if ((ret == WOLFCOSE_SUCCESS) && (wolfCose_IsEcdhEsDirectAlg(recipients[0].algId) != 0)) { /* ECDH-ES direct is single-recipient only */ @@ -518,18 +559,35 @@ int wc_CoseEncrypt_Encrypt(const WOLFCOSE_RECIPIENT* recipients, /* Encode each recipient */ for (i = 0; (ret == WOLFCOSE_SUCCESS) && (i < recipientCount); i++) { - /* For direct key agreement, the wrapped CEK is empty */ - /* COSE_recipient = [protected, unprotected, ciphertext] */ - - /* Encode recipient protected header. RFC 9053 Section 6.1 (direct key) - * and Section 6.2.1 (AES Key Wrap) both require an empty protected - * bucket; the algorithm is carried in the unprotected header. */ + /* COSE_recipient = [protected, unprotected, ciphertext]. */ + +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) + (void)XMEMSET(&hpkeSealCtx, 0, sizeof(hpkeSealCtx)); + if (useHpke != 0) { + ret = wolfCose_Hpke0EncodeRecipientProtectedHdr( + WOLFCOSE_ALG_HPKE_0_KE, + recipients[i].kid, recipients[i].kidLen, + recipientProtectedBuf, sizeof(recipientProtectedBuf), + &recipientProtectedLen); + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_Hpke0BuildRecipientInfo(contentAlgId, + recipientProtectedBuf, recipientProtectedLen, + scratch, scratchSz, &hpkeRecipientInfoLen); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_Hpke0SealInit(&hpkeSealCtx, + recipients[i].key, WOLFCOSE_ALG_HPKE_0_KE, rng, + hpkeEnc, sizeof(hpkeEnc)); + } + } + else +#endif if (recipients[i].algId == WOLFCOSE_ALG_DIRECT) { - recipientProtectedLen = 0; + recipientProtectedLen = 0u; } #if defined(WOLFCOSE_KEY_WRAP) else if (wolfCose_IsKeyWrapAlg(recipients[i].algId) != 0) { - recipientProtectedLen = 0; + recipientProtectedLen = 0u; } #endif else { @@ -550,6 +608,18 @@ int wc_CoseEncrypt_Encrypt(const WOLFCOSE_RECIPIENT* recipients, /* [1] unprotected header map */ if (ret == WOLFCOSE_SUCCESS) { +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) + if (useHpke != 0) { + ret = wc_CBOR_EncodeMapStart(&ctx, 1u); + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_HDR_HPKE_EK); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeBstr(&ctx, hpkeEnc, sizeof(hpkeEnc)); + } + } + else +#endif #if defined(WOLFCOSE_ECDH_ES_DIRECT) && defined(HAVE_ECC) && defined(HAVE_HKDF) if (useEcdhEs != 0) { /* ECDH-ES: encode kid (label 4 -> 0x04) before ephemeral @@ -622,9 +692,32 @@ int wc_CoseEncrypt_Encrypt(const WOLFCOSE_RECIPIENT* recipients, } } - /* [2] wrapped CEK (empty for direct key and ECDH-ES, computed - * per-recipient for key wrap). */ + /* [2] wrapped CEK. Direct and ECDH-ES use an empty bstr; key wrap and + * HPKE create one ciphertext per recipient. */ if (ret == WOLFCOSE_SUCCESS) { +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) + if (useHpke != 0) { + size_t hpkeCiphertextLen = keyLen + WOLFCOSE_HPKE_0_TAG_SZ; + + ret = wolfCose_CBOR_EncodeHead(&ctx, WOLFCOSE_CBOR_BSTR, + hpkeCiphertextLen); + if ((ret == WOLFCOSE_SUCCESS) && + ((ctx.idx > ctx.bufSz) || + (hpkeCiphertextLen > (ctx.bufSz - ctx.idx)))) { + ret = WOLFCOSE_E_CBOR_OVERFLOW; + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_Hpke0Seal(&hpkeSealCtx, recipients[i].key, + scratch, hpkeRecipientInfoLen, + NULL, 0u, cekHpke, keyLen, + &ctx.buf[ctx.idx], hpkeCiphertextLen); + } + if (ret == WOLFCOSE_SUCCESS) { + ctx.idx += hpkeCiphertextLen; + } + } + else +#endif #if defined(WOLFCOSE_KEY_WRAP) if (useKeyWrap != 0) { ret = wolfCose_KeyWrap(recipients[i].algId, @@ -642,6 +735,10 @@ int wc_CoseEncrypt_Encrypt(const WOLFCOSE_RECIPIENT* recipients, ret = wc_CBOR_EncodeBstr(&ctx, NULL, 0); } } + +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) + wolfCose_Hpke0SealFree(&hpkeSealCtx); +#endif } /* Set output length on success */ @@ -662,6 +759,11 @@ int wc_CoseEncrypt_Encrypt(const WOLFCOSE_RECIPIENT* recipients, #endif #if defined(WOLFCOSE_ECDH_ES_DIRECT) && defined(HAVE_ECC) && defined(HAVE_HKDF) (void)wolfCose_ForceZero(cek, sizeof(cek)); +#endif +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) + wolfCose_Hpke0SealFree(&hpkeSealCtx); + (void)wolfCose_ForceZero(cekHpke, sizeof(cekHpke)); + (void)wolfCose_ForceZero(hpkeEnc, sizeof(hpkeEnc)); #endif if (scratch != NULL) { (void)wolfCose_ForceZero(scratch, scratchSz); @@ -711,12 +813,16 @@ int wc_CoseEncrypt_Decrypt(const WOLFCOSE_RECIPIENT* recipient, const uint8_t* decKey = NULL; const uint8_t* recipientProtectedData = NULL; size_t recipientProtectedLen = 0; - int32_t recipientAlgId = 0; + int32_t recipientAlgId = WOLFCOSE_ALG_UNSET; int recipientMode = 0; WOLFCOSE_HDR recipientHdr; WOLFCOSE_HDR_STATE hdrState; WOLFCOSE_HDR_STATE recipientHdrState; int bodyAlgProtected = 0; +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + int recipientAlgProtected = 0; + int isHpkeRecipient = 0; +#endif #if defined(WOLFCOSE_ECDH_ES_DIRECT) && defined(HAVE_ECC) && defined(HAVE_HKDF) uint8_t cek[32]; uint8_t ephemPubX[66]; @@ -734,6 +840,20 @@ int wc_CoseEncrypt_Decrypt(const WOLFCOSE_RECIPIENT* recipient, size_t unwrappedCekLen = 0; int useKeyWrap = 0; #endif +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + uint8_t cekHpke[32]; + const uint8_t* hpkeWrappedCekData = NULL; + size_t hpkeWrappedCekLen = 0u; + size_t hpkeRecipientInfoLen = 0u; + WOLFCOSE_HPKE_HDR recipHpkeHdr; + int useHpke = 0; +#endif + +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + (void)XMEMSET(cekHpke, 0, sizeof(cekHpke)); + (void)XMEMSET(&recipientHdr, 0, sizeof(recipientHdr)); + (void)XMEMSET(&recipHpkeHdr, 0, sizeof(recipHpkeHdr)); +#endif /* Parameter validation */ if ((recipient == NULL) || (in == NULL) || (inSz == 0u) || @@ -876,8 +996,89 @@ int wc_CoseEncrypt_Decrypt(const WOLFCOSE_RECIPIENT* recipient, ret = wolfCose_DecodeProtectedHdr(recipientProtectedData, recipientProtectedLen, &recipientHdr, &recipientHdrState); + if (ret == WOLFCOSE_SUCCESS) { + recipientAlgId = recipientHdr.alg; + } + } +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + if (ret == WOLFCOSE_SUCCESS) { + recipientAlgProtected = wolfCose_HdrStateContains(&recipientHdrState, + WOLFCOSE_HDR_ALG); + } + if ((ret == WOLFCOSE_SUCCESS) && + (((recipientAlgProtected != 0) && + (recipientAlgId == WOLFCOSE_ALG_HPKE_0_KE)) || + ((recipientAlgProtected == 0) && + (recipient->algId == WOLFCOSE_ALG_HPKE_0_KE)))) { + if ((wolfCose_HdrStateContains(&recipientHdrState, + WOLFCOSE_HDR_HPKE_EK) != 0) || + (wolfCose_HdrStateContains(&recipientHdrState, + WOLFCOSE_HPKE_0_PSK_ID_LABEL) != 0)) { + ret = WOLFCOSE_E_COSE_BAD_HDR; + } + else { + isHpkeRecipient = 1; + recipientAlgId = WOLFCOSE_ALG_HPKE_0_KE; + } + } +#endif + if ((ret == WOLFCOSE_SUCCESS) && + (recipientAlgId != WOLFCOSE_ALG_UNSET)) { + ret = wolfCose_ValidateRecipientKeyAlg(recipient->key, recipientAlgId, + alg); } + /* Classify the recipient key-management algorithm. Only direct, ECDH-ES + * direct, AES key wrap, and enabled HPKE are supported. */ + if (ret == WOLFCOSE_SUCCESS) { + int recipModeOk = 0; + if ((recipientAlgId == WOLFCOSE_ALG_UNSET) || + (recipientAlgId == WOLFCOSE_ALG_DIRECT)) { + recipModeOk = 1; + } +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + else if (recipientAlgId == WOLFCOSE_ALG_HPKE_0_KE) { + recipModeOk = 1; + } +#endif +#if defined(WOLFCOSE_ECDH_ES_DIRECT) && defined(HAVE_ECC) && defined(HAVE_HKDF) + else if (wolfCose_IsEcdhEsDirectAlg(recipientAlgId) != 0) { + recipModeOk = 1; + } +#endif +#if defined(WOLFCOSE_KEY_WRAP) + else if (wolfCose_IsKeyWrapAlg(recipientAlgId) != 0) { + recipModeOk = 1; + } +#endif + else { + /* No action required */ + } + if (recipModeOk == 0) { + ret = WOLFCOSE_E_COSE_BAD_ALG; + } + } + + /* Enforce the caller's recipient->algId policy when the protected header + * supplies an algorithm. */ + if ((ret == WOLFCOSE_SUCCESS) && + (recipientAlgId != WOLFCOSE_ALG_UNSET) && + (recipient->algId != WOLFCOSE_ALG_UNSET)) { + if (recipient->algId != recipientAlgId) { + ret = WOLFCOSE_E_COSE_BAD_ALG; + } + } + +#if defined(WOLFCOSE_ECDH_ES_DIRECT) && defined(HAVE_ECC) && defined(HAVE_HKDF) + /* RFC 9052 Section 8.5.5: direct key agreement carries exactly one + * recipient. */ + if ((ret == WOLFCOSE_SUCCESS) && + (wolfCose_IsEcdhEsDirectAlg(recipientAlgId) != 0) && + ((recipientsCount != 1u) || (recipientIndex != 0u))) { + ret = WOLFCOSE_E_COSE_BAD_HDR; + } +#endif + /* [1] recipient unprotected header */ #if defined(WOLFCOSE_ECDH_ES_DIRECT) && defined(HAVE_ECC) && defined(HAVE_HKDF) if ((ret == WOLFCOSE_SUCCESS) && @@ -957,15 +1158,46 @@ int wc_CoseEncrypt_Decrypt(const WOLFCOSE_RECIPIENT* recipient, if (ret == WOLFCOSE_SUCCESS) { /* Decode the recipient unprotected map with duplicate-label tracking * (within the map and against the recipient protected bucket). */ - ret = wolfCose_DecodeUnprotectedHdr(&ctx, &recipientHdr, +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + if (isHpkeRecipient != 0) { + (void)XMEMSET(&recipHpkeHdr, 0, sizeof(recipHpkeHdr)); + ret = wolfCose_DecodeUnprotectedHdrEx(&ctx, &recipientHdr, + &recipientHdrState, + &recipHpkeHdr); + if ((ret == WOLFCOSE_SUCCESS) && + (((recipientAlgProtected == 0) && + (wolfCose_HdrStateContains(&recipientHdrState, + WOLFCOSE_HDR_ALG) != 0)) || + (recipientHdr.iv != NULL) || + (recipientHdr.partialIv != NULL) || + (recipHpkeHdr.hasEk == 0) || + (recipHpkeHdr.ekLen != WOLFCOSE_HPKE_0_ENC_SZ) || + (wolfCose_HdrStateContains(&recipientHdrState, + WOLFCOSE_HPKE_0_PSK_ID_LABEL) != 0))) { + ret = WOLFCOSE_E_COSE_BAD_HDR; + } + } + else +#endif + { + ret = wolfCose_DecodeUnprotectedHdr(&ctx, &recipientHdr, &recipientHdrState); + } } else { /* No action required */ } if (ret == WOLFCOSE_SUCCESS) { +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + if (isHpkeRecipient == 0) { + recipientAlgId = recipientHdr.alg; + } +#else recipientAlgId = recipientHdr.alg; +#endif + } + if (ret == WOLFCOSE_SUCCESS) { ret = wolfCose_ValidateRecipientKeyAlg(recipient->key, recipientAlgId, alg); } @@ -989,6 +1221,11 @@ int wc_CoseEncrypt_Decrypt(const WOLFCOSE_RECIPIENT* recipient, if (recipientAlgId == WOLFCOSE_ALG_DIRECT) { recipModeOk = 1; } +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + else if (recipientAlgId == WOLFCOSE_ALG_HPKE_0_KE) { + recipModeOk = 1; + } +#endif #if defined(WOLFCOSE_ECDH_ES_DIRECT) && defined(HAVE_ECC) && defined(HAVE_HKDF) else if (wolfCose_IsEcdhEsDirectAlg(recipientAlgId) != 0) { recipModeOk = 1; @@ -1037,13 +1274,22 @@ int wc_CoseEncrypt_Decrypt(const WOLFCOSE_RECIPIENT* recipient, } /* An unprotected content algorithm is safe only when direct-key policy - * independently pins the same algorithm. Other recipient modes pin their - * key-management algorithm, not the content algorithm. */ + * independently pins it, or HPKE-0-KE binds it in the authenticated + * Recipient_structure next_layer_alg field. */ +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) if ((ret == WOLFCOSE_SUCCESS) && (bodyAlgProtected == 0) && + (recipientAlgId != WOLFCOSE_ALG_HPKE_0_KE) && ((recipientAlgId != WOLFCOSE_ALG_DIRECT) || (recipient->key == NULL) || (recipient->key->alg != alg))) { ret = WOLFCOSE_E_COSE_BAD_ALG; } +#else + if ((ret == WOLFCOSE_SUCCESS) && (bodyAlgProtected == 0) && + ((recipientAlgId != WOLFCOSE_ALG_DIRECT) || + (recipient->key == NULL) || (recipient->key->alg != alg))) { + ret = WOLFCOSE_E_COSE_BAD_ALG; + } +#endif #if defined(WOLFCOSE_ECDH_ES_DIRECT) && defined(HAVE_ECC) && defined(HAVE_HKDF) /* RFC 9052 Section 8.5.5: direct key agreement carries exactly one @@ -1056,6 +1302,21 @@ int wc_CoseEncrypt_Decrypt(const WOLFCOSE_RECIPIENT* recipient, #endif /* [2] wrapped CEK */ +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + if ((ret == WOLFCOSE_SUCCESS) && + (recipientAlgId == WOLFCOSE_ALG_HPKE_0_KE)) { + ret = wc_CBOR_DecodeBstr(&ctx, &hpkeWrappedCekData, + &hpkeWrappedCekLen); + if ((ret == WOLFCOSE_SUCCESS) && + (hpkeWrappedCekLen < WOLFCOSE_HPKE_0_TAG_SZ)) { + ret = WOLFCOSE_E_CBOR_MALFORMED; + } + if (ret == WOLFCOSE_SUCCESS) { + useHpke = 1; + } + } + else +#endif #if defined(WOLFCOSE_KEY_WRAP) if ((ret == WOLFCOSE_SUCCESS) && (wolfCose_IsKeyWrapAlg(recipientAlgId) != 0)) { @@ -1123,6 +1384,33 @@ int wc_CoseEncrypt_Decrypt(const WOLFCOSE_RECIPIENT* recipient, } /* Derive/validate decryption key */ +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + if ((ret == WOLFCOSE_SUCCESS) && (useHpke != 0)) { + if ((keyLen > (SIZE_MAX - WOLFCOSE_HPKE_0_TAG_SZ)) || + (hpkeWrappedCekLen != + (keyLen + WOLFCOSE_HPKE_0_TAG_SZ))) { + ret = WOLFCOSE_E_COSE_BAD_HDR; + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_Hpke0BuildRecipientInfo(alg, + recipientProtectedData, recipientProtectedLen, + scratch, scratchSz, &hpkeRecipientInfoLen); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_Hpke0Open(recipient->key, + WOLFCOSE_ALG_HPKE_0_KE, + scratch, hpkeRecipientInfoLen, + NULL, 0u, + recipHpkeHdr.ek, recipHpkeHdr.ekLen, + hpkeWrappedCekData, hpkeWrappedCekLen, + cekHpke, sizeof(cekHpke)); + } + if (ret == WOLFCOSE_SUCCESS) { + decKey = cekHpke; + } + } + else +#endif #if defined(WOLFCOSE_ECDH_ES_DIRECT) && defined(HAVE_ECC) && defined(HAVE_HKDF) if ((ret == WOLFCOSE_SUCCESS) && (useEcdhEs != 0)) { WOLFCOSE_KEY* recipientKey = recipient->key; @@ -1312,6 +1600,9 @@ int wc_CoseEncrypt_Decrypt(const WOLFCOSE_RECIPIENT* recipient, #endif #if defined(WOLFCOSE_ECDH_ES_DIRECT) && defined(HAVE_ECC) && defined(HAVE_HKDF) (void)wolfCose_ForceZero(cek, sizeof(cek)); +#endif +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + (void)wolfCose_ForceZero(cekHpke, sizeof(cekHpke)); #endif if (scratch != NULL) { (void)wolfCose_ForceZero(scratch, scratchSz); diff --git a/src/wolfcose_encrypt0.c b/src/wolfcose_encrypt0.c index f275c1e..a147f08 100644 --- a/src/wolfcose_encrypt0.c +++ b/src/wolfcose_encrypt0.c @@ -743,3 +743,326 @@ int wc_CoseEncrypt0_Decrypt(const WOLFCOSE_KEY* key, #endif /* WOLFCOSE_ENCRYPT0_DECRYPT */ #endif /* WOLFCOSE_ENCRYPT0 && (WOLFCOSE_HAVE_AESGCM || WOLFCOSE_HAVE_AESCCM || (WOLFCOSE_HAVE_CHACHA20)) */ + +/* ----- COSE-HPKE Integrated Encryption API ----- */ + +#if defined(WOLFCOSE_HPKE_0_ENCRYPT) +int wc_CoseHpkeEncrypt0_Encrypt(const WOLFCOSE_KEY* recipientKey, + const uint8_t* kid, size_t kidLen, + const uint8_t* payload, size_t payloadLen, + uint8_t* detachedPayload, size_t detachedSz, size_t* detachedLen, + const uint8_t* extAad, size_t extAadLen, + uint8_t* scratch, size_t scratchSz, + uint8_t* out, size_t outSz, size_t* outLen, + WC_RNG* rng) +{ + int ret = WOLFCOSE_SUCCESS; + uint8_t protectedBuf[WOLFCOSE_PROTECTED_HDR_MAX]; + uint8_t enc[WOLFCOSE_HPKE_0_ENC_SZ]; + size_t protectedLen = 0u; + size_t encStructLen = 0u; + size_t ciphertextLen = 0u; + size_t ciphertextOffset = 0u; + WOLFCOSE_CBOR_CTX ctx; + WOLFCOSE_HPKE_0_SEAL_CTX sealCtx; + int isDetached = 0; + + (void)XMEMSET(&sealCtx, 0, sizeof(sealCtx)); + if (detachedPayload != NULL) { + isDetached = 1; + } + + if ((recipientKey == NULL) || (payload == NULL) || (scratch == NULL) || + (out == NULL) || (outLen == NULL) || (rng == NULL)) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else if (((kid == NULL) && (kidLen != 0u)) || + ((kid != NULL) && (kidLen == 0u))) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else if ((payloadLen > (SIZE_MAX - WOLFCOSE_HPKE_0_TAG_SZ)) || + ((isDetached != 0) && + ((detachedLen == NULL) || + (detachedSz < (payloadLen + WOLFCOSE_HPKE_0_TAG_SZ))))) { + ret = WOLFCOSE_E_BUFFER_TOO_SMALL; + } +#ifdef WOLFCOSE_CHECK_WORD32_LEN + else if ((wolfCose_LenFitsWord32(payloadLen) == 0) || + (wolfCose_LenFitsWord32(extAadLen) == 0) || + (wolfCose_LenFitsWord32(detachedSz) == 0) || + (wolfCose_LenFitsWord32(outSz) == 0) || + (wolfCose_LenFitsWord32(scratchSz) == 0)) { + ret = WOLFCOSE_E_INVALID_ARG; + } +#endif + else { + /* Input parameters are structurally valid. */ + } + + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_EncodeProtectedHdr(WOLFCOSE_ALG_HPKE_0, + protectedBuf, + sizeof(protectedBuf), + &protectedLen); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_BuildEncStructure0(protectedBuf, protectedLen, + extAad, extAadLen, + scratch, scratchSz, &encStructLen); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_Hpke0SealInit(&sealCtx, recipientKey, + WOLFCOSE_ALG_HPKE_0, rng, + enc, sizeof(enc)); + } + if (ret == WOLFCOSE_SUCCESS) { + ciphertextLen = payloadLen + WOLFCOSE_HPKE_0_TAG_SZ; + ctx.buf = out; + ctx.bufSz = outSz; + ctx.idx = 0u; + ret = wc_CBOR_EncodeTag(&ctx, WOLFCOSE_TAG_ENCRYPT0); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeArrayStart(&ctx, 3u); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeBstr(&ctx, protectedBuf, protectedLen); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeMapStart(&ctx, (kid != NULL) ? 2u : 1u); + } + if ((ret == WOLFCOSE_SUCCESS) && (kid != NULL)) { + ret = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_HDR_KID); + } + if ((ret == WOLFCOSE_SUCCESS) && (kid != NULL)) { + ret = wc_CBOR_EncodeBstr(&ctx, kid, kidLen); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_HDR_HPKE_EK); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeBstr(&ctx, enc, sizeof(enc)); + } + + if ((ret == WOLFCOSE_SUCCESS) && (isDetached != 0)) { + ret = wc_CBOR_EncodeNull(&ctx); + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_Hpke0Seal(&sealCtx, recipientKey, + NULL, 0u, scratch, encStructLen, + payload, payloadLen, detachedPayload, detachedSz); + } + if (ret == WOLFCOSE_SUCCESS) { + *detachedLen = ciphertextLen; + } + } + else if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_CBOR_EncodeHead(&ctx, WOLFCOSE_CBOR_BSTR, + (uint64_t)ciphertextLen); + if ((ret == WOLFCOSE_SUCCESS) && + ((ctx.idx > ctx.bufSz) || + (ciphertextLen > (ctx.bufSz - ctx.idx)))) { + ret = WOLFCOSE_E_BUFFER_TOO_SMALL; + } + if (ret == WOLFCOSE_SUCCESS) { + ciphertextOffset = ctx.idx; + ret = wolfCose_Hpke0Seal(&sealCtx, recipientKey, + NULL, 0u, scratch, encStructLen, + payload, payloadLen, &out[ciphertextOffset], ciphertextLen); + } + if (ret == WOLFCOSE_SUCCESS) { + ctx.idx += ciphertextLen; + } + } + else { + /* No action required. */ + } + + if (ret == WOLFCOSE_SUCCESS) { + *outLen = ctx.idx; + } + + wolfCose_Hpke0SealFree(&sealCtx); + (void)wolfCose_ForceZero(enc, sizeof(enc)); + (void)wolfCose_ForceZero(protectedBuf, sizeof(protectedBuf)); + if (scratch != NULL) { + (void)wolfCose_ForceZero(scratch, scratchSz); + } + if (ret != WOLFCOSE_SUCCESS) { + if (out != NULL) { + (void)wolfCose_ForceZero(out, outSz); + } + if (outLen != NULL) { + *outLen = 0u; + } + if ((isDetached != 0) && (detachedPayload != NULL)) { + (void)wolfCose_ForceZero(detachedPayload, detachedSz); + } + if (detachedLen != NULL) { + *detachedLen = 0u; + } + } + + return ret; +} +#endif /* WOLFCOSE_HPKE_0_ENCRYPT */ + +#if defined(WOLFCOSE_HPKE_0_DECRYPT) +int wc_CoseHpkeEncrypt0_Decrypt(const WOLFCOSE_KEY* recipientKey, + const uint8_t* in, size_t inSz, + const uint8_t* detachedCt, size_t detachedCtLen, + const uint8_t* extAad, size_t extAadLen, + uint8_t* scratch, size_t scratchSz, + WOLFCOSE_HDR* hdr, + uint8_t* plaintext, size_t plaintextSz, size_t* plaintextLen) +{ + int ret = WOLFCOSE_SUCCESS; + WOLFCOSE_CBOR_CTX ctx; + WOLFCOSE_CBOR_ITEM item; + uint64_t tag = 0u; + size_t arrayCount = 0u; + const uint8_t* protectedData = NULL; + size_t protectedLen = 0u; + const uint8_t* ciphertext = NULL; + size_t ciphertextLen = 0u; + size_t encStructLen = 0u; + size_t payloadLen = 0u; + WOLFCOSE_HDR_STATE hdrState; + WOLFCOSE_HPKE_HDR hpkeHdr; + int algProtected = 0; + + if ((recipientKey == NULL) || (in == NULL) || (inSz == 0u) || + (scratch == NULL) || (hdr == NULL) || (plaintext == NULL) || + (plaintextLen == NULL)) { + ret = WOLFCOSE_E_INVALID_ARG; + } +#ifdef WOLFCOSE_CHECK_WORD32_LEN + else if ((wolfCose_LenFitsWord32(inSz) == 0) || + (wolfCose_LenFitsWord32(detachedCtLen) == 0) || + (wolfCose_LenFitsWord32(extAadLen) == 0) || + (wolfCose_LenFitsWord32(plaintextSz) == 0) || + (wolfCose_LenFitsWord32(scratchSz) == 0)) { + ret = WOLFCOSE_E_INVALID_ARG; + } +#endif + else { + (void)XMEMSET(hdr, 0, sizeof(*hdr)); + (void)XMEMSET(&hpkeHdr, 0, sizeof(hpkeHdr)); + ctx.cbuf = in; + ctx.bufSz = inSz; + ctx.idx = 0u; + } + + if ((ret == WOLFCOSE_SUCCESS) && (ctx.idx < ctx.bufSz) && + (wc_CBOR_PeekType(&ctx) == WOLFCOSE_CBOR_TAG)) { + ret = wc_CBOR_DecodeTag(&ctx, &tag); + if ((ret == WOLFCOSE_SUCCESS) && (tag != WOLFCOSE_TAG_ENCRYPT0)) { + ret = WOLFCOSE_E_COSE_BAD_TAG; + } + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_DecodeArrayStart(&ctx, &arrayCount); + } + if ((ret == WOLFCOSE_SUCCESS) && (arrayCount != 3u)) { + ret = WOLFCOSE_E_CBOR_MALFORMED; + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_DecodeBstr(&ctx, &protectedData, &protectedLen); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_DecodeProtectedHdr(protectedData, protectedLen, hdr, + &hdrState); + } + if (ret == WOLFCOSE_SUCCESS) { + algProtected = wolfCose_HdrStateContains(&hdrState, + WOLFCOSE_HDR_ALG); + } + if ((ret == WOLFCOSE_SUCCESS) && + (((algProtected != 0) && (hdr->alg != WOLFCOSE_ALG_HPKE_0)) || + (wolfCose_HdrStateContains(&hdrState, + WOLFCOSE_HDR_HPKE_EK) != 0) || + (wolfCose_HdrStateContains(&hdrState, + WOLFCOSE_HPKE_0_PSK_ID_LABEL) != 0))) { + ret = WOLFCOSE_E_COSE_BAD_HDR; + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_DecodeUnprotectedHdrEx(&ctx, hdr, &hdrState, + &hpkeHdr); + } + if ((ret == WOLFCOSE_SUCCESS) && + (((algProtected == 0) && + (wolfCose_HdrStateContains(&hdrState, WOLFCOSE_HDR_ALG) != 0)) || + (hdr->iv != NULL) || (hdr->partialIv != NULL) || + (hpkeHdr.hasEk == 0) || + (hpkeHdr.ekLen != WOLFCOSE_HPKE_0_ENC_SZ) || + (wolfCose_HdrStateContains(&hdrState, + WOLFCOSE_HPKE_0_PSK_ID_LABEL) != 0))) { + ret = WOLFCOSE_E_COSE_BAD_HDR; + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_CBOR_DecodeHead(&ctx, &item); + } + if (ret == WOLFCOSE_SUCCESS) { + if ((item.majorType == WOLFCOSE_CBOR_SIMPLE) && (item.val == 22u)) { + if (detachedCt == NULL) { + hdr->flags |= WOLFCOSE_HDR_FLAG_DETACHED; + ret = WOLFCOSE_E_DETACHED_PAYLOAD; + } + else { + ciphertext = detachedCt; + ciphertextLen = detachedCtLen; + hdr->flags |= WOLFCOSE_HDR_FLAG_DETACHED; + } + } + else if (item.majorType == WOLFCOSE_CBOR_BSTR) { + ciphertext = item.data; + ciphertextLen = item.dataLen; + } + else { + ret = WOLFCOSE_E_CBOR_TYPE; + } + } + if ((ret == WOLFCOSE_SUCCESS) && (ctx.idx != ctx.bufSz)) { + ret = WOLFCOSE_E_CBOR_MALFORMED; + } + if ((ret == WOLFCOSE_SUCCESS) && + (ciphertextLen < WOLFCOSE_HPKE_0_TAG_SZ)) { + ret = WOLFCOSE_E_CBOR_MALFORMED; + } + if (ret == WOLFCOSE_SUCCESS) { + payloadLen = ciphertextLen - WOLFCOSE_HPKE_0_TAG_SZ; + if (payloadLen > plaintextSz) { + ret = WOLFCOSE_E_BUFFER_TOO_SMALL; + } + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_BuildEncStructure0(protectedData, protectedLen, + extAad, extAadLen, + scratch, scratchSz, &encStructLen); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_Hpke0Open(recipientKey, WOLFCOSE_ALG_HPKE_0, + NULL, 0u, scratch, encStructLen, + hpkeHdr.ek, hpkeHdr.ekLen, + ciphertext, ciphertextLen, plaintext, plaintextSz); + } + if (ret == WOLFCOSE_SUCCESS) { + *plaintextLen = payloadLen; + } + else if (plaintextLen != NULL) { + *plaintextLen = 0u; + } + else { + /* No action required. */ + } + + wolfCose_HdrClearOnFail(ret, hdr); + if (scratch != NULL) { + (void)wolfCose_ForceZero(scratch, scratchSz); + } + if ((ret != WOLFCOSE_SUCCESS) && (plaintext != NULL)) { + (void)wolfCose_ForceZero(plaintext, plaintextSz); + } + + return ret; +} +#endif /* WOLFCOSE_HPKE_0_DECRYPT */ diff --git a/src/wolfcose_hdr.c b/src/wolfcose_hdr.c index 0647060..6156021 100644 --- a/src/wolfcose_hdr.c +++ b/src/wolfcose_hdr.c @@ -533,8 +533,14 @@ int wolfCose_DecodeProtectedHdr(const uint8_t* data, size_t dataLen, return ret; } -int wolfCose_DecodeUnprotectedHdr(WOLFCOSE_CBOR_CTX* ctx, WOLFCOSE_HDR* hdr, - WOLFCOSE_HDR_STATE* hdrState) +#if defined(WOLFCOSE_HAVE_HPKE_0) +int wolfCose_DecodeUnprotectedHdrEx(WOLFCOSE_CBOR_CTX* ctx, + WOLFCOSE_HDR* hdr, WOLFCOSE_HDR_STATE* hdrState, + WOLFCOSE_HPKE_HDR* hpkeHdr) +#else +static int wolfCose_DecodeUnprotectedHdrEx(WOLFCOSE_CBOR_CTX* ctx, + WOLFCOSE_HDR* hdr, WOLFCOSE_HDR_STATE* hdrState) +#endif { int ret; size_t mapCount = 0; @@ -606,6 +612,23 @@ int wolfCose_DecodeUnprotectedHdr(WOLFCOSE_CBOR_CTX* ctx, WOLFCOSE_HDR* hdr, hdr->partialIvLen = bstrLen; } } +#if defined(WOLFCOSE_HAVE_HPKE_0) + else if ((ret == WOLFCOSE_SUCCESS) && + (wc_CBOR_LabelIsInt(&label, + WOLFCOSE_HDR_HPKE_EK) != 0)) { + if (hpkeHdr == NULL) { + ret = wc_CBOR_Skip(ctx); + } + else { + ret = wc_CBOR_DecodeBstr(ctx, &bstrData, &bstrLen); + if (ret == WOLFCOSE_SUCCESS) { + hpkeHdr->ek = bstrData; + hpkeHdr->ekLen = bstrLen; + hpkeHdr->hasEk = 1; + } + } + } +#endif else if ((ret == WOLFCOSE_SUCCESS) && (wc_CBOR_LabelIsInt(&label, WOLFCOSE_HDR_ALG) != 0)) { if ((ctx->idx < ctx->bufSz) && @@ -667,12 +690,22 @@ int wolfCose_DecodeUnprotectedHdr(WOLFCOSE_CBOR_CTX* ctx, WOLFCOSE_HDR* hdr, return ret; } +int wolfCose_DecodeUnprotectedHdr(WOLFCOSE_CBOR_CTX* ctx, + WOLFCOSE_HDR* hdr, WOLFCOSE_HDR_STATE* hdrState) +{ +#if defined(WOLFCOSE_HAVE_HPKE_0) + return wolfCose_DecodeUnprotectedHdrEx(ctx, hdr, hdrState, NULL); +#else + return wolfCose_DecodeUnprotectedHdrEx(ctx, hdr, hdrState); +#endif +} + #if defined(WOLFCOSE_SIGN_VERIFY) || defined(WOLFCOSE_ENCRYPT_DECRYPT) || \ defined(WOLFCOSE_MAC_VERIFY) || defined(WOLFCOSE_COUNTERSIGN) /* Decode only the algorithm from an unselected header map. Other labels and * values are intentionally left to the application that selected the entry. */ static int wolfCose_DecodeSkippedHdrAlg(WOLFCOSE_CBOR_CTX* ctx, - int32_t* alg, int* algFound) + int32_t* alg, int* algFound, int* hpkeEkFound) { int ret; size_t mapCount = 0u; @@ -682,6 +715,13 @@ static int wolfCose_DecodeSkippedHdrAlg(WOLFCOSE_CBOR_CTX* ctx, ret = WOLFCOSE_E_INVALID_ARG; } else { +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + if (hpkeEkFound != NULL) { + *hpkeEkFound = 0; + } +#else + (void)hpkeEkFound; +#endif ret = wc_CBOR_DecodeMapStart(ctx, &mapCount); } if ((ret == WOLFCOSE_SUCCESS) && (mapCount > ctx->bufSz)) { @@ -717,6 +757,20 @@ static int wolfCose_DecodeSkippedHdrAlg(WOLFCOSE_CBOR_CTX* ctx, } } } +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + else if ((ret == WOLFCOSE_SUCCESS) && + (wc_CBOR_LabelIsInt(&label, WOLFCOSE_HDR_HPKE_EK) != 0)) { + if ((hpkeEkFound != NULL) && (*hpkeEkFound != 0)) { + ret = WOLFCOSE_E_CBOR_MALFORMED; + } + else { + if (hpkeEkFound != NULL) { + *hpkeEkFound = 1; + } + ret = wc_CBOR_Skip(ctx); + } + } +#endif else if (ret == WOLFCOSE_SUCCESS) { ret = wc_CBOR_Skip(ctx); } @@ -733,7 +787,7 @@ static int wolfCose_DecodeSkippedHdrAlg(WOLFCOSE_CBOR_CTX* ctx, * bstr or null value and may have a fourth nested-recipients field. */ static int wolfCose_DecodeSkippedHeaderEntry(WOLFCOSE_CBOR_CTX* ctx, size_t maxArrayCount, size_t* arrayCount, int32_t* alg, - uint8_t isSignature) + uint8_t isSignature, int* algFoundOut, int* hpkeEkFound) { int ret; const uint8_t* protectedData = NULL; @@ -750,6 +804,14 @@ static int wolfCose_DecodeSkippedHeaderEntry(WOLFCOSE_CBOR_CTX* ctx, if (alg != NULL) { *alg = WOLFCOSE_ALG_UNSET; } + if (algFoundOut != NULL) { + *algFoundOut = 0; + } +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + if (hpkeEkFound != NULL) { + *hpkeEkFound = 0; + } +#endif ret = wc_CBOR_DecodeArrayStart(ctx, arrayCount); } if ((ret == WOLFCOSE_SUCCESS) && @@ -766,14 +828,16 @@ static int wolfCose_DecodeSkippedHeaderEntry(WOLFCOSE_CBOR_CTX* ctx, (void)XMEMSET(&protectedCtx, 0, sizeof(protectedCtx)); protectedCtx.cbuf = protectedData; protectedCtx.bufSz = protectedLen; - ret = wolfCose_DecodeSkippedHdrAlg(&protectedCtx, alg, &algFound); + ret = wolfCose_DecodeSkippedHdrAlg(&protectedCtx, alg, &algFound, + NULL); if ((ret == WOLFCOSE_SUCCESS) && (protectedCtx.idx != protectedCtx.bufSz)) { ret = WOLFCOSE_E_CBOR_MALFORMED; } } if ((ret == WOLFCOSE_SUCCESS) && (alg != NULL)) { - ret = wolfCose_DecodeSkippedHdrAlg(ctx, alg, &algFound); + ret = wolfCose_DecodeSkippedHdrAlg(ctx, alg, &algFound, + hpkeEkFound); } else if (ret == WOLFCOSE_SUCCESS) { ret = wc_CBOR_Skip(ctx); @@ -787,6 +851,9 @@ static int wolfCose_DecodeSkippedHeaderEntry(WOLFCOSE_CBOR_CTX* ctx, (*alg == WOLFCOSE_ALG_DIRECT) && (protectedLen != 0u)) { ret = WOLFCOSE_E_COSE_BAD_HDR; } + if ((ret == WOLFCOSE_SUCCESS) && (algFoundOut != NULL)) { + *algFoundOut = algFound; + } if ((ret == WOLFCOSE_SUCCESS) && (isSignature != 0u)) { ret = wc_CBOR_DecodeBstr(ctx, &valueData, &valueLen); } @@ -824,7 +891,8 @@ int wolfCose_DecodeSkippedSignature(WOLFCOSE_CBOR_CTX* ctx) { size_t arrayCount = 0u; - return wolfCose_DecodeSkippedHeaderEntry(ctx, 3u, &arrayCount, NULL, 1u); + return wolfCose_DecodeSkippedHeaderEntry(ctx, 3u, &arrayCount, NULL, 1u, + NULL, NULL); } #endif @@ -852,10 +920,25 @@ int wolfCose_DecodeSkippedRecipient(WOLFCOSE_CBOR_CTX* ctx, while ((ret == WOLFCOSE_SUCCESS) && (remaining > 0u)) { size_t arrayCount = 0u; int32_t decodedAlg = WOLFCOSE_ALG_UNSET; + int algFound = 0; +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + int hpkeEkFound = 0; +#endif ret = wolfCose_DecodeSkippedHeaderEntry(ctx, 4u, &arrayCount, - &decodedAlg, 0u); + &decodedAlg, 0u, +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + &algFound, &hpkeEkFound); +#else + &algFound, NULL); +#endif remaining--; +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + if ((ret == WOLFCOSE_SUCCESS) && + (algFound == 0) && (hpkeEkFound != 0)) { + decodedAlg = WOLFCOSE_ALG_HPKE_0_KE; + } +#endif if ((ret == WOLFCOSE_SUCCESS) && (arrayCount == 4u) && (decodedAlg == WOLFCOSE_ALG_DIRECT)) { ret = WOLFCOSE_E_COSE_BAD_HDR; diff --git a/src/wolfcose_internal.h b/src/wolfcose_internal.h index dbc7e95..ff4fc8e 100644 --- a/src/wolfcose_internal.h +++ b/src/wolfcose_internal.h @@ -33,6 +33,9 @@ #if defined(WOLFCOSE_HAVE_CHACHA20) #include #endif +#if defined(WOLFCOSE_HAVE_HPKE_0) + #include +#endif #ifdef WOLFCOSE_FORCE_FAILURE #include "../tests/force_failure.h" #endif @@ -157,6 +160,27 @@ typedef struct WOLFCOSE_HDR_STATE { size_t extraTextCount; } WOLFCOSE_HDR_STATE; +#if defined(WOLFCOSE_HAVE_HPKE_0) +#define WOLFCOSE_HPKE_0_ENC_SZ 65u +#define WOLFCOSE_HPKE_0_TAG_SZ 16u +#define WOLFCOSE_HPKE_0_PSK_ID_LABEL (-5) + +typedef struct WOLFCOSE_HPKE_HDR { + const uint8_t* ek; + size_t ekLen; + int hasEk; +} WOLFCOSE_HPKE_HDR; + +#if defined(WOLFCOSE_HPKE_0_ENCRYPT) || \ + defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) +typedef struct WOLFCOSE_HPKE_0_SEAL_CTX { + Hpke hpke; + ecc_key ephemeralKey; + int ephemeralInited; +} WOLFCOSE_HPKE_0_SEAL_CTX; +#endif +#endif + /* ----- COSE internal helpers ----- */ /** @@ -461,6 +485,55 @@ WOLFCOSE_LOCAL int wolfCose_HdrStateContainsLabel(const WOLFCOSE_HDR_STATE* stat WOLFCOSE_LOCAL int wolfCose_HdrStateAddLabel(WOLFCOSE_HDR_STATE* state, const WOLFCOSE_CBOR_LABEL* label, const uint8_t* encodedLabel); +#if defined(WOLFCOSE_HAVE_HPKE_0) +WOLFCOSE_LOCAL int wolfCose_DecodeUnprotectedHdrEx(WOLFCOSE_CBOR_CTX* ctx, + WOLFCOSE_HDR* hdr, WOLFCOSE_HDR_STATE* hdrState, + WOLFCOSE_HPKE_HDR* hpkeHdr); + +WOLFCOSE_LOCAL int wolfCose_Hpke0ValidateKey(const WOLFCOSE_KEY* key, + int32_t alg, int needPrivate); + +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) || \ + defined(WOLFCOSE_HPKE_0_KE_DECRYPT) +WOLFCOSE_LOCAL int wolfCose_Hpke0BuildRecipientInfo(int32_t nextLayerAlg, + const uint8_t* recipientProtected, size_t recipientProtectedLen, + uint8_t* scratch, size_t scratchSz, size_t* infoLen); +#endif + +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) +WOLFCOSE_LOCAL int wolfCose_Hpke0EncodeRecipientProtectedHdr(int32_t alg, + const uint8_t* kid, size_t kidLen, + uint8_t* buf, size_t bufSz, size_t* outLen); +#endif + +#if defined(WOLFCOSE_HPKE_0_ENCRYPT) || \ + defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) +WOLFCOSE_LOCAL int wolfCose_Hpke0SealInit( + WOLFCOSE_HPKE_0_SEAL_CTX* sealCtx, + const WOLFCOSE_KEY* recipientKey, int32_t alg, WC_RNG* rng, + uint8_t* enc, size_t encSz); +WOLFCOSE_LOCAL int wolfCose_Hpke0Seal(WOLFCOSE_HPKE_0_SEAL_CTX* sealCtx, + const WOLFCOSE_KEY* recipientKey, + uint8_t* info, size_t infoLen, + uint8_t* aad, size_t aadLen, + const uint8_t* plaintext, size_t plaintextLen, + uint8_t* ciphertext, size_t ciphertextSz); +WOLFCOSE_LOCAL void wolfCose_Hpke0SealFree( + WOLFCOSE_HPKE_0_SEAL_CTX* sealCtx); +#endif + +#if defined(WOLFCOSE_HPKE_0_DECRYPT) || \ + defined(WOLFCOSE_HPKE_0_KE_DECRYPT) +WOLFCOSE_LOCAL int wolfCose_Hpke0Open(const WOLFCOSE_KEY* recipientKey, + int32_t alg, + uint8_t* info, size_t infoLen, + uint8_t* aad, size_t aadLen, + const uint8_t* enc, size_t encLen, + const uint8_t* ciphertext, size_t ciphertextLen, + uint8_t* plaintext, size_t plaintextSz); +#endif +#endif + #if defined(WOLFCOSE_SIGN_VERIFY) || defined(WOLFCOSE_ENCRYPT_DECRYPT) || \ defined(WOLFCOSE_MAC_VERIFY) || defined(WOLFCOSE_COUNTERSIGN) diff --git a/src/wolfcose_recipient.c b/src/wolfcose_recipient.c index 719e4b4..230a9e8 100644 --- a/src/wolfcose_recipient.c +++ b/src/wolfcose_recipient.c @@ -978,3 +978,351 @@ int wolfCose_LenFitsWord32(size_t n) return ret; } #endif + +#if defined(WOLFCOSE_HAVE_HPKE_0) + +/* Validate the fixed P-256 HPKE-0 key representation. The caller-provided + * COSE metadata and the attached wolfCrypt key must both agree on P-256. */ +int wolfCose_Hpke0ValidateKey(const WOLFCOSE_KEY* key, + int32_t alg, int needPrivate) +{ + int ret = WOLFCOSE_SUCCESS; + ecc_key* eccKey = NULL; + + if (key == NULL) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else if (key->attachedType != WOLFCOSE_ATT_ECC) { + ret = WOLFCOSE_E_COSE_KEY_TYPE; + } + else { + eccKey = key->key.ecc; + if ((key->kty != WOLFCOSE_KTY_EC2) || + (key->crv != WOLFCOSE_CRV_P256) || + (eccKey == NULL) || + (wc_ecc_size(eccKey) != 32)) { + ret = WOLFCOSE_E_COSE_KEY_TYPE; + } + else if (wolfCose_EccKeyCheckCurve(WOLFCOSE_CRV_P256, + eccKey) != WOLFCOSE_SUCCESS) { + ret = WOLFCOSE_E_COSE_KEY_TYPE; + } + else if (wc_ecc_check_key(eccKey) != 0) { + ret = WOLFCOSE_E_COSE_KEY_TYPE; + } + else if ((key->alg != WOLFCOSE_ALG_UNSET) && (key->alg != alg)) { + ret = WOLFCOSE_E_COSE_BAD_ALG; + } + else if ((needPrivate != 0) && (key->hasPrivate != 1u)) { + ret = WOLFCOSE_E_COSE_KEY_TYPE; + } + else { + /* Key is valid. */ + } + } + + return ret; +} + +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) || \ + defined(WOLFCOSE_HPKE_0_KE_DECRYPT) +/* Serialize the deterministic Recipient_structure used as HPKE info for + * HPKE-0-KE. recipient_extra_info is intentionally the empty bstr in P0. */ +int wolfCose_Hpke0BuildRecipientInfo(int32_t nextLayerAlg, + const uint8_t* recipientProtected, size_t recipientProtectedLen, + uint8_t* scratch, size_t scratchSz, size_t* infoLen) +{ + static const uint8_t recipientContext[14] = { + 0x48u, 0x50u, 0x4Bu, 0x45u, 0x20u, 0x52u, 0x65u, + 0x63u, 0x69u, 0x70u, 0x69u, 0x65u, 0x6Eu, 0x74u + }; + int ret; + WOLFCOSE_CBOR_CTX ctx; + + if ((recipientProtected == NULL) || (scratch == NULL) || + (infoLen == NULL)) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else { + ctx.buf = scratch; + ctx.bufSz = scratchSz; + ctx.idx = 0u; + + ret = wc_CBOR_EncodeArrayStart(&ctx, 4u); + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeTstr(&ctx, recipientContext, + sizeof(recipientContext)); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeInt(&ctx, (int64_t)nextLayerAlg); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeBstr(&ctx, recipientProtected, + recipientProtectedLen); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeBstr(&ctx, NULL, 0u); + } + if (ret == WOLFCOSE_SUCCESS) { + *infoLen = ctx.idx; + } + } + + return ret; +} +#endif /* WOLFCOSE_HPKE_0_KE_ENCRYPT || WOLFCOSE_HPKE_0_KE_DECRYPT */ + +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) +/* Encode recipient protected headers in deterministic label order. Including + * kid here binds recipient selection into the HPKE Recipient_structure. */ +int wolfCose_Hpke0EncodeRecipientProtectedHdr(int32_t alg, + const uint8_t* kid, size_t kidLen, + uint8_t* buf, size_t bufSz, size_t* outLen) +{ + int ret; + WOLFCOSE_CBOR_CTX ctx; + size_t mapEntries; + + if ((buf == NULL) || (outLen == NULL) || + ((kid == NULL) && (kidLen != 0u)) || + ((kid != NULL) && (kidLen == 0u))) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else { + mapEntries = (size_t)1u; + if (kid != NULL) { + mapEntries = (size_t)2u; + } + ctx.buf = buf; + ctx.bufSz = bufSz; + ctx.idx = 0u; + + ret = wc_CBOR_EncodeMapStart(&ctx, mapEntries); + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_HDR_ALG); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeInt(&ctx, (int64_t)alg); + } + if ((ret == WOLFCOSE_SUCCESS) && (kid != NULL)) { + ret = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_HDR_KID); + } + if ((ret == WOLFCOSE_SUCCESS) && (kid != NULL)) { + ret = wc_CBOR_EncodeBstr(&ctx, kid, kidLen); + } + if (ret == WOLFCOSE_SUCCESS) { + *outLen = ctx.idx; + } + } + + return ret; +} +#endif /* WOLFCOSE_HPKE_0_KE_ENCRYPT */ + +#if defined(WOLFCOSE_HPKE_0_ENCRYPT) || \ + defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) +/* Generate and serialize the ephemeral P-256 key before the COSE headers are + * emitted. The caller then seals into its final ciphertext destination. */ +int wolfCose_Hpke0SealInit(WOLFCOSE_HPKE_0_SEAL_CTX* sealCtx, + const WOLFCOSE_KEY* recipientKey, int32_t alg, WC_RNG* rng, + uint8_t* enc, size_t encSz) +{ + int ret = WOLFCOSE_SUCCESS; + int hpkeRet; + word16 encLen = (word16)WOLFCOSE_HPKE_0_ENC_SZ; + + if (sealCtx != NULL) { + (void)XMEMSET(sealCtx, 0, sizeof(*sealCtx)); + } + + if ((sealCtx == NULL) || (rng == NULL) || (enc == NULL)) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else if (encSz < WOLFCOSE_HPKE_0_ENC_SZ) { + ret = WOLFCOSE_E_BUFFER_TOO_SMALL; + } + else { + ret = wolfCose_Hpke0ValidateKey(recipientKey, alg, 0); + } + + if (ret == WOLFCOSE_SUCCESS) { + hpkeRet = wc_HpkeInit(&sealCtx->hpke, DHKEM_P256_HKDF_SHA256, + HKDF_SHA256, HPKE_AES_128_GCM, NULL); + if (hpkeRet != 0) { + ret = WOLFCOSE_E_CRYPTO; + } + } + if (ret == WOLFCOSE_SUCCESS) { + hpkeRet = wc_ecc_init(&sealCtx->ephemeralKey); + if (hpkeRet != 0) { + ret = WOLFCOSE_E_CRYPTO; + } + else { + sealCtx->ephemeralInited = 1; + } + } + if (ret == WOLFCOSE_SUCCESS) { + hpkeRet = wc_ecc_make_key_ex(rng, 32, &sealCtx->ephemeralKey, + ECC_SECP256R1); + if (hpkeRet != 0) { + ret = WOLFCOSE_E_CRYPTO; + } + } + if (ret == WOLFCOSE_SUCCESS) { + hpkeRet = wc_HpkeSerializePublicKey(&sealCtx->hpke, + &sealCtx->ephemeralKey, + enc, &encLen); + if ((hpkeRet != 0) || (encLen != WOLFCOSE_HPKE_0_ENC_SZ)) { + ret = WOLFCOSE_E_CRYPTO; + } + } + + if ((ret != WOLFCOSE_SUCCESS) && (sealCtx != NULL)) { + if (sealCtx->ephemeralInited != 0) { + (void)wc_ecc_free(&sealCtx->ephemeralKey); + } + (void)wolfCose_ForceZero(sealCtx, sizeof(*sealCtx)); + } + + return ret; +} + +/* Perform the one-shot HPKE seal with a prepared ephemeral key. */ +int wolfCose_Hpke0Seal(WOLFCOSE_HPKE_0_SEAL_CTX* sealCtx, + const WOLFCOSE_KEY* recipientKey, + uint8_t* info, size_t infoLen, + uint8_t* aad, size_t aadLen, + const uint8_t* plaintext, size_t plaintextLen, + uint8_t* ciphertext, size_t ciphertextSz) +{ + int ret = WOLFCOSE_SUCCESS; + int hpkeRet; + ecc_key* recipientEcc = NULL; + + if ((sealCtx == NULL) || (recipientKey == NULL) || + (plaintext == NULL) || (ciphertext == NULL)) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else if ((info == NULL) && (infoLen != 0u)) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else if ((aad == NULL) && (aadLen != 0u)) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else if ((plaintextLen > (SIZE_MAX - WOLFCOSE_HPKE_0_TAG_SZ)) || + (ciphertextSz < (plaintextLen + WOLFCOSE_HPKE_0_TAG_SZ))) { + ret = WOLFCOSE_E_BUFFER_TOO_SMALL; + } +#ifdef WOLFCOSE_CHECK_WORD32_LEN + else if ((wolfCose_LenFitsWord32(infoLen) == 0) || + (wolfCose_LenFitsWord32(aadLen) == 0) || + (wolfCose_LenFitsWord32(plaintextLen) == 0)) { + ret = WOLFCOSE_E_INVALID_ARG; + } +#endif + else if (sealCtx->ephemeralInited == 0) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else { + /* Seal context is valid. */ + } + + if (ret == WOLFCOSE_SUCCESS) { + recipientEcc = recipientKey->key.ecc; + hpkeRet = wc_HpkeSealBase(&sealCtx->hpke, + &sealCtx->ephemeralKey, recipientEcc, + info, (word32)infoLen, + aad, (word32)aadLen, + (byte*)plaintext, (word32)plaintextLen, ciphertext); + if (hpkeRet != 0) { + ret = WOLFCOSE_E_CRYPTO; + } + } + + return ret; +} + +void wolfCose_Hpke0SealFree(WOLFCOSE_HPKE_0_SEAL_CTX* sealCtx) +{ + if (sealCtx != NULL) { + if (sealCtx->ephemeralInited != 0) { + (void)wc_ecc_free(&sealCtx->ephemeralKey); + } + (void)wolfCose_ForceZero(sealCtx, sizeof(*sealCtx)); + } +} +#endif /* WOLFCOSE_HPKE_0_ENCRYPT || WOLFCOSE_HPKE_0_KE_ENCRYPT */ + +#if defined(WOLFCOSE_HPKE_0_DECRYPT) || \ + defined(WOLFCOSE_HPKE_0_KE_DECRYPT) +/* Perform a single HPKE base-mode open with the fixed HPKE-0 suite. */ +int wolfCose_Hpke0Open(const WOLFCOSE_KEY* recipientKey, + int32_t alg, + uint8_t* info, size_t infoLen, + uint8_t* aad, size_t aadLen, + const uint8_t* enc, size_t encLen, + const uint8_t* ciphertext, size_t ciphertextLen, + uint8_t* plaintext, size_t plaintextSz) +{ + int ret = WOLFCOSE_SUCCESS; + int hpkeRet; + Hpke hpke; + ecc_key* recipientEcc = NULL; + size_t ciphertextBodyLen = 0u; + + if ((enc == NULL) || (ciphertext == NULL) || (plaintext == NULL)) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else if ((info == NULL) && (infoLen != 0u)) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else if ((aad == NULL) && (aadLen != 0u)) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else if ((encLen != WOLFCOSE_HPKE_0_ENC_SZ) || + (ciphertextLen < WOLFCOSE_HPKE_0_TAG_SZ)) { + ret = WOLFCOSE_E_COSE_BAD_HDR; + } + else { + ciphertextBodyLen = ciphertextLen - WOLFCOSE_HPKE_0_TAG_SZ; + if (plaintextSz < ciphertextBodyLen) { + ret = WOLFCOSE_E_BUFFER_TOO_SMALL; + } + } +#ifdef WOLFCOSE_CHECK_WORD32_LEN + if ((ret == WOLFCOSE_SUCCESS) && + ((wolfCose_LenFitsWord32(infoLen) == 0) || + (wolfCose_LenFitsWord32(aadLen) == 0) || + (wolfCose_LenFitsWord32(ciphertextBodyLen) == 0))) { + ret = WOLFCOSE_E_INVALID_ARG; + } +#endif + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_Hpke0ValidateKey(recipientKey, alg, 1); + } + if (ret == WOLFCOSE_SUCCESS) { + hpkeRet = wc_HpkeInit(&hpke, DHKEM_P256_HKDF_SHA256, + HKDF_SHA256, HPKE_AES_128_GCM, NULL); + if (hpkeRet != 0) { + ret = WOLFCOSE_E_CRYPTO; + } + } + if (ret == WOLFCOSE_SUCCESS) { + recipientEcc = recipientKey->key.ecc; + hpkeRet = wc_HpkeOpenBase(&hpke, recipientEcc, + enc, (word16)encLen, + info, (word32)infoLen, + aad, (word32)aadLen, + (byte*)ciphertext, (word32)ciphertextBodyLen, plaintext); + if (hpkeRet != 0) { + ret = WOLFCOSE_E_COSE_DECRYPT_FAIL; + } + } + + (void)wolfCose_ForceZero(&hpke, sizeof(hpke)); + + return ret; +} +#endif /* WOLFCOSE_HPKE_0_DECRYPT || WOLFCOSE_HPKE_0_KE_DECRYPT */ + +#endif /* WOLFCOSE_HAVE_HPKE_0 */ diff --git a/tests/config/hpke_prereqs/user_settings.h b/tests/config/hpke_prereqs/user_settings.h new file mode 100644 index 0000000..cea102e --- /dev/null +++ b/tests/config/hpke_prereqs/user_settings.h @@ -0,0 +1,20 @@ +/* Minimal synthetic wolfSSL feature set for HPKE prerequisite gate tests. */ +#ifndef WOLFCOSE_TEST_HPKE_USER_SETTINGS_H +#define WOLFCOSE_TEST_HPKE_USER_SETTINGS_H + +#define HAVE_HPKE +#define HAVE_ECC +#define HAVE_AESGCM +#define WOLFSSL_AES_128 + +#if !defined(WOLFCOSE_TEST_NO_HKDF) + #define HAVE_HKDF +#endif + +#if defined(WOLFCOSE_TEST_NO_ECC_DHE) + #define NO_ECC_DHE +#else + #define HAVE_ECC_DHE +#endif + +#endif /* WOLFCOSE_TEST_HPKE_USER_SETTINGS_H */ diff --git a/tests/misra_consumer.c b/tests/misra_consumer.c index bf83383..a5c4a7a 100644 --- a/tests/misra_consumer.c +++ b/tests/misra_consumer.c @@ -100,6 +100,12 @@ static size_t wolfCose_MisraUsePublicApis(void) #ifdef WOLFCOSE_ENCRYPT0_DECRYPT WOLFCOSE_MISRA_USE_API(wc_CoseEncrypt0_Decrypt); #endif +#ifdef WOLFCOSE_HPKE_0_ENCRYPT + WOLFCOSE_MISRA_USE_API(wc_CoseHpkeEncrypt0_Encrypt); +#endif +#ifdef WOLFCOSE_HPKE_0_DECRYPT + WOLFCOSE_MISRA_USE_API(wc_CoseHpkeEncrypt0_Decrypt); +#endif #ifdef WOLFCOSE_MAC0_CREATE WOLFCOSE_MISRA_USE_API(wc_CoseMac0_Create); #endif @@ -146,6 +152,11 @@ int main(void) WOLFCOSE_ALG_ECDH_ES_A192KW, WOLFCOSE_ALG_ECDH_ES_A256KW, (int32_t)WOLFCOSE_TAG_COUNTERSIGNATURE, +#ifdef WOLFCOSE_HAVE_HPKE_0 + WOLFCOSE_HDR_HPKE_EK, + WOLFCOSE_ALG_HPKE_0, + WOLFCOSE_ALG_HPKE_0_KE, +#endif WOLFCOSE_CRV_ML_DSA_44, WOLFCOSE_CRV_ML_DSA_65, WOLFCOSE_CRV_ML_DSA_87, diff --git a/tests/test_cose.c b/tests/test_cose.c index 34452e8..553c684 100644 --- a/tests/test_cose.c +++ b/tests/test_cose.c @@ -42,9 +42,12 @@ #include "../src/wolfcose_internal.h" /* For testing internal helpers */ #include "test_suite.h" #include -#ifdef WOLFCOSE_HAVE_ES256 +#if defined(WOLFCOSE_HAVE_ES256) || defined(WOLFCOSE_HAVE_HPKE_0) #include #endif +#if defined(WOLFCOSE_HAVE_HPKE_0) + #include +#endif #ifdef WOLFCOSE_HAVE_EDDSA #include #endif @@ -67,6 +70,7 @@ #ifdef WOLFCOSE_TEST_LOG_ENABLE #include #endif +#include #include #ifdef WOLFCOSE_FORCE_FAILURE #include "force_failure.h" @@ -315,6 +319,46 @@ static void test_cose_countersign_ecdsa_curves(void) } #endif +/* This replica is the public WOLFCOSE_HDR layout before COSE-HPKE was added. + * Keep the compatibility check in the default test build so a feature-gated + * addition cannot silently grow the ABI-visible structure. */ +typedef struct WOLFCOSE_HDR_PRE_HPKE { + int32_t alg; + const uint8_t* kid; + size_t kidLen; + const uint8_t* iv; + size_t ivLen; + const uint8_t* partialIv; + size_t partialIvLen; + int32_t contentType; + uint8_t flags; +} WOLFCOSE_HDR_PRE_HPKE; + +static void test_cose_hdr_abi_layout(void) +{ + TEST_LOG(" [WOLFCOSE_HDR ABI layout]\n"); + TEST_ASSERT(sizeof(WOLFCOSE_HDR) == sizeof(WOLFCOSE_HDR_PRE_HPKE), + "hpke preserves WOLFCOSE_HDR size"); + TEST_ASSERT(offsetof(WOLFCOSE_HDR, alg) == + offsetof(WOLFCOSE_HDR_PRE_HPKE, alg), + "hpke preserves WOLFCOSE_HDR alg offset"); + TEST_ASSERT(offsetof(WOLFCOSE_HDR, kid) == + offsetof(WOLFCOSE_HDR_PRE_HPKE, kid), + "hpke preserves WOLFCOSE_HDR kid offset"); + TEST_ASSERT(offsetof(WOLFCOSE_HDR, iv) == + offsetof(WOLFCOSE_HDR_PRE_HPKE, iv), + "hpke preserves WOLFCOSE_HDR iv offset"); + TEST_ASSERT(offsetof(WOLFCOSE_HDR, partialIv) == + offsetof(WOLFCOSE_HDR_PRE_HPKE, partialIv), + "hpke preserves WOLFCOSE_HDR partial IV offset"); + TEST_ASSERT(offsetof(WOLFCOSE_HDR, contentType) == + offsetof(WOLFCOSE_HDR_PRE_HPKE, contentType), + "hpke preserves WOLFCOSE_HDR content type offset"); + TEST_ASSERT(offsetof(WOLFCOSE_HDR, flags) == + offsetof(WOLFCOSE_HDR_PRE_HPKE, flags), + "hpke preserves WOLFCOSE_HDR flags offset"); +} + #if (defined(WOLFCOSE_MAC) && defined(WOLFCOSE_HAVE_HMAC256)) || \ (defined(WOLFCOSE_ENCRYPT) && defined(WOLFCOSE_HAVE_AESGCM)) /* Locate the one-byte direct-alg value in a generated recipient header. */ @@ -414,6 +458,61 @@ static int find_recipient_direct_alg(const uint8_t* msg, size_t msgLen, } #endif +#ifdef WOLFCOSE_HAVE_HPKE_0 +/* Decode fixed, public draft-vector literals without adding a wolfSSL + * Base16 feature requirement to HPKE test builds. */ +static int test_cose_hex_digit(uint8_t hex, uint8_t* digit) +{ + int ret = WOLFCOSE_SUCCESS; + + if (digit == NULL) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else if ((hex >= (uint8_t)'0') && (hex <= (uint8_t)'9')) { + *digit = (uint8_t)(hex - (uint8_t)'0'); + } + else if ((hex >= (uint8_t)'a') && (hex <= (uint8_t)'f')) { + *digit = (uint8_t)(hex - (uint8_t)'a' + 10u); + } + else if ((hex >= (uint8_t)'A') && (hex <= (uint8_t)'F')) { + *digit = (uint8_t)(hex - (uint8_t)'A' + 10u); + } + else { + ret = WOLFCOSE_E_INVALID_ARG; + } + + return ret; +} + +static int test_cose_hex_decode(const uint8_t* hex, size_t hexLen, + uint8_t* out, size_t outSz, size_t* outLen) +{ + size_t i; + uint8_t high = 0u; + uint8_t low = 0u; + int ret = WOLFCOSE_SUCCESS; + + if ((hex == NULL) || (out == NULL) || (outLen == NULL) || + ((hexLen & 1u) != 0u) || (outSz < (hexLen / 2u))) { + ret = WOLFCOSE_E_INVALID_ARG; + } + for (i = 0u; (ret == WOLFCOSE_SUCCESS) && (i < hexLen); i += 2u) { + ret = test_cose_hex_digit(hex[i], &high); + if (ret == WOLFCOSE_SUCCESS) { + ret = test_cose_hex_digit(hex[i + 1u], &low); + } + if (ret == WOLFCOSE_SUCCESS) { + out[i / 2u] = (uint8_t)((high << 4) | low); + } + } + if (ret == WOLFCOSE_SUCCESS) { + *outLen = hexLen / 2u; + } + + return ret; +} +#endif /* WOLFCOSE_HAVE_HPKE_0 */ + /* ----- Internal helper tests ----- */ static void test_wolfcose_force_zero(void) { @@ -10749,204 +10848,1829 @@ static int mutate_first_recipient_alg(uint8_t* msg, size_t msgLen, size_t protectedLen = 0; int64_t label = 0; - (void)XMEMSET(&ctx, 0, sizeof(ctx)); - ctx.cbuf = msg; - ctx.bufSz = msgLen; + (void)XMEMSET(&ctx, 0, sizeof(ctx)); + ctx.cbuf = msg; + ctx.bufSz = msgLen; + + if ((ctx.idx < ctx.bufSz) && + (wc_CBOR_PeekType(&ctx) == WOLFCOSE_CBOR_TAG)) { + ret = wc_CBOR_DecodeTag(&ctx, &tagVal); + } + else { + ret = 0; + } + + if (ret == 0) { + ret = wc_CBOR_DecodeArrayStart(&ctx, &count); + if ((ret != 0) || (count != 4u)) { + ret = -1; + } + } + if (ret == 0) { + ret = wc_CBOR_Skip(&ctx); + if (ret == 0) { + ret = wc_CBOR_Skip(&ctx); + } + if (ret == 0) { + ret = wc_CBOR_Skip(&ctx); + } + if (ret != 0) { + ret = -1; + } + } + if (ret == 0) { + ret = wc_CBOR_DecodeArrayStart(&ctx, &count); + if ((ret != 0) || (count < 1u)) { + ret = -1; + } + } + if (ret == 0) { + ret = wc_CBOR_DecodeArrayStart(&ctx, &count); + if ((ret != 0) || (count != 3u)) { + ret = -1; + } + } + /* RFC 9053 6.2.1: the AES Key Wrap recipient has an empty protected + * bucket, so the algorithm is carried in the unprotected header. */ + if (ret == 0) { + ret = wc_CBOR_DecodeBstr(&ctx, &protectedData, &protectedLen); + if ((ret != 0) || (protectedLen != 0u)) { + ret = -1; + } + } + if (ret == 0) { + ret = wc_CBOR_DecodeMapStart(&ctx, &count); + if ((ret != 0) || (count < 1u)) { + ret = -1; + } + } + if (ret == 0) { + ret = wc_CBOR_DecodeInt(&ctx, &label); + if ((ret != 0) || (label != WOLFCOSE_HDR_ALG)) { + ret = -1; + } + } + if (ret == 0) { + (void)protectedData; + if (ctx.idx < ctx.bufSz) { + msg[ctx.idx] = algByte; + } + else { + ret = -1; + } + } + + return ret; +} + +static void test_cose_encrypt_multi_recipient(void) +{ + WOLFCOSE_KEY key1, key2; + WOLFCOSE_RECIPIENT recipients[2]; + WOLFCOSE_HDR hdr; + int ret; + uint8_t out[512]; + size_t outLen; + uint8_t scratch[256]; + uint8_t plaintext[256]; + size_t plaintextLen; + const uint8_t payload[] = "Multi-recipient encryption test"; + const uint8_t iv[12] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C}; + const uint8_t keyData[16] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F}; + const uint8_t wrongKeyData[16] = {0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8, + 0xF7, 0xF6, 0xF5, 0xF4, 0xF3, 0xF2, 0xF1, 0xF0}; + const uint8_t kid1[] = "recipient-1"; + const uint8_t kid2[] = "recipient-2"; + + TEST_LOG(" [Encrypt Multi-Recipient A128GCM]\n"); + + /* Setup keys - both recipients use the same shared key in direct mode */ + (void)wc_CoseKey_Init(&key1); + ret = wc_CoseKey_SetSymmetric(&key1, keyData, sizeof(keyData)); + TEST_ASSERT(ret == 0, "encrypt key1 set"); + + (void)wc_CoseKey_Init(&key2); + ret = wc_CoseKey_SetSymmetric(&key2, keyData, sizeof(keyData)); + TEST_ASSERT(ret == 0, "encrypt key2 set"); + + /* Setup recipients */ + recipients[0].algId = WOLFCOSE_ALG_DIRECT; /* Direct key */ + recipients[0].key = &key1; + recipients[0].kid = kid1; + recipients[0].kidLen = sizeof(kid1) - 1; + + recipients[1].algId = WOLFCOSE_ALG_DIRECT; /* Direct key */ + recipients[1].key = &key2; + recipients[1].kid = kid2; + recipients[1].kidLen = sizeof(kid2) - 1; + + /* Encrypt with two recipients */ + ret = wc_CoseEncrypt_Encrypt(recipients, 2, + WOLFCOSE_ALG_A128GCM, + iv, sizeof(iv), + payload, sizeof(payload) - 1, + NULL, 0, /* no detached payload */ + NULL, 0, /* no external AAD */ + scratch, sizeof(scratch), + out, sizeof(out), &outLen, + NULL); + TEST_ASSERT(ret == 0, "encrypt multi create"); + + /* Decrypt with first recipient */ + memset(&hdr, 0, sizeof(hdr)); + ret = wc_CoseEncrypt_Decrypt(&recipients[0], 0, + out, outLen, + NULL, 0, /* no detached ciphertext */ + NULL, 0, /* no external AAD */ + scratch, sizeof(scratch), + &hdr, + plaintext, sizeof(plaintext), &plaintextLen); + TEST_ASSERT(ret == 0, "encrypt decrypt recipient 0"); + TEST_ASSERT(plaintextLen == sizeof(payload) - 1, "encrypt payload len 0"); + TEST_ASSERT(memcmp(plaintext, payload, plaintextLen) == 0, "encrypt payload match 0"); + + /* Decrypt with second recipient */ + memset(&hdr, 0, sizeof(hdr)); + memset(plaintext, 0, sizeof(plaintext)); + ret = wc_CoseEncrypt_Decrypt(&recipients[1], 1, + out, outLen, + NULL, 0, + NULL, 0, + scratch, sizeof(scratch), + &hdr, + plaintext, sizeof(plaintext), &plaintextLen); + TEST_ASSERT(ret == 0, "encrypt decrypt recipient 1"); + TEST_ASSERT(plaintextLen == sizeof(payload) - 1, "encrypt payload len 1"); + TEST_ASSERT(memcmp(plaintext, payload, plaintextLen) == 0, "encrypt payload match 1"); + + /* Verify headers */ + TEST_ASSERT(hdr.alg == WOLFCOSE_ALG_A128GCM, "encrypt hdr alg"); + TEST_ASSERT(hdr.ivLen == sizeof(iv), "encrypt hdr iv len"); + TEST_ASSERT(memcmp(hdr.iv, iv, sizeof(iv)) == 0, "encrypt hdr iv match"); + + /* Wrong key should fail */ + /* empty-brace-scan: allow - test-local temporary scope */ + { + WOLFCOSE_KEY wrongKey; + WOLFCOSE_RECIPIENT wrongRecipient; + (void)wc_CoseKey_Init(&wrongKey); + (void)wc_CoseKey_SetSymmetric(&wrongKey, wrongKeyData, sizeof(wrongKeyData)); + wrongRecipient.algId = 0; + wrongRecipient.key = &wrongKey; + wrongRecipient.kid = NULL; + wrongRecipient.kidLen = 0; + + ret = wc_CoseEncrypt_Decrypt(&wrongRecipient, 0, + out, outLen, + NULL, 0, + NULL, 0, + scratch, sizeof(scratch), + &hdr, + plaintext, sizeof(plaintext), &plaintextLen); + TEST_ASSERT(ret == WOLFCOSE_E_COSE_DECRYPT_FAIL, "encrypt wrong key fails"); + wc_CoseKey_Free(&wrongKey); + } + + /* Invalid recipient index should fail */ + ret = wc_CoseEncrypt_Decrypt(&recipients[0], 5, + out, outLen, + NULL, 0, + NULL, 0, + scratch, sizeof(scratch), + &hdr, + plaintext, sizeof(plaintext), &plaintextLen); + TEST_ASSERT(ret != 0, "encrypt invalid recipient index fails"); + + wc_CoseKey_Free(&key1); + wc_CoseKey_Free(&key2); +} + +#endif /* WOLFCOSE_ENCRYPT && WOLFCOSE_HAVE_AESGCM && WOLFCOSE_KEY_WRAP */ + +#if (defined(WOLFCOSE_HPKE_0_ENCRYPT) && \ + defined(WOLFCOSE_HPKE_0_DECRYPT)) || \ + (defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) && \ + defined(WOLFCOSE_HPKE_0_KE_DECRYPT)) +static int test_cose_hpke_ciphertext_len(const uint8_t* encoded, + size_t encodedLen, uint64_t expectedTag, size_t expectedItems, + size_t* ciphertextLen) +{ + WOLFCOSE_CBOR_CTX ctx; + uint64_t tag = 0u; + size_t items = 0u; + const uint8_t* ciphertext = NULL; + int ret; + + if ((encoded == NULL) || (ciphertextLen == NULL)) { + return WOLFCOSE_E_INVALID_ARG; + } + (void)XMEMSET(&ctx, 0, sizeof(ctx)); + ctx.cbuf = encoded; + ctx.bufSz = encodedLen; + ret = wc_CBOR_DecodeTag(&ctx, &tag); + if ((ret == WOLFCOSE_SUCCESS) && (tag != expectedTag)) { + ret = WOLFCOSE_E_COSE_BAD_HDR; + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_DecodeArrayStart(&ctx, &items); + } + if ((ret == WOLFCOSE_SUCCESS) && (items != expectedItems)) { + ret = WOLFCOSE_E_COSE_BAD_HDR; + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_Skip(&ctx); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_Skip(&ctx); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_DecodeBstr(&ctx, &ciphertext, ciphertextLen); + } + return ret; +} +#endif + +#if defined(WOLFCOSE_HPKE_0_ENCRYPT) && defined(WOLFCOSE_HPKE_0_DECRYPT) +static void test_cose_hpke_key_validation(const WOLFCOSE_KEY* validKey) +{ + WOLFCOSE_KEY invalidKey; + ecc_key incompleteEcc; + int incompleteEccInited = 0; + int ret; + + invalidKey = *validKey; + invalidKey.attachedType = WOLFCOSE_ATT_NONE; + ret = wolfCose_Hpke0ValidateKey(&invalidKey, WOLFCOSE_ALG_HPKE_0, 0); + TEST_ASSERT(ret == WOLFCOSE_E_COSE_KEY_TYPE, + "hpke encrypt key attachment type rejected"); + invalidKey.hasPrivate = 1u; + ret = wolfCose_Hpke0ValidateKey(&invalidKey, WOLFCOSE_ALG_HPKE_0, 1); + TEST_ASSERT(ret == WOLFCOSE_E_COSE_KEY_TYPE, + "hpke decrypt key attachment type rejected"); + + (void)XMEMSET(&incompleteEcc, 0, sizeof(incompleteEcc)); + ret = wc_ecc_init(&incompleteEcc); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke incomplete key initializes"); + if (ret == WOLFCOSE_SUCCESS) { + incompleteEccInited = 1; + ret = wc_ecc_set_curve(&incompleteEcc, 32, ECC_SECP256R1); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke incomplete key selects P-256"); + } + if (ret == WOLFCOSE_SUCCESS) { + invalidKey = *validKey; + invalidKey.key.ecc = &incompleteEcc; + ret = wolfCose_Hpke0ValidateKey(&invalidKey, + WOLFCOSE_ALG_HPKE_0, 0); + TEST_ASSERT(ret == WOLFCOSE_E_COSE_KEY_TYPE, + "hpke incomplete public key rejected"); + invalidKey.hasPrivate = 1u; + ret = wolfCose_Hpke0ValidateKey(&invalidKey, + WOLFCOSE_ALG_HPKE_0, 1); + TEST_ASSERT(ret == WOLFCOSE_E_COSE_KEY_TYPE, + "hpke incomplete private key rejected"); + } + if (incompleteEccInited != 0) { + (void)wc_ecc_free(&incompleteEcc); + } +} + +static void test_cose_hpke_encrypt0(void) +{ + WOLFCOSE_KEY recipientKey; + WOLFCOSE_KEY wrongKey; +#if defined(HAVE_ECC_KOBLITZ) + WOLFCOSE_KEY nonP256Key; +#endif + WOLFCOSE_HDR hdr; + ecc_key recipientEcc; + ecc_key wrongEcc; +#if defined(HAVE_ECC_KOBLITZ) + ecc_key nonP256Ecc; +#endif + WC_RNG rng; + int ret = WOLFCOSE_SUCCESS; + int rngInited = 0; + int recipientEccInited = 0; + int wrongEccInited = 0; +#if defined(HAVE_ECC_KOBLITZ) + int nonP256EccInited = 0; +#endif + int recipientKeyInited = 0; + int wrongKeyInited = 0; +#if defined(HAVE_ECC_KOBLITZ) + int nonP256KeyInited = 0; +#endif + uint8_t out[512]; + uint8_t detached[128]; + uint8_t plaintext[128]; + uint8_t scratch[256]; + size_t outLen = 0u; + size_t detachedLen = 0u; + size_t plaintextLen = 0u; + size_t ciphertextLen = 0u; + const uint8_t kid[] = "hpke-recipient"; + const uint8_t payload[] = "COSE HPKE integrated encryption"; + const uint8_t emptyPayload[] = { 0u }; + const uint8_t aad[] = "hpke external aad"; + const uint8_t wrongAad[] = "wrong hpke external aad"; + + TEST_LOG(" [COSE HPKE-0 Encrypt0]\n"); + (void)XMEMSET(&recipientEcc, 0, sizeof(recipientEcc)); + (void)XMEMSET(&wrongEcc, 0, sizeof(wrongEcc)); +#if defined(HAVE_ECC_KOBLITZ) + (void)XMEMSET(&nonP256Ecc, 0, sizeof(nonP256Ecc)); +#endif + + ret = wc_InitRng(&rng); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke encrypt0 rng init"); + if (ret == WOLFCOSE_SUCCESS) { + rngInited = 1; + ret = wc_ecc_init(&recipientEcc); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke encrypt0 recipient ecc init"); + } + if (ret == WOLFCOSE_SUCCESS) { + recipientEccInited = 1; + ret = wc_ecc_make_key(&rng, 32, &recipientEcc); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke encrypt0 recipient key make"); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_ecc_init(&wrongEcc); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke encrypt0 wrong ecc init"); + } + if (ret == WOLFCOSE_SUCCESS) { + wrongEccInited = 1; + ret = wc_ecc_make_key(&rng, 32, &wrongEcc); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke encrypt0 wrong key make"); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CoseKey_Init(&recipientKey); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke encrypt0 recipient key init"); + } + if (ret == WOLFCOSE_SUCCESS) { + recipientKeyInited = 1; + ret = wc_CoseKey_SetEcc(&recipientKey, WOLFCOSE_CRV_P256, + &recipientEcc); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke encrypt0 recipient key set"); + } + if (ret == WOLFCOSE_SUCCESS) { + recipientKey.alg = WOLFCOSE_ALG_HPKE_0; + recipientKey.hasPrivate = 0u; + test_cose_hpke_key_validation(&recipientKey); + ret = wc_CoseKey_Init(&wrongKey); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke encrypt0 wrong key init"); + } + if (ret == WOLFCOSE_SUCCESS) { + wrongKeyInited = 1; + ret = wc_CoseKey_SetEcc(&wrongKey, WOLFCOSE_CRV_P256, &wrongEcc); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke encrypt0 wrong key set"); + } + if (ret == WOLFCOSE_SUCCESS) { + wrongKey.alg = WOLFCOSE_ALG_HPKE_0; + } +#if defined(HAVE_ECC_KOBLITZ) + if (rngInited != 0) { + int curveRet; + size_t curveOutLen = sizeof(out); + + curveRet = wc_ecc_init(&nonP256Ecc); + TEST_ASSERT(curveRet == WOLFCOSE_SUCCESS, + "hpke encrypt0 non-P256 ecc init"); + if (curveRet == WOLFCOSE_SUCCESS) { + nonP256EccInited = 1; + curveRet = wc_ecc_make_key_ex(&rng, 32, &nonP256Ecc, + ECC_SECP256K1); + TEST_ASSERT(curveRet == WOLFCOSE_SUCCESS, + "hpke encrypt0 non-P256 key make"); + } + if (curveRet == WOLFCOSE_SUCCESS) { + curveRet = wc_CoseKey_Init(&nonP256Key); + TEST_ASSERT(curveRet == WOLFCOSE_SUCCESS, + "hpke encrypt0 non-P256 COSE key init"); + } + if (curveRet == WOLFCOSE_SUCCESS) { + nonP256KeyInited = 1; + /* Deliberately bypass SetEcc: this verifies the HPKE boundary + * when caller metadata claims P-256 for a 32-byte Koblitz key. */ + nonP256Key.kty = WOLFCOSE_KTY_EC2; + nonP256Key.crv = WOLFCOSE_CRV_P256; + nonP256Key.attachedType = WOLFCOSE_ATT_ECC; + nonP256Key.key.ecc = &nonP256Ecc; + nonP256Key.hasPrivate = 0u; + nonP256Key.alg = WOLFCOSE_ALG_HPKE_0; + curveRet = wc_CoseHpkeEncrypt0_Encrypt(&nonP256Key, kid, + sizeof(kid) - 1u, payload, sizeof(payload) - 1u, + NULL, 0u, NULL, aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), out, sizeof(out), &curveOutLen, + &rng); + TEST_ASSERT(curveRet == WOLFCOSE_E_COSE_KEY_TYPE, + "hpke encrypt0 non-P256 key rejected"); + TEST_ASSERT(curveOutLen == 0u, + "hpke encrypt0 non-P256 output length cleared"); + } + } +#endif + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CoseHpkeEncrypt0_Encrypt(&recipientKey, kid, + sizeof(kid) - 1u, payload, sizeof(payload) - 1u, + NULL, 0u, NULL, aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), out, sizeof(out), &outLen, &rng); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke encrypt0 encrypt"); + } + if (ret == WOLFCOSE_SUCCESS) { + uint8_t tinyOut[1]; + size_t failedOutLen = sizeof(out); + int failedRet; + + (void)XMEMSET(tinyOut, 0xa5, sizeof(tinyOut)); + failedRet = wc_CoseHpkeEncrypt0_Encrypt(&recipientKey, kid, + sizeof(kid) - 1u, payload, sizeof(payload) - 1u, + NULL, 0u, NULL, aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), tinyOut, sizeof(tinyOut), + &failedOutLen, &rng); + TEST_ASSERT(failedRet != WOLFCOSE_SUCCESS, + "hpke encrypt0 undersized output rejected"); + TEST_ASSERT((failedOutLen == 0u) && (tinyOut[0] == 0u), + "hpke encrypt0 failed out length cleared"); + } + if (ret == WOLFCOSE_SUCCESS) { + recipientKey.hasPrivate = 1u; + (void)XMEMSET(&hdr, 0, sizeof(hdr)); + ret = wc_CoseHpkeEncrypt0_Decrypt(&recipientKey, out, outLen, + NULL, 0u, aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), &hdr, + plaintext, sizeof(plaintext), &plaintextLen); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke encrypt0 decrypt"); + TEST_ASSERT(plaintextLen == (sizeof(payload) - 1u), + "hpke encrypt0 payload length"); + TEST_ASSERT(memcmp(plaintext, payload, plaintextLen) == 0, + "hpke encrypt0 payload matches"); + TEST_ASSERT(hdr.alg == WOLFCOSE_ALG_HPKE_0, + "hpke encrypt0 protected algorithm"); + TEST_ASSERT((hdr.kidLen == (sizeof(kid) - 1u)) && + (memcmp(hdr.kid, kid, hdr.kidLen) == 0), + "hpke encrypt0 kid decoded"); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CoseHpkeEncrypt0_Decrypt(&recipientKey, out, outLen, + NULL, 0u, wrongAad, sizeof(wrongAad) - 1u, + scratch, sizeof(scratch), &hdr, + plaintext, sizeof(plaintext), &plaintextLen); + TEST_ASSERT(ret == WOLFCOSE_E_COSE_DECRYPT_FAIL, + "hpke encrypt0 wrong aad rejected"); + } + if ((outLen > 0u) && (recipientKey.hasPrivate == 1u)) { + out[outLen - 1u] ^= 0x01u; + ret = wc_CoseHpkeEncrypt0_Decrypt(&recipientKey, out, outLen, + NULL, 0u, aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), &hdr, + plaintext, sizeof(plaintext), &plaintextLen); + TEST_ASSERT(ret == WOLFCOSE_E_COSE_DECRYPT_FAIL, + "hpke encrypt0 tampered ciphertext rejected"); + out[outLen - 1u] ^= 0x01u; + } + if ((outLen > 0u) && (wrongKeyInited != 0)) { + ret = wc_CoseHpkeEncrypt0_Decrypt(&wrongKey, out, outLen, + NULL, 0u, aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), &hdr, + plaintext, sizeof(plaintext), &plaintextLen); + TEST_ASSERT(ret == WOLFCOSE_E_COSE_DECRYPT_FAIL, + "hpke encrypt0 wrong private key rejected"); + } + if (recipientKeyInited != 0) { + recipientKey.hasPrivate = 0u; + ret = wc_CoseHpkeEncrypt0_Encrypt(&recipientKey, kid, + sizeof(kid) - 1u, payload, sizeof(payload) - 1u, + detached, sizeof(detached), &detachedLen, + aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), out, sizeof(out), &outLen, &rng); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke encrypt0 detached encrypt"); + if (ret == WOLFCOSE_SUCCESS) { + recipientKey.hasPrivate = 1u; + (void)XMEMSET(&hdr, 0, sizeof(hdr)); + ret = wc_CoseHpkeEncrypt0_Decrypt(&recipientKey, out, outLen, + detached, detachedLen, aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), &hdr, + plaintext, sizeof(plaintext), &plaintextLen); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke encrypt0 detached decrypt"); + TEST_ASSERT((hdr.flags & WOLFCOSE_HDR_FLAG_DETACHED) != 0u, + "hpke encrypt0 detached header flag"); + TEST_ASSERT((plaintextLen == (sizeof(payload) - 1u)) && + (memcmp(plaintext, payload, plaintextLen) == 0), + "hpke encrypt0 detached payload matches"); + } + } + if (recipientKeyInited != 0) { + recipientKey.hasPrivate = 0u; + ret = wc_CoseHpkeEncrypt0_Encrypt(&recipientKey, kid, + sizeof(kid) - 1u, emptyPayload, 0u, + NULL, 0u, NULL, aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), out, sizeof(out), &outLen, &rng); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke encrypt0 empty attached encrypt"); + if (ret == WOLFCOSE_SUCCESS) { + ret = test_cose_hpke_ciphertext_len(out, outLen, + WOLFCOSE_TAG_ENCRYPT0, 3u, &ciphertextLen); + TEST_ASSERT((ret == WOLFCOSE_SUCCESS) && (ciphertextLen == 16u), + "hpke encrypt0 empty attached tag length"); + recipientKey.hasPrivate = 1u; + (void)XMEMSET(&hdr, 0, sizeof(hdr)); + ret = wc_CoseHpkeEncrypt0_Decrypt(&recipientKey, out, outLen, + NULL, 0u, aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), &hdr, + plaintext, sizeof(plaintext), &plaintextLen); + TEST_ASSERT((ret == WOLFCOSE_SUCCESS) && (plaintextLen == 0u), + "hpke encrypt0 empty attached decrypt"); + } + } + if (recipientKeyInited != 0) { + recipientKey.hasPrivate = 0u; + ret = wc_CoseHpkeEncrypt0_Encrypt(&recipientKey, kid, + sizeof(kid) - 1u, emptyPayload, 0u, + detached, sizeof(detached), &detachedLen, + aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), out, sizeof(out), &outLen, &rng); + TEST_ASSERT((ret == WOLFCOSE_SUCCESS) && (detachedLen == 16u), + "hpke encrypt0 empty detached tag length"); + if (ret == WOLFCOSE_SUCCESS) { + recipientKey.hasPrivate = 1u; + (void)XMEMSET(&hdr, 0, sizeof(hdr)); + ret = wc_CoseHpkeEncrypt0_Decrypt(&recipientKey, out, outLen, + detached, detachedLen, aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), &hdr, + plaintext, sizeof(plaintext), &plaintextLen); + TEST_ASSERT((ret == WOLFCOSE_SUCCESS) && (plaintextLen == 0u), + "hpke encrypt0 empty detached decrypt"); + } + } + + if (wrongKeyInited != 0) { + wc_CoseKey_Free(&wrongKey); + } +#if defined(HAVE_ECC_KOBLITZ) + if (nonP256KeyInited != 0) { + wc_CoseKey_Free(&nonP256Key); + } +#endif + if (recipientKeyInited != 0) { + wc_CoseKey_Free(&recipientKey); + } + if (wrongEccInited != 0) { + (void)wc_ecc_free(&wrongEcc); + } +#if defined(HAVE_ECC_KOBLITZ) + if (nonP256EccInited != 0) { + (void)wc_ecc_free(&nonP256Ecc); + } +#endif + if (recipientEccInited != 0) { + (void)wc_ecc_free(&recipientEcc); + } + if (rngInited != 0) { + (void)wc_FreeRng(&rng); + } +} +#endif /* WOLFCOSE_HPKE_0_ENCRYPT && WOLFCOSE_HPKE_0_DECRYPT */ + +#if defined(WOLFCOSE_HPKE_0_ENCRYPT) && defined(WOLFCOSE_HPKE_0_DECRYPT) +/* draft-ietf-cose-hpke-26 permits an integrated HPKE `alg` to be absent. + * Build a valid base-mode message directly with wolfCrypt so this test covers + * the omitted-header wire form, not merely a parser mutation. */ +static void test_cose_hpke_encrypt0_optional_alg(void) +{ + static const uint8_t encContext[] = "Encrypt0"; + static const uint8_t payload[] = "HPKE without a protected algorithm"; + WOLFCOSE_KEY recipientKey; + WOLFCOSE_HDR hdr; + WC_RNG rng; + ecc_key recipientEcc; + ecc_key ephemeralEcc; + Hpke hpke; + WOLFCOSE_CBOR_CTX ctx; + uint8_t emptyHdr = 0u; + uint8_t scratch[256]; + uint8_t enc[65]; + uint8_t ciphertext[sizeof(payload) - 1u + 16u]; + uint8_t encoded[256]; + uint8_t plaintext[sizeof(payload)]; + size_t encStructLen = 0u; + size_t encodedLen = 0u; + word16 encLen = (word16)sizeof(enc); + int ret = WOLFCOSE_SUCCESS; + int rngInited = 0; + int recipientEccInited = 0; + int ephemeralEccInited = 0; + int keyInited = 0; + enum { + HPKE_HDR_CASE_UNPROTECTED_ALG = 0x01u, + HPKE_HDR_CASE_NO_EK = 0x02u, + HPKE_HDR_CASE_SHORT_EK = 0x04u, + HPKE_HDR_CASE_DUPLICATE_EK = 0x08u, + HPKE_HDR_CASE_NON_BSTR_EK = 0x10u, + HPKE_HDR_CASE_PSK_ID = 0x20u, + HPKE_HDR_CASE_PROTECTED_EK = 0x40u, + HPKE_HDR_CASE_DETACHED = 0x80u + }; + typedef struct WOLFCOSE_HPKE_HDR_CASE { + uint32_t flags; + int expected; + const char* name; + } WOLFCOSE_HPKE_HDR_CASE; + static const WOLFCOSE_HPKE_HDR_CASE cases[] = { + { 0u, WOLFCOSE_SUCCESS, "hpke optional protected alg accepted" }, + { HPKE_HDR_CASE_UNPROTECTED_ALG, WOLFCOSE_E_COSE_BAD_HDR, + "hpke unprotected alg rejected" }, + { HPKE_HDR_CASE_NO_EK, WOLFCOSE_E_COSE_BAD_HDR, + "hpke missing ek rejected" }, + { HPKE_HDR_CASE_SHORT_EK, WOLFCOSE_E_COSE_BAD_HDR, + "hpke short ek rejected" }, + { HPKE_HDR_CASE_DUPLICATE_EK, WOLFCOSE_E_CBOR_MALFORMED, + "hpke duplicate ek rejected" }, + { HPKE_HDR_CASE_NON_BSTR_EK, WOLFCOSE_E_CBOR_TYPE, + "hpke non-bstr ek rejected" }, + { HPKE_HDR_CASE_PSK_ID, WOLFCOSE_E_COSE_BAD_HDR, + "hpke psk id rejected" }, + { HPKE_HDR_CASE_NO_EK | HPKE_HDR_CASE_PROTECTED_EK, + WOLFCOSE_E_COSE_BAD_HDR, "hpke protected ek rejected" }, + { HPKE_HDR_CASE_DETACHED, WOLFCOSE_E_DETACHED_PAYLOAD, + "hpke detached ciphertext required" } + }; + static const uint8_t pskId[] = { 0u }; + static const uint8_t nonBstrEk[] = "not-an-enc"; + size_t caseIndex; + + TEST_LOG(" [COSE HPKE Encrypt0 optional algorithm]\n"); + (void)XMEMSET(&recipientKey, 0, sizeof(recipientKey)); + (void)XMEMSET(&recipientEcc, 0, sizeof(recipientEcc)); + (void)XMEMSET(&ephemeralEcc, 0, sizeof(ephemeralEcc)); + (void)XMEMSET(&hpke, 0, sizeof(hpke)); + + ret = wc_InitRng(&rng); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke optional alg rng init"); + if (ret == WOLFCOSE_SUCCESS) { + rngInited = 1; + ret = wc_ecc_init(&recipientEcc); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke optional alg recipient ecc init"); + } + if (ret == WOLFCOSE_SUCCESS) { + recipientEccInited = 1; + ret = wc_ecc_make_key_ex(&rng, 32, &recipientEcc, ECC_SECP256R1); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke optional alg recipient key make"); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CoseKey_Init(&recipientKey); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke optional alg cose key init"); + } + if (ret == WOLFCOSE_SUCCESS) { + keyInited = 1; + ret = wc_CoseKey_SetEcc(&recipientKey, WOLFCOSE_CRV_P256, + &recipientEcc); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke optional alg cose key set"); + recipientKey.alg = WOLFCOSE_ALG_HPKE_0; + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_HpkeInit(&hpke, DHKEM_P256_HKDF_SHA256, HKDF_SHA256, + HPKE_AES_128_GCM, NULL); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke optional alg init"); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_ecc_init(&ephemeralEcc); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke optional alg ephemeral ecc init"); + } + if (ret == WOLFCOSE_SUCCESS) { + ephemeralEccInited = 1; + ret = wc_ecc_make_key_ex(&rng, 32, &ephemeralEcc, ECC_SECP256R1); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke optional alg ephemeral key make"); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_HpkeSerializePublicKey(&hpke, &ephemeralEcc, enc, &encLen); + TEST_ASSERT((ret == WOLFCOSE_SUCCESS) && (encLen == sizeof(enc)), + "hpke optional alg encapsulated key"); + } + if (ret == WOLFCOSE_SUCCESS) { + (void)XMEMSET(&ctx, 0, sizeof(ctx)); + ctx.buf = scratch; + ctx.bufSz = sizeof(scratch); + ret = wc_CBOR_EncodeArrayStart(&ctx, 3u); + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeTstr(&ctx, encContext, + sizeof(encContext) - 1u); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeBstr(&ctx, &emptyHdr, 0u); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeBstr(&ctx, NULL, 0u); + } + if (ret == WOLFCOSE_SUCCESS) { + encStructLen = ctx.idx; + } + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke optional alg Encrypt0 structure"); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_HpkeSealBase(&hpke, &ephemeralEcc, &recipientEcc, + NULL, 0u, scratch, (word32)encStructLen, + (byte*)payload, (word32)(sizeof(payload) - 1u), ciphertext); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke optional alg seal"); + } + + for (caseIndex = 0u; + (ret == WOLFCOSE_SUCCESS) && (caseIndex < + (sizeof(cases) / sizeof(cases[0]))); + caseIndex++) { + WOLFCOSE_CBOR_CTX protectedCtx; + WOLFCOSE_HDR clearedHdr; + const uint8_t* protectedCase = &emptyHdr; + size_t protectedCaseLen = 0u; + size_t mapEntries = 0u; + uint8_t protectedHdr[96]; + uint8_t clearedPlaintext[sizeof(plaintext)]; + int decRet = WOLFCOSE_SUCCESS; + size_t plaintextLen = sizeof(plaintext); + + if ((cases[caseIndex].flags & HPKE_HDR_CASE_PROTECTED_EK) != 0u) { + (void)XMEMSET(&protectedCtx, 0, sizeof(protectedCtx)); + protectedCtx.buf = protectedHdr; + protectedCtx.bufSz = sizeof(protectedHdr); + decRet = wc_CBOR_EncodeMapStart(&protectedCtx, 1u); + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeInt(&protectedCtx, + WOLFCOSE_HDR_HPKE_EK); + } + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeBstr(&protectedCtx, enc, sizeof(enc)); + } + if (decRet == WOLFCOSE_SUCCESS) { + protectedCase = protectedHdr; + protectedCaseLen = protectedCtx.idx; + } + } + + (void)XMEMSET(&ctx, 0, sizeof(ctx)); + ctx.buf = encoded; + ctx.bufSz = sizeof(encoded); + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeTag(&ctx, WOLFCOSE_TAG_ENCRYPT0); + } + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeArrayStart(&ctx, 3u); + } + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeBstr(&ctx, protectedCase, + protectedCaseLen); + } + if ((cases[caseIndex].flags & HPKE_HDR_CASE_NO_EK) == 0u) { + mapEntries++; + } + if ((cases[caseIndex].flags & HPKE_HDR_CASE_UNPROTECTED_ALG) != 0u) { + mapEntries++; + } + if ((cases[caseIndex].flags & HPKE_HDR_CASE_DUPLICATE_EK) != 0u) { + mapEntries++; + } + if ((cases[caseIndex].flags & HPKE_HDR_CASE_PSK_ID) != 0u) { + mapEntries++; + } + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeMapStart(&ctx, mapEntries); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((cases[caseIndex].flags & HPKE_HDR_CASE_UNPROTECTED_ALG) != 0u)) { + decRet = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_HDR_ALG); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((cases[caseIndex].flags & HPKE_HDR_CASE_UNPROTECTED_ALG) != 0u)) { + decRet = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_ALG_HPKE_0); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((cases[caseIndex].flags & HPKE_HDR_CASE_NO_EK) == 0u)) { + decRet = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_HDR_HPKE_EK); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((cases[caseIndex].flags & HPKE_HDR_CASE_NO_EK) == 0u) && + ((cases[caseIndex].flags & HPKE_HDR_CASE_NON_BSTR_EK) != 0u)) { + decRet = wc_CBOR_EncodeTstr(&ctx, nonBstrEk, + sizeof(nonBstrEk) - 1u); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((cases[caseIndex].flags & HPKE_HDR_CASE_NO_EK) == 0u) && + ((cases[caseIndex].flags & HPKE_HDR_CASE_NON_BSTR_EK) == 0u)) { + size_t ekLen = sizeof(enc); + + if ((cases[caseIndex].flags & HPKE_HDR_CASE_SHORT_EK) != 0u) { + ekLen--; + } + decRet = wc_CBOR_EncodeBstr(&ctx, enc, ekLen); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((cases[caseIndex].flags & HPKE_HDR_CASE_DUPLICATE_EK) != 0u)) { + decRet = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_HDR_HPKE_EK); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((cases[caseIndex].flags & HPKE_HDR_CASE_DUPLICATE_EK) != 0u)) { + decRet = wc_CBOR_EncodeBstr(&ctx, enc, sizeof(enc)); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((cases[caseIndex].flags & HPKE_HDR_CASE_PSK_ID) != 0u)) { + decRet = wc_CBOR_EncodeInt(&ctx, -5); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((cases[caseIndex].flags & HPKE_HDR_CASE_PSK_ID) != 0u)) { + decRet = wc_CBOR_EncodeBstr(&ctx, pskId, sizeof(pskId)); + } + if (decRet == WOLFCOSE_SUCCESS) { + if ((cases[caseIndex].flags & HPKE_HDR_CASE_DETACHED) != 0u) { + decRet = wc_CBOR_EncodeNull(&ctx); + } + else { + decRet = wc_CBOR_EncodeBstr(&ctx, ciphertext, + sizeof(ciphertext)); + } + } + if (decRet == WOLFCOSE_SUCCESS) { + encodedLen = ctx.idx; + (void)XMEMSET(&hdr, 0xa5, sizeof(hdr)); + (void)XMEMSET(plaintext, 0xa5, sizeof(plaintext)); + decRet = wc_CoseHpkeEncrypt0_Decrypt(&recipientKey, + encoded, encodedLen, NULL, 0u, NULL, 0u, + scratch, sizeof(scratch), &hdr, plaintext, sizeof(plaintext), + &plaintextLen); + } + TEST_ASSERT(decRet == cases[caseIndex].expected, cases[caseIndex].name); + if (cases[caseIndex].expected == WOLFCOSE_SUCCESS) { + TEST_ASSERT(decRet == WOLFCOSE_SUCCESS, + "hpke optional protected alg accepted"); + TEST_ASSERT((hdr.alg == WOLFCOSE_ALG_UNSET) && + (plaintextLen == (sizeof(payload) - 1u)) && + (memcmp(plaintext, payload, plaintextLen) == 0), + "hpke optional protected alg payload"); + } + else { + (void)XMEMSET(&clearedHdr, 0, sizeof(clearedHdr)); + (void)XMEMSET(clearedPlaintext, 0, sizeof(clearedPlaintext)); + TEST_ASSERT((plaintextLen == 0u) && + (memcmp(&hdr, &clearedHdr, sizeof(hdr)) == 0) && + (memcmp(plaintext, clearedPlaintext, + sizeof(plaintext)) == 0), + "hpke malformed header clears outputs"); + } + } + + if (keyInited != 0) { + wc_CoseKey_Free(&recipientKey); + } + if (ephemeralEccInited != 0) { + (void)wc_ecc_free(&ephemeralEcc); + } + if (recipientEccInited != 0) { + (void)wc_ecc_free(&recipientEcc); + } + if (rngInited != 0) { + (void)wc_FreeRng(&rng); + } + (void)XMEMSET(&hpke, 0, sizeof(hpke)); +} +#endif /* WOLFCOSE_HPKE_0_ENCRYPT && WOLFCOSE_HPKE_0_DECRYPT */ + +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) && \ + defined(WOLFCOSE_HPKE_0_KE_DECRYPT) +/* The multi-recipient draft permits an omitted recipient `alg`, and its + * Recipient_structure authenticates an unprotected content algorithm. Build + * two valid empty-protected recipients so recipient index 1 also covers + * skipped-entry classification. */ +static void test_cose_hpke_key_encryption_optional_alg(void) +{ + static const uint8_t hpkeRecipientContext[] = "HPKE Recipient"; + static const uint8_t payload[] = "HPKE key encryption without alg"; + static const uint8_t keyData[16] = { + 0x00u, 0x01u, 0x02u, 0x03u, 0x04u, 0x05u, 0x06u, 0x07u, + 0x08u, 0x09u, 0x0au, 0x0bu, 0x0cu, 0x0du, 0x0eu, 0x0fu + }; + static const uint8_t iv[12] = { + 0x11u, 0x12u, 0x13u, 0x14u, 0x15u, 0x16u, + 0x17u, 0x18u, 0x19u, 0x1au, 0x1bu, 0x1cu + }; + WOLFCOSE_KEY recipientKey; + WOLFCOSE_RECIPIENT hpkeRecipient; + WOLFCOSE_HDR hdr; + WC_RNG rng; + ecc_key recipientEcc; + ecc_key ephemeralEcc; + Hpke hpke; + Aes aes; + WOLFCOSE_CBOR_CTX ctx; + uint8_t emptyHdr = 0u; + uint8_t aad[32]; + uint8_t scratch[256]; + uint8_t enc[65]; + uint8_t bodyCiphertext[sizeof(payload) - 1u + WOLFCOSE_AES_GCM_TAG_SZ]; + uint8_t wrappedCek[sizeof(keyData) + 16u + 8u]; + uint8_t encoded[1024]; + uint8_t plaintext[sizeof(payload)]; + size_t aadLen = 0u; + size_t encodedLen = 0u; + size_t recipientInfoLen = 0u; + size_t wrappedCekLen = sizeof(keyData) + 16u; + word16 encLen = (word16)sizeof(enc); + int ret = WOLFCOSE_SUCCESS; + int rngInited = 0; + int recipientEccInited = 0; + int ephemeralEccInited = 0; + int recipientKeyInited = 0; + int aesInited = 0; + enum { + HPKE_KE_HDR_CASE_UNPROTECTED_ALG = 0x01u, + HPKE_KE_HDR_CASE_NO_EK = 0x02u, + HPKE_KE_HDR_CASE_SHORT_EK = 0x04u, + HPKE_KE_HDR_CASE_DUPLICATE_EK = 0x08u, + HPKE_KE_HDR_CASE_NON_BSTR_EK = 0x10u, + HPKE_KE_HDR_CASE_PSK_ID = 0x20u, + HPKE_KE_HDR_CASE_PROTECTED_EK = 0x40u, + HPKE_KE_HDR_CASE_SKIP_ZERO_ALG = 0x80u, + HPKE_KE_HDR_CASE_SKIP_TEXT_ALG = 0x100u + }; + typedef struct WOLFCOSE_HPKE_KE_HDR_CASE { + uint32_t flags; + int expected; + const char* name; + } WOLFCOSE_HPKE_KE_HDR_CASE; + static const WOLFCOSE_HPKE_KE_HDR_CASE cases[] = { + { 0u, WOLFCOSE_SUCCESS, + "hpke ke optional recipient alg and body alg accepted" }, + { HPKE_KE_HDR_CASE_UNPROTECTED_ALG, WOLFCOSE_E_COSE_BAD_HDR, + "hpke ke unprotected alg rejected" }, + { HPKE_KE_HDR_CASE_SKIP_ZERO_ALG, WOLFCOSE_E_COSE_BAD_ALG, + "hpke ke skipped zero alg rejected" }, + { HPKE_KE_HDR_CASE_SKIP_TEXT_ALG, WOLFCOSE_E_COSE_BAD_ALG, + "hpke ke skipped text alg rejected" }, + { HPKE_KE_HDR_CASE_NO_EK, WOLFCOSE_E_COSE_BAD_HDR, + "hpke ke missing ek rejected" }, + { HPKE_KE_HDR_CASE_SHORT_EK, WOLFCOSE_E_COSE_BAD_HDR, + "hpke ke short ek rejected" }, + { HPKE_KE_HDR_CASE_DUPLICATE_EK, WOLFCOSE_E_CBOR_MALFORMED, + "hpke ke duplicate ek rejected" }, + { HPKE_KE_HDR_CASE_NON_BSTR_EK, WOLFCOSE_E_CBOR_TYPE, + "hpke ke non-bstr ek rejected" }, + { HPKE_KE_HDR_CASE_PSK_ID, WOLFCOSE_E_COSE_BAD_HDR, + "hpke ke psk id rejected" }, + { HPKE_KE_HDR_CASE_NO_EK | HPKE_KE_HDR_CASE_PROTECTED_EK, + WOLFCOSE_E_COSE_BAD_HDR, "hpke ke protected ek rejected" } + }; + static const uint8_t pskId[] = { 0u }; + static const uint8_t nonBstrEk[] = "not-an-enc"; + static const uint8_t invalidAlg[] = "invalid"; + size_t caseIndex; + size_t i; + + TEST_LOG(" [COSE HPKE key-encryption optional algorithm]\n"); + (void)XMEMSET(&recipientKey, 0, sizeof(recipientKey)); + (void)XMEMSET(&recipientEcc, 0, sizeof(recipientEcc)); + (void)XMEMSET(&ephemeralEcc, 0, sizeof(ephemeralEcc)); + (void)XMEMSET(&hpke, 0, sizeof(hpke)); + (void)XMEMSET(&aes, 0, sizeof(aes)); + + (void)XMEMSET(&ctx, 0, sizeof(ctx)); + ctx.buf = aad; + ctx.bufSz = sizeof(aad); + ret = wc_CBOR_EncodeArrayStart(&ctx, 3u); + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeTstr(&ctx, WOLFCOSE_CTX_ENCRYPT, + sizeof(WOLFCOSE_CTX_ENCRYPT)); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeBstr(&ctx, NULL, 0u); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeBstr(&ctx, NULL, 0u); + } + if (ret == WOLFCOSE_SUCCESS) { + aadLen = ctx.idx; + } + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke ke unprotected body alg Enc_structure"); + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_AesInit(&aes, NULL, INVALID_DEVID); + if (ret == 0) { + aesInited = 1; + ret = wc_AesGcmSetKey(&aes, keyData, sizeof(keyData)); + } + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_AesGcmEncrypt(&aes, bodyCiphertext, payload, + (word32)(sizeof(payload) - 1u), iv, (word32)sizeof(iv), + &bodyCiphertext[sizeof(payload) - 1u], + (word32)WOLFCOSE_AES_GCM_TAG_SZ, aad, (word32)aadLen); + } + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke ke unprotected body alg encrypt"); + if (aesInited != 0) { + (void)wc_AesFree(&aes); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_InitRng(&rng); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke optional rng init"); + } + if (ret == WOLFCOSE_SUCCESS) { + rngInited = 1; + ret = wc_ecc_init(&recipientEcc); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke ke optional recipient ecc init"); + } + if (ret == WOLFCOSE_SUCCESS) { + recipientEccInited = 1; + ret = wc_ecc_make_key_ex(&rng, 32, &recipientEcc, ECC_SECP256R1); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke ke optional recipient key make"); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CoseKey_Init(&recipientKey); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke ke optional recipient cose key init"); + } + if (ret == WOLFCOSE_SUCCESS) { + recipientKeyInited = 1; + ret = wc_CoseKey_SetEcc(&recipientKey, WOLFCOSE_CRV_P256, + &recipientEcc); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke ke optional recipient cose key set"); + recipientKey.alg = WOLFCOSE_ALG_HPKE_0_KE; + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_HpkeInit(&hpke, DHKEM_P256_HKDF_SHA256, HKDF_SHA256, + HPKE_AES_128_GCM, NULL); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke optional init"); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_ecc_init(&ephemeralEcc); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke ke optional ephemeral ecc init"); + } + if (ret == WOLFCOSE_SUCCESS) { + ephemeralEccInited = 1; + ret = wc_ecc_make_key_ex(&rng, 32, &ephemeralEcc, ECC_SECP256R1); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke ke optional ephemeral key make"); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_HpkeSerializePublicKey(&hpke, &ephemeralEcc, enc, &encLen); + TEST_ASSERT((ret == WOLFCOSE_SUCCESS) && (encLen == sizeof(enc)), + "hpke ke optional encapsulated key"); + } + if (ret == WOLFCOSE_SUCCESS) { + (void)XMEMSET(&ctx, 0, sizeof(ctx)); + ctx.buf = scratch; + ctx.bufSz = sizeof(scratch); + ret = wc_CBOR_EncodeArrayStart(&ctx, 4u); + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeTstr(&ctx, hpkeRecipientContext, + sizeof(hpkeRecipientContext) - 1u); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_ALG_A128GCM); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeBstr(&ctx, &emptyHdr, 0u); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeBstr(&ctx, NULL, 0u); + } + if (ret == WOLFCOSE_SUCCESS) { + recipientInfoLen = ctx.idx; + } + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke ke optional Recipient structure"); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_HpkeSealBase(&hpke, &ephemeralEcc, &recipientEcc, + scratch, (word32)recipientInfoLen, NULL, 0u, + (byte*)keyData, (word32)sizeof(keyData), wrappedCek); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke optional seal"); + } + + for (caseIndex = 0u; + (ret == WOLFCOSE_SUCCESS) && (caseIndex < + (sizeof(cases) / sizeof(cases[0]))); + caseIndex++) { + WOLFCOSE_CBOR_CTX protectedCtx; + WOLFCOSE_HDR clearedHdr; + const uint8_t* protectedCase = &emptyHdr; + size_t protectedCaseLen = 0u; + uint8_t protectedHdr[96]; + uint8_t clearedPlaintext[sizeof(plaintext)]; + int decRet = WOLFCOSE_SUCCESS; + size_t bodyAlgOffset = 0u; + size_t wrappedCekLenOffset = 0u; + size_t wrappedCekDataEnd = 0u; + size_t plaintextLen = sizeof(plaintext); + + if ((cases[caseIndex].flags & HPKE_KE_HDR_CASE_PROTECTED_EK) != 0u) { + (void)XMEMSET(&protectedCtx, 0, sizeof(protectedCtx)); + protectedCtx.buf = protectedHdr; + protectedCtx.bufSz = sizeof(protectedHdr); + decRet = wc_CBOR_EncodeMapStart(&protectedCtx, 1u); + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeInt(&protectedCtx, + WOLFCOSE_HDR_HPKE_EK); + } + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeBstr(&protectedCtx, enc, sizeof(enc)); + } + if (decRet == WOLFCOSE_SUCCESS) { + protectedCase = protectedHdr; + protectedCaseLen = protectedCtx.idx; + } + } + + (void)XMEMSET(&ctx, 0, sizeof(ctx)); + ctx.buf = encoded; + ctx.bufSz = sizeof(encoded); + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeTag(&ctx, WOLFCOSE_TAG_ENCRYPT); + } + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeArrayStart(&ctx, 4u); + } + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeBstr(&ctx, &emptyHdr, 0u); + } + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeMapStart(&ctx, 2u); + } + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_HDR_ALG); + } + if (decRet == WOLFCOSE_SUCCESS) { + bodyAlgOffset = ctx.idx; + decRet = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_ALG_A128GCM); + } + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_HDR_IV); + } + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeBstr(&ctx, iv, sizeof(iv)); + } + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeBstr(&ctx, bodyCiphertext, + sizeof(bodyCiphertext)); + } + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeArrayStart(&ctx, 2u); + } + for (i = 0u; (decRet == WOLFCOSE_SUCCESS) && (i < 2u); i++) { + uint32_t flags = cases[caseIndex].flags; + const uint8_t* recipientProtected = &emptyHdr; + size_t recipientProtectedLen = 0u; + size_t mapEntries = 0u; + + if (i == 0u) { + flags &= HPKE_KE_HDR_CASE_SKIP_ZERO_ALG | + HPKE_KE_HDR_CASE_SKIP_TEXT_ALG; + } + else { + flags &= ~(uint32_t)(HPKE_KE_HDR_CASE_SKIP_ZERO_ALG | + HPKE_KE_HDR_CASE_SKIP_TEXT_ALG); + } + if (i == 1u) { + recipientProtected = protectedCase; + recipientProtectedLen = protectedCaseLen; + } + decRet = wc_CBOR_EncodeArrayStart(&ctx, 3u); + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeBstr(&ctx, recipientProtected, + recipientProtectedLen); + } + if ((flags & HPKE_KE_HDR_CASE_NO_EK) == 0u) { + mapEntries++; + } + if ((flags & HPKE_KE_HDR_CASE_UNPROTECTED_ALG) != 0u) { + mapEntries++; + } + if ((flags & (HPKE_KE_HDR_CASE_SKIP_ZERO_ALG | + HPKE_KE_HDR_CASE_SKIP_TEXT_ALG)) != 0u) { + mapEntries++; + } + if ((flags & HPKE_KE_HDR_CASE_DUPLICATE_EK) != 0u) { + mapEntries++; + } + if ((flags & HPKE_KE_HDR_CASE_PSK_ID) != 0u) { + mapEntries++; + } + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeMapStart(&ctx, mapEntries); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((flags & HPKE_KE_HDR_CASE_UNPROTECTED_ALG) != 0u)) { + decRet = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_HDR_ALG); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((flags & HPKE_KE_HDR_CASE_UNPROTECTED_ALG) != 0u)) { + decRet = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_ALG_HPKE_0_KE); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((flags & HPKE_KE_HDR_CASE_SKIP_ZERO_ALG) != 0u)) { + decRet = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_HDR_ALG); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((flags & HPKE_KE_HDR_CASE_SKIP_ZERO_ALG) != 0u)) { + decRet = wc_CBOR_EncodeInt(&ctx, 0); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((flags & HPKE_KE_HDR_CASE_SKIP_TEXT_ALG) != 0u)) { + decRet = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_HDR_ALG); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((flags & HPKE_KE_HDR_CASE_SKIP_TEXT_ALG) != 0u)) { + decRet = wc_CBOR_EncodeTstr(&ctx, invalidAlg, + sizeof(invalidAlg) - 1u); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((flags & HPKE_KE_HDR_CASE_NO_EK) == 0u)) { + decRet = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_HDR_HPKE_EK); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((flags & HPKE_KE_HDR_CASE_NO_EK) == 0u) && + ((flags & HPKE_KE_HDR_CASE_NON_BSTR_EK) != 0u)) { + decRet = wc_CBOR_EncodeTstr(&ctx, nonBstrEk, + sizeof(nonBstrEk) - 1u); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((flags & HPKE_KE_HDR_CASE_NO_EK) == 0u) && + ((flags & HPKE_KE_HDR_CASE_NON_BSTR_EK) == 0u)) { + size_t ekLen = sizeof(enc); + + if ((flags & HPKE_KE_HDR_CASE_SHORT_EK) != 0u) { + ekLen--; + } + decRet = wc_CBOR_EncodeBstr(&ctx, enc, ekLen); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((flags & HPKE_KE_HDR_CASE_DUPLICATE_EK) != 0u)) { + decRet = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_HDR_HPKE_EK); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((flags & HPKE_KE_HDR_CASE_DUPLICATE_EK) != 0u)) { + decRet = wc_CBOR_EncodeBstr(&ctx, enc, sizeof(enc)); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((flags & HPKE_KE_HDR_CASE_PSK_ID) != 0u)) { + decRet = wc_CBOR_EncodeInt(&ctx, -5); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((flags & HPKE_KE_HDR_CASE_PSK_ID) != 0u)) { + decRet = wc_CBOR_EncodeBstr(&ctx, pskId, sizeof(pskId)); + } + if (decRet == WOLFCOSE_SUCCESS) { + if (i == 1u) { + wrappedCekLenOffset = ctx.idx; + } + decRet = wc_CBOR_EncodeBstr(&ctx, wrappedCek, + wrappedCekLen); + if ((decRet == WOLFCOSE_SUCCESS) && (i == 1u)) { + wrappedCekDataEnd = ctx.idx; + } + } + } + if (decRet == WOLFCOSE_SUCCESS) { + encodedLen = ctx.idx; + hpkeRecipient.algId = WOLFCOSE_ALG_HPKE_0_KE; + hpkeRecipient.key = &recipientKey; + hpkeRecipient.kid = NULL; + hpkeRecipient.kidLen = 0u; + (void)XMEMSET(&hdr, 0xa5, sizeof(hdr)); + (void)XMEMSET(plaintext, 0xa5, sizeof(plaintext)); + decRet = wc_CoseEncrypt_Decrypt(&hpkeRecipient, 1u, encoded, + encodedLen, NULL, 0u, NULL, 0u, scratch, sizeof(scratch), + &hdr, plaintext, sizeof(plaintext), &plaintextLen); + } + TEST_ASSERT(decRet == cases[caseIndex].expected, cases[caseIndex].name); + if (cases[caseIndex].expected == WOLFCOSE_SUCCESS) { + TEST_ASSERT(decRet == WOLFCOSE_SUCCESS, + "hpke ke optional recipient alg accepted"); + TEST_ASSERT((plaintextLen == (sizeof(payload) - 1u)) && + (memcmp(plaintext, payload, plaintextLen) == 0), + "hpke ke unprotected body alg payload"); + if (cases[caseIndex].flags == 0u) { + int tamperedRet = WOLFCOSE_SUCCESS; + size_t tamperedPlaintextLen = sizeof(plaintext); + const size_t expandedCekLen = wrappedCekLen + 8u; + + if ((bodyAlgOffset < encodedLen) && + ((wrappedCekLenOffset + 1u) < encodedLen) && + (wrappedCekDataEnd <= encodedLen) && + ((encodedLen + 8u) <= sizeof(encoded)) && + (encoded[bodyAlgOffset] == 0x01u) && + (encoded[wrappedCekLenOffset] == 0x58u) && + (encoded[wrappedCekLenOffset + 1u] == + (uint8_t)wrappedCekLen)) { + XMEMMOVE(&encoded[wrappedCekDataEnd + 8u], + &encoded[wrappedCekDataEnd], + encodedLen - wrappedCekDataEnd); + (void)XMEMSET(&encoded[wrappedCekDataEnd], 0, 8u); + encoded[bodyAlgOffset] = 0x02u; + encoded[wrappedCekLenOffset + 1u] = + (uint8_t)expandedCekLen; + encodedLen += 8u; + (void)XMEMSET(&hdr, 0xa5, sizeof(hdr)); + (void)XMEMSET(plaintext, 0xa5, sizeof(plaintext)); + tamperedRet = wc_CoseEncrypt_Decrypt(&hpkeRecipient, 1u, + encoded, encodedLen, NULL, 0u, NULL, 0u, scratch, + sizeof(scratch), &hdr, plaintext, sizeof(plaintext), + &tamperedPlaintextLen); + } + TEST_ASSERT(tamperedRet != WOLFCOSE_SUCCESS, + "hpke ke content alg tamper rejected"); + (void)XMEMSET(&clearedHdr, 0, sizeof(clearedHdr)); + (void)XMEMSET(clearedPlaintext, 0, sizeof(clearedPlaintext)); + TEST_ASSERT((tamperedPlaintextLen == 0u) && + (memcmp(&hdr, &clearedHdr, sizeof(hdr)) == 0) && + (memcmp(plaintext, clearedPlaintext, + sizeof(plaintext)) == 0), + "hpke ke content alg tamper clears outputs"); + } + } + else { + (void)XMEMSET(&clearedHdr, 0, sizeof(clearedHdr)); + (void)XMEMSET(clearedPlaintext, 0, sizeof(clearedPlaintext)); + TEST_ASSERT((plaintextLen == 0u) && + (memcmp(&hdr, &clearedHdr, sizeof(hdr)) == 0) && + (memcmp(plaintext, clearedPlaintext, + sizeof(plaintext)) == 0), + "hpke ke malformed header clears outputs"); + } + } + + if (recipientKeyInited != 0) { + wc_CoseKey_Free(&recipientKey); + } + if (ephemeralEccInited != 0) { + (void)wc_ecc_free(&ephemeralEcc); + } + if (recipientEccInited != 0) { + (void)wc_ecc_free(&recipientEcc); + } + if (rngInited != 0) { + (void)wc_FreeRng(&rng); + } + (void)XMEMSET(&hpke, 0, sizeof(hpke)); +} +#endif /* WOLFCOSE_HPKE_0_KE_ENCRYPT && WOLFCOSE_HPKE_0_KE_DECRYPT */ + +#if defined(WOLFCOSE_HPKE_0_DECRYPT) +/* draft-ietf-cose-hpke-26, Section 5.1, Figure 2 and Figure 4. */ +static void test_cose_hpke_encrypt0_draft_vector(void) +{ + static const uint8_t encodedHex[] = + "d08344a1011823a20443626f622358410457229bdd99407b384a9e59fa15" + "53224d58b106e9ebebdaa06d2126bd96757674847669966ecb0dcdf21af5" + "623f19f0b799b0cddf3ee930b739dd474f6282de0158253f3c1595e9d252" + "e816215a9ce73f47ba4b57acb06ecc39ca5a03a14108bbe7807af5688d61"; + static const uint8_t xHex[] = + "02a8e3315f96bc7355dbf85740c6d8e53fb070cd8ba5c419be49a91d789ef55c"; + static const uint8_t yHex[] = + "96b6621abf5ca532e042dc5c346c1ef0c9186b83cb122e50a46f1458de023d35"; + static const uint8_t dHex[] = + "eca39300147c91a2a65d17e00ea278b57a14178245bf5686d9a404cca1816b8e"; + /* The draft's stated plaintext omits the trailing LF carried by its + * published ciphertext. Test the bytes on the wire. */ + static const uint8_t expected[] = "This is the content.\n"; + WOLFCOSE_KEY recipientKey; + WOLFCOSE_HDR hdr; + ecc_key eccKey; + uint8_t encoded[160]; + uint8_t x[32]; + uint8_t y[32]; + uint8_t d[32]; + uint8_t scratch[256]; + uint8_t plaintext[64]; + size_t plaintextLen = 0u; + size_t encodedLen = sizeof(encoded); + size_t xLen = sizeof(x); + size_t yLen = sizeof(y); + size_t dLen = sizeof(d); + int ret = WOLFCOSE_SUCCESS; + int eccInited = 0; + int keyInited = 0; + + TEST_LOG(" [COSE HPKE Encrypt0 draft vector]\n"); + (void)XMEMSET(&recipientKey, 0, sizeof(recipientKey)); + (void)XMEMSET(&hdr, 0, sizeof(hdr)); + + ret = test_cose_hex_decode(encodedHex, sizeof(encodedHex) - 1u, + encoded, sizeof(encoded), &encodedLen); + TEST_ASSERT(ret == 0, "hpke encrypt0 vector decode"); + if (ret == 0) { + ret = test_cose_hex_decode(xHex, sizeof(xHex) - 1u, + x, sizeof(x), &xLen); + } + if (ret == 0) { + ret = test_cose_hex_decode(yHex, sizeof(yHex) - 1u, + y, sizeof(y), &yLen); + } + if (ret == 0) { + ret = test_cose_hex_decode(dHex, sizeof(dHex) - 1u, + d, sizeof(d), &dLen); + } + TEST_ASSERT((ret == 0) && (xLen == sizeof(x)) && + (yLen == sizeof(y)) && (dLen == sizeof(d)), + "hpke encrypt0 vector key decode"); + if (ret == 0) { + ret = wc_ecc_init(&eccKey); + TEST_ASSERT(ret == 0, "hpke encrypt0 vector ecc init"); + if (ret == 0) { + eccInited = 1; + } + } + if (ret == 0) { + ret = wc_ecc_import_unsigned(&eccKey, x, y, d, ECC_SECP256R1); + TEST_ASSERT(ret == 0, "hpke encrypt0 vector key import"); + } + if (ret == 0) { + ret = wc_CoseKey_Init(&recipientKey); + TEST_ASSERT(ret == 0, "hpke encrypt0 vector cose key init"); + if (ret == 0) { + keyInited = 1; + } + } + if (ret == 0) { + ret = wc_CoseKey_SetEcc(&recipientKey, WOLFCOSE_CRV_P256, &eccKey); + TEST_ASSERT(ret == 0, "hpke encrypt0 vector cose key set"); + recipientKey.alg = WOLFCOSE_ALG_HPKE_0; + } + if (ret == 0) { + ret = wc_CoseHpkeEncrypt0_Decrypt(&recipientKey, encoded, encodedLen, + NULL, 0u, NULL, 0u, scratch, sizeof(scratch), &hdr, + plaintext, sizeof(plaintext), &plaintextLen); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke encrypt0 draft vector decrypt"); + TEST_ASSERT((plaintextLen == (sizeof(expected) - 1u)) && + (memcmp(plaintext, expected, plaintextLen) == 0), + "hpke encrypt0 draft vector plaintext"); + TEST_ASSERT((hdr.alg == WOLFCOSE_ALG_HPKE_0) && + (hdr.kidLen == 3u), + "hpke encrypt0 draft vector headers"); + } + + if (keyInited != 0) { + wc_CoseKey_Free(&recipientKey); + } + if (eccInited != 0) { + (void)wc_ecc_free(&eccKey); + } +} +#endif /* WOLFCOSE_HPKE_0_DECRYPT */ + +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) && \ + defined(WOLFCOSE_HPKE_0_KE_DECRYPT) +static void test_cose_hpke_key_encryption(void) +{ + WOLFCOSE_KEY recipientKey1; + WOLFCOSE_KEY recipientKey2; + WOLFCOSE_KEY wrongKey; + WOLFCOSE_RECIPIENT recipients[2]; + WOLFCOSE_RECIPIENT wrongRecipient; + WOLFCOSE_HDR hdr; + ecc_key recipientEcc1; + ecc_key recipientEcc2; + ecc_key wrongEcc; + WC_RNG rng; + int ret = WOLFCOSE_SUCCESS; + int rngInited = 0; + int recipientEcc1Inited = 0; + int recipientEcc2Inited = 0; + int wrongEccInited = 0; + int recipientKey1Inited = 0; + int recipientKey2Inited = 0; + int wrongKeyInited = 0; + uint8_t out[1024]; + uint8_t plaintext[128]; + uint8_t scratch[512]; + size_t outLen = 0u; + size_t plaintextLen = 0u; + size_t ciphertextLen = 0u; + size_t i; + int foundKid = 0; + const uint8_t kid1[] = "hpke-recipient-1"; + const uint8_t kid2[] = "hpke-recipient-2"; + const uint8_t payload[] = "COSE HPKE key encryption"; + const uint8_t emptyPayload[] = { 0u }; + const uint8_t iv[12] = { + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c + }; + const uint8_t aad[] = "COSE Encrypt external aad"; + + TEST_LOG(" [COSE HPKE-0-KE Multi-Recipient]\n"); + (void)XMEMSET(&recipientEcc1, 0, sizeof(recipientEcc1)); + (void)XMEMSET(&recipientEcc2, 0, sizeof(recipientEcc2)); + (void)XMEMSET(&wrongEcc, 0, sizeof(wrongEcc)); + + ret = wc_InitRng(&rng); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke rng init"); + if (ret == WOLFCOSE_SUCCESS) { + rngInited = 1; + ret = wc_ecc_init(&recipientEcc1); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke recipient 1 ecc init"); + } + if (ret == WOLFCOSE_SUCCESS) { + recipientEcc1Inited = 1; + ret = wc_ecc_make_key(&rng, 32, &recipientEcc1); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke recipient 1 key make"); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_ecc_init(&recipientEcc2); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke recipient 2 ecc init"); + } + if (ret == WOLFCOSE_SUCCESS) { + recipientEcc2Inited = 1; + ret = wc_ecc_make_key(&rng, 32, &recipientEcc2); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke recipient 2 key make"); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_ecc_init(&wrongEcc); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke wrong ecc init"); + } + if (ret == WOLFCOSE_SUCCESS) { + wrongEccInited = 1; + ret = wc_ecc_make_key(&rng, 32, &wrongEcc); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke wrong key make"); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CoseKey_Init(&recipientKey1); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke recipient 1 key init"); + } + if (ret == WOLFCOSE_SUCCESS) { + recipientKey1Inited = 1; + ret = wc_CoseKey_SetEcc(&recipientKey1, WOLFCOSE_CRV_P256, + &recipientEcc1); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke recipient 1 key set"); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CoseKey_Init(&recipientKey2); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke recipient 2 key init"); + } + if (ret == WOLFCOSE_SUCCESS) { + recipientKey2Inited = 1; + ret = wc_CoseKey_SetEcc(&recipientKey2, WOLFCOSE_CRV_P256, + &recipientEcc2); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke recipient 2 key set"); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CoseKey_Init(&wrongKey); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke wrong key init"); + } + if (ret == WOLFCOSE_SUCCESS) { + wrongKeyInited = 1; + ret = wc_CoseKey_SetEcc(&wrongKey, WOLFCOSE_CRV_P256, &wrongEcc); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke wrong key set"); + } + if (ret == WOLFCOSE_SUCCESS) { + recipientKey1.alg = WOLFCOSE_ALG_HPKE_0_KE; + recipientKey2.alg = WOLFCOSE_ALG_HPKE_0_KE; + wrongKey.alg = WOLFCOSE_ALG_HPKE_0_KE; + recipientKey1.hasPrivate = 0u; + recipientKey2.hasPrivate = 0u; + recipients[0].algId = WOLFCOSE_ALG_HPKE_0_KE; + recipients[0].key = &recipientKey1; + recipients[0].kid = kid1; + recipients[0].kidLen = sizeof(kid1) - 1u; + recipients[1].algId = WOLFCOSE_ALG_HPKE_0_KE; + recipients[1].key = &recipientKey2; + recipients[1].kid = kid2; + recipients[1].kidLen = sizeof(kid2) - 1u; + + ret = wc_CoseEncrypt_Encrypt(recipients, 2, + WOLFCOSE_ALG_A128GCM, iv, sizeof(iv), + payload, sizeof(payload) - 1u, + NULL, 0u, aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), out, sizeof(out), &outLen, &rng); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke encrypt"); + } + if (ret == WOLFCOSE_SUCCESS) { + recipientKey1.hasPrivate = 1u; + recipientKey2.hasPrivate = 1u; + (void)XMEMSET(&hdr, 0, sizeof(hdr)); + ret = wc_CoseEncrypt_Decrypt(&recipients[0], 0, out, outLen, + NULL, 0u, aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), &hdr, + plaintext, sizeof(plaintext), &plaintextLen); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke recipient 1 decrypt"); + TEST_ASSERT((plaintextLen == (sizeof(payload) - 1u)) && + (memcmp(plaintext, payload, plaintextLen) == 0), + "hpke ke recipient 1 payload matches"); + } + if (ret == WOLFCOSE_SUCCESS) { + (void)XMEMSET(&hdr, 0, sizeof(hdr)); + ret = wc_CoseEncrypt_Decrypt(&recipients[1], 1, out, outLen, + NULL, 0u, aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), &hdr, + plaintext, sizeof(plaintext), &plaintextLen); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke recipient 2 decrypt"); + TEST_ASSERT((plaintextLen == (sizeof(payload) - 1u)) && + (memcmp(plaintext, payload, plaintextLen) == 0), + "hpke ke recipient 2 payload matches"); + } + if (ret == WOLFCOSE_SUCCESS) { + wrongRecipient.algId = WOLFCOSE_ALG_HPKE_0_KE; + wrongRecipient.key = &wrongKey; + wrongRecipient.kid = kid1; + wrongRecipient.kidLen = sizeof(kid1) - 1u; + ret = wc_CoseEncrypt_Decrypt(&wrongRecipient, 0, out, outLen, + NULL, 0u, aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), &hdr, + plaintext, sizeof(plaintext), &plaintextLen); + TEST_ASSERT(ret == WOLFCOSE_E_COSE_DECRYPT_FAIL, + "hpke ke wrong private key rejected"); + } + if (outLen >= (sizeof(kid1) - 1u)) { + for (i = 0u; i <= (outLen - (sizeof(kid1) - 1u)); i++) { + if (memcmp(&out[i], kid1, sizeof(kid1) - 1u) == 0) { + foundKid = 1; + out[i] ^= 0x01u; + break; + } + } + TEST_ASSERT(foundKid != 0, "hpke ke recipient kid located"); + if (foundKid != 0) { + ret = wc_CoseEncrypt_Decrypt(&recipients[0], 0, out, outLen, + NULL, 0u, aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), &hdr, + plaintext, sizeof(plaintext), &plaintextLen); + TEST_ASSERT(ret == WOLFCOSE_E_COSE_DECRYPT_FAIL, + "hpke ke recipient protected header bound"); + out[i] ^= 0x01u; + } + } + if ((recipientKey1Inited != 0) && (recipientKey2Inited != 0)) { + int emptyRet; + + recipientKey1.hasPrivate = 0u; + recipientKey2.hasPrivate = 0u; + ret = wc_CoseEncrypt_Encrypt(recipients, 2, + WOLFCOSE_ALG_A128GCM, iv, sizeof(iv), + emptyPayload, 0u, NULL, 0u, aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), out, sizeof(out), &outLen, &rng); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke empty encrypt"); + if (ret == WOLFCOSE_SUCCESS) { + emptyRet = test_cose_hpke_ciphertext_len(out, outLen, + WOLFCOSE_TAG_ENCRYPT, 4u, &ciphertextLen); + TEST_ASSERT((emptyRet == WOLFCOSE_SUCCESS) && + (ciphertextLen == 16u), + "hpke ke empty tag length"); + recipientKey1.hasPrivate = 1u; + recipientKey2.hasPrivate = 1u; + for (i = 0u; i < 2u; i++) { + (void)XMEMSET(&hdr, 0, sizeof(hdr)); + plaintextLen = sizeof(plaintext); + emptyRet = wc_CoseEncrypt_Decrypt(&recipients[i], i, + out, outLen, NULL, 0u, aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), &hdr, + plaintext, sizeof(plaintext), &plaintextLen); + TEST_ASSERT((emptyRet == WOLFCOSE_SUCCESS) && + (plaintextLen == 0u), + "hpke ke empty recipient decrypt"); + } + } + } - if ((ctx.idx < ctx.bufSz) && - (wc_CBOR_PeekType(&ctx) == WOLFCOSE_CBOR_TAG)) { - ret = wc_CBOR_DecodeTag(&ctx, &tagVal); + if (wrongKeyInited != 0) { + wc_CoseKey_Free(&wrongKey); } - else { - ret = 0; + if (recipientKey2Inited != 0) { + wc_CoseKey_Free(&recipientKey2); + } + if (recipientKey1Inited != 0) { + wc_CoseKey_Free(&recipientKey1); + } + if (wrongEccInited != 0) { + (void)wc_ecc_free(&wrongEcc); + } + if (recipientEcc2Inited != 0) { + (void)wc_ecc_free(&recipientEcc2); + } + if (recipientEcc1Inited != 0) { + (void)wc_ecc_free(&recipientEcc1); + } + if (rngInited != 0) { + (void)wc_FreeRng(&rng); } +} +#endif /* WOLFCOSE_HPKE_0_KE_ENCRYPT && WOLFCOSE_HPKE_0_KE_DECRYPT */ + +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) +/* draft-ietf-cose-hpke-26, Section 5.2, encodes a 16-byte IV for + * A128GCM. RFC 9053 fixes the COSE AES-GCM nonce size at 96 bits, so reject + * the non-conforming draft vector before attempting to unwrap its CEK. */ +static void test_cose_hpke_key_encryption_draft_vector_rejected(void) +{ + static const uint8_t encodedHex[] = + "d8608443a10101a1055089115f10ecc1c7fd834442cb87929bc15825534d" + "b92f5366e3cadd096774a9576bb8d8867e75ea38c329ecfc7b8793c5a4ae" + "9603e5b0b6818349a201182e0443626f62a12358410417cd85837981ddb1" + "4963061ab5fb7308988eb922f87cf6cf6ef83556f7657922c9815947e41b" + "9bc932e48c6f1c4677d9a5506a30d694587628b5193a4cde2f3f58204b50" + "8a340e463c317f4e62fb8d08c887cac4788087ad022562d05855a50ca4a0"; + static const uint8_t xHex[] = + "d832916778598ea6203af974c97b45970ac0266fc6a3b7f213ba9f8b591b9297"; + static const uint8_t yHex[] = + "8d9410599a8e83d00eb46d67b34d4dac8fbd4b8b1f08864599659cee9ef09184"; + static const uint8_t dHex[] = + "b1162c568efcba91c8e4e82f66e36b45aa10bc55228cf65ecd3bb29cfb09f989"; + WOLFCOSE_KEY recipientKey; + WOLFCOSE_RECIPIENT recipient; + WOLFCOSE_HDR hdr; + ecc_key eccKey; + uint8_t encoded[256]; + uint8_t x[32]; + uint8_t y[32]; + uint8_t d[32]; + uint8_t scratch[256]; + uint8_t plaintext[64]; + size_t plaintextLen = 0u; + size_t encodedLen = sizeof(encoded); + size_t xLen = sizeof(x); + size_t yLen = sizeof(y); + size_t dLen = sizeof(d); + int ret = WOLFCOSE_SUCCESS; + int eccInited = 0; + int keyInited = 0; + TEST_LOG(" [COSE HPKE key-encryption draft vector rejection]\n"); + (void)XMEMSET(&recipientKey, 0, sizeof(recipientKey)); + (void)XMEMSET(&hdr, 0, sizeof(hdr)); + + ret = test_cose_hex_decode(encodedHex, sizeof(encodedHex) - 1u, + encoded, sizeof(encoded), &encodedLen); + TEST_ASSERT(ret == 0, "hpke ke vector decode"); if (ret == 0) { - ret = wc_CBOR_DecodeArrayStart(&ctx, &count); - if ((ret != 0) || (count != 4u)) { - ret = -1; - } + ret = test_cose_hex_decode(xHex, sizeof(xHex) - 1u, + x, sizeof(x), &xLen); } if (ret == 0) { - ret = wc_CBOR_Skip(&ctx); - if (ret == 0) { - ret = wc_CBOR_Skip(&ctx); - } - if (ret == 0) { - ret = wc_CBOR_Skip(&ctx); - } - if (ret != 0) { - ret = -1; - } + ret = test_cose_hex_decode(yHex, sizeof(yHex) - 1u, + y, sizeof(y), &yLen); } if (ret == 0) { - ret = wc_CBOR_DecodeArrayStart(&ctx, &count); - if ((ret != 0) || (count < 1u)) { - ret = -1; - } + ret = test_cose_hex_decode(dHex, sizeof(dHex) - 1u, + d, sizeof(d), &dLen); } + TEST_ASSERT((ret == 0) && (xLen == sizeof(x)) && + (yLen == sizeof(y)) && (dLen == sizeof(d)), + "hpke ke vector key decode"); if (ret == 0) { - ret = wc_CBOR_DecodeArrayStart(&ctx, &count); - if ((ret != 0) || (count != 3u)) { - ret = -1; + ret = wc_ecc_init(&eccKey); + TEST_ASSERT(ret == 0, "hpke ke vector ecc init"); + if (ret == 0) { + eccInited = 1; } } - /* RFC 9053 6.2.1: the AES Key Wrap recipient has an empty protected - * bucket, so the algorithm is carried in the unprotected header. */ if (ret == 0) { - ret = wc_CBOR_DecodeBstr(&ctx, &protectedData, &protectedLen); - if ((ret != 0) || (protectedLen != 0u)) { - ret = -1; - } + ret = wc_ecc_import_unsigned(&eccKey, x, y, d, ECC_SECP256R1); + TEST_ASSERT(ret == 0, "hpke ke vector key import"); } if (ret == 0) { - ret = wc_CBOR_DecodeMapStart(&ctx, &count); - if ((ret != 0) || (count < 1u)) { - ret = -1; + ret = wc_CoseKey_Init(&recipientKey); + TEST_ASSERT(ret == 0, "hpke ke vector cose key init"); + if (ret == 0) { + keyInited = 1; } } if (ret == 0) { - ret = wc_CBOR_DecodeInt(&ctx, &label); - if ((ret != 0) || (label != WOLFCOSE_HDR_ALG)) { - ret = -1; - } + ret = wc_CoseKey_SetEcc(&recipientKey, WOLFCOSE_CRV_P256, &eccKey); + TEST_ASSERT(ret == 0, "hpke ke vector cose key set"); + recipientKey.alg = WOLFCOSE_ALG_HPKE_0_KE; } if (ret == 0) { - (void)protectedData; - if (ctx.idx < ctx.bufSz) { - msg[ctx.idx] = algByte; - } - else { - ret = -1; - } - } - - return ret; -} - -static void test_cose_encrypt_multi_recipient(void) -{ - WOLFCOSE_KEY key1, key2; - WOLFCOSE_RECIPIENT recipients[2]; - WOLFCOSE_HDR hdr; - int ret; - uint8_t out[512]; - size_t outLen; - uint8_t scratch[256]; - uint8_t plaintext[256]; - size_t plaintextLen; - const uint8_t payload[] = "Multi-recipient encryption test"; - const uint8_t iv[12] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, - 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C}; - const uint8_t keyData[16] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, - 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F}; - const uint8_t wrongKeyData[16] = {0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8, - 0xF7, 0xF6, 0xF5, 0xF4, 0xF3, 0xF2, 0xF1, 0xF0}; - const uint8_t kid1[] = "recipient-1"; - const uint8_t kid2[] = "recipient-2"; - - TEST_LOG(" [Encrypt Multi-Recipient A128GCM]\n"); - - /* Setup keys - both recipients use the same shared key in direct mode */ - (void)wc_CoseKey_Init(&key1); - ret = wc_CoseKey_SetSymmetric(&key1, keyData, sizeof(keyData)); - TEST_ASSERT(ret == 0, "encrypt key1 set"); - - (void)wc_CoseKey_Init(&key2); - ret = wc_CoseKey_SetSymmetric(&key2, keyData, sizeof(keyData)); - TEST_ASSERT(ret == 0, "encrypt key2 set"); - - /* Setup recipients */ - recipients[0].algId = WOLFCOSE_ALG_DIRECT; /* Direct key */ - recipients[0].key = &key1; - recipients[0].kid = kid1; - recipients[0].kidLen = sizeof(kid1) - 1; - - recipients[1].algId = WOLFCOSE_ALG_DIRECT; /* Direct key */ - recipients[1].key = &key2; - recipients[1].kid = kid2; - recipients[1].kidLen = sizeof(kid2) - 1; - - /* Encrypt with two recipients */ - ret = wc_CoseEncrypt_Encrypt(recipients, 2, - WOLFCOSE_ALG_A128GCM, - iv, sizeof(iv), - payload, sizeof(payload) - 1, - NULL, 0, /* no detached payload */ - NULL, 0, /* no external AAD */ - scratch, sizeof(scratch), - out, sizeof(out), &outLen, - NULL); - TEST_ASSERT(ret == 0, "encrypt multi create"); - - /* Decrypt with first recipient */ - memset(&hdr, 0, sizeof(hdr)); - ret = wc_CoseEncrypt_Decrypt(&recipients[0], 0, - out, outLen, - NULL, 0, /* no detached ciphertext */ - NULL, 0, /* no external AAD */ - scratch, sizeof(scratch), - &hdr, - plaintext, sizeof(plaintext), &plaintextLen); - TEST_ASSERT(ret == 0, "encrypt decrypt recipient 0"); - TEST_ASSERT(plaintextLen == sizeof(payload) - 1, "encrypt payload len 0"); - TEST_ASSERT(memcmp(plaintext, payload, plaintextLen) == 0, "encrypt payload match 0"); - - /* Decrypt with second recipient */ - memset(&hdr, 0, sizeof(hdr)); - memset(plaintext, 0, sizeof(plaintext)); - ret = wc_CoseEncrypt_Decrypt(&recipients[1], 1, - out, outLen, - NULL, 0, - NULL, 0, - scratch, sizeof(scratch), - &hdr, - plaintext, sizeof(plaintext), &plaintextLen); - TEST_ASSERT(ret == 0, "encrypt decrypt recipient 1"); - TEST_ASSERT(plaintextLen == sizeof(payload) - 1, "encrypt payload len 1"); - TEST_ASSERT(memcmp(plaintext, payload, plaintextLen) == 0, "encrypt payload match 1"); - - /* Verify headers */ - TEST_ASSERT(hdr.alg == WOLFCOSE_ALG_A128GCM, "encrypt hdr alg"); - TEST_ASSERT(hdr.ivLen == sizeof(iv), "encrypt hdr iv len"); - TEST_ASSERT(memcmp(hdr.iv, iv, sizeof(iv)) == 0, "encrypt hdr iv match"); - - /* Wrong key should fail */ - /* empty-brace-scan: allow - test-local temporary scope */ - { - WOLFCOSE_KEY wrongKey; - WOLFCOSE_RECIPIENT wrongRecipient; - (void)wc_CoseKey_Init(&wrongKey); - (void)wc_CoseKey_SetSymmetric(&wrongKey, wrongKeyData, sizeof(wrongKeyData)); - wrongRecipient.algId = 0; - wrongRecipient.key = &wrongKey; - wrongRecipient.kid = NULL; - wrongRecipient.kidLen = 0; - - ret = wc_CoseEncrypt_Decrypt(&wrongRecipient, 0, - out, outLen, - NULL, 0, - NULL, 0, - scratch, sizeof(scratch), - &hdr, + recipient.algId = WOLFCOSE_ALG_HPKE_0_KE; + recipient.key = &recipientKey; + recipient.kid = NULL; + recipient.kidLen = 0u; + ret = wc_CoseEncrypt_Decrypt(&recipient, 0u, encoded, encodedLen, + NULL, 0u, NULL, 0u, scratch, sizeof(scratch), &hdr, plaintext, sizeof(plaintext), &plaintextLen); - TEST_ASSERT(ret == WOLFCOSE_E_COSE_DECRYPT_FAIL, "encrypt wrong key fails"); - wc_CoseKey_Free(&wrongKey); + TEST_ASSERT(ret == WOLFCOSE_E_COSE_BAD_HDR, + "hpke ke draft vector rejects 16-byte GCM IV"); } - /* Invalid recipient index should fail */ - ret = wc_CoseEncrypt_Decrypt(&recipients[0], 5, - out, outLen, - NULL, 0, - NULL, 0, - scratch, sizeof(scratch), - &hdr, - plaintext, sizeof(plaintext), &plaintextLen); - TEST_ASSERT(ret != 0, "encrypt invalid recipient index fails"); - - wc_CoseKey_Free(&key1); - wc_CoseKey_Free(&key2); + if (keyInited != 0) { + wc_CoseKey_Free(&recipientKey); + } + if (eccInited != 0) { + (void)wc_ecc_free(&eccKey); + } } +#endif /* WOLFCOSE_HPKE_0_KE_DECRYPT */ + +#if defined(WOLFCOSE_ENCRYPT) && defined(WOLFCOSE_HAVE_AESGCM) && \ + defined(WOLFCOSE_KEY_WRAP) static void test_cose_encrypt_with_aad(void) { @@ -13393,7 +15117,7 @@ static void test_cose_encrypt_direct_wrong_key_type(void) } #endif /* WOLFCOSE_HAVE_ES256 */ -#endif /* WOLFCOSE_ENCRYPT && WOLFCOSE_HAVE_AESGCM */ +#endif /* WOLFCOSE_ENCRYPT && WOLFCOSE_HAVE_AESGCM && WOLFCOSE_KEY_WRAP */ /* ----- COSE_Mac Multi-Recipient Tests (RFC 9052 Section 6.1) ----- */ #if defined(WOLFCOSE_MAC) && defined(WOLFCOSE_HAVE_HMAC256) @@ -26700,6 +28424,7 @@ int test_cose(void) g_failures = 0; /* Internal helper tests */ + test_cose_hdr_abi_layout(); test_wolfcose_force_zero(); #if defined(WOLFCOSE_HAVE_ES256) && defined(WOLFCOSE_SIGN1_SIGN) test_cose_sign1_size_and_untagged(); @@ -26821,6 +28546,13 @@ int test_cose(void) #if defined(SIZE_MAX) && (SIZE_MAX > 0xFFFFFFFFUL) test_cose_encrypt0_word32_overflow_guard(); #endif +#if defined(WOLFCOSE_HPKE_0_ENCRYPT) && defined(WOLFCOSE_HPKE_0_DECRYPT) + test_cose_hpke_encrypt0(); + test_cose_hpke_encrypt0_optional_alg(); +#endif +#if defined(WOLFCOSE_HPKE_0_DECRYPT) + test_cose_hpke_encrypt0_draft_vector(); +#endif #endif /* ChaCha20-Poly1305 encryption tests */ @@ -26979,6 +28711,14 @@ int test_cose(void) #ifdef WOLFCOSE_HAVE_ES256 test_cose_encrypt_direct_wrong_key_type(); #endif +#endif +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) && \ + defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + test_cose_hpke_key_encryption(); + test_cose_hpke_key_encryption_optional_alg(); +#endif +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + test_cose_hpke_key_encryption_draft_vector_rejected(); #endif /* Multi-recipient MAC tests */ diff --git a/tools/wolfcose_tool.c b/tools/wolfcose_tool.c index 224cbba..7073a5f 100644 --- a/tools/wolfcose_tool.c +++ b/tools/wolfcose_tool.c @@ -30,12 +30,23 @@ * counterverify -k -i * enc -k -a -i -o <cose_file> * dec -k <keyfile> -i <cose_file> -o <plaintext> + * hpke0-enc -k <public-key> -i <plaintext> -o <cose_file> + * hpke0-dec -k <private-key> -i <cose_file> -o <plaintext> + * hpke-ke-enc -a <alg> -k <public-key> [-k <public-key> ...] -i <plaintext> -o <cose_file> + * hpke-ke-dec -k <private-key> [-r <recipient-index>] -i <cose_file> -o <plaintext> * info -i <cose_file> * * Key files: raw COSE_Key CBOR format. * Exit codes: 0=success, 1=usage, 2=crypto failure, 3=I/O error. */ +/* Request the POSIX interfaces used by the optional HPKE key-output guard + * before any system header on POSIX hosts. */ +#if (defined(__unix__) || defined(__APPLE__) || defined(__MACH__) || \ + defined(__CYGWIN__)) && !defined(_XOPEN_SOURCE) + #define _XOPEN_SOURCE 700 +#endif + #ifdef HAVE_CONFIG_H #include <config.h> #endif @@ -45,8 +56,18 @@ #include <wolfssl/wolfcrypt/settings.h> #include <wolfcose/wolfcose.h> + +#if !defined(WOLFCOSE_TOOL_HAVE_POSIX_FS) + #if defined(__unix__) || defined(__APPLE__) || defined(__MACH__) || \ + defined(__CYGWIN__) + #define WOLFCOSE_TOOL_HAVE_POSIX_FS 1 + #else + #define WOLFCOSE_TOOL_HAVE_POSIX_FS 0 + #endif +#endif + #include <wolfssl/wolfcrypt/random.h> -#ifdef WOLFCOSE_HAVE_ECDSA +#if defined(WOLFCOSE_HAVE_ECDSA) || defined(WOLFCOSE_HAVE_HPKE_0) #include <wolfssl/wolfcrypt/ecc.h> #endif #ifdef WOLFCOSE_HAVE_EDDSA @@ -65,6 +86,15 @@ #include <stdio.h> #include <stdlib.h> #include <string.h> +#if defined(WOLFCOSE_HAVE_HPKE_0) + #include <errno.h> + #if (WOLFCOSE_TOOL_HAVE_POSIX_FS == 1) + #include <fcntl.h> + #include <limits.h> + #include <sys/stat.h> + #include <unistd.h> + #endif +#endif #ifndef WOLFCOSE_TOOL_MAX_MSG #define WOLFCOSE_TOOL_MAX_MSG 8192 @@ -85,6 +115,51 @@ * verified again by the next command. */ #define WOLFCOSE_TOOL_MAX_COUNTER_MSG \ (WOLFCOSE_TOOL_MAX_MSG + WOLFCOSE_TOOL_MAX_KEY + WOLFCOSE_MAX_SIG_SZ + 128u) +#if defined(WOLFCOSE_HAVE_HPKE_0) && \ + (WOLFCOSE_TOOL_HAVE_POSIX_FS == 1) +/* PATH_MAX is optional in POSIX headers. This fallback matches the minimum + * practical path size supported by the command-line tool. */ + #ifndef PATH_MAX + #define WOLFCOSE_TOOL_PATH_MAX 4096u + #else + #define WOLFCOSE_TOOL_PATH_MAX PATH_MAX + #endif +#endif + +/* Bounds the command-line multi-recipient HPKE helper without limiting the + * library API. Integrators can reduce this for constrained tools. */ +#if defined(WOLFCOSE_HAVE_HPKE_0) +#ifndef WOLFCOSE_TOOL_MAX_HPKE_RECIPIENTS + #define WOLFCOSE_TOOL_MAX_HPKE_RECIPIENTS 4u +#endif +#if (WOLFCOSE_TOOL_MAX_HPKE_RECIPIENTS < 1u) + #error "WOLFCOSE_TOOL_MAX_HPKE_RECIPIENTS must be at least one" +#endif +#endif + +#if defined(WOLFCOSE_HPKE_0_ENCRYPT) || defined(WOLFCOSE_HPKE_0_DECRYPT) +/* Direct HPKE-0 adds protected and unprotected headers, a P-256 enc value, + * an AEAD tag, and CBOR framing. */ +#define WOLFCOSE_TOOL_HPKE_0_ENCODED_MAX \ + ((size_t)WOLFCOSE_TOOL_MAX_MSG + 128u) +#endif + +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) || \ + defined(WOLFCOSE_HPKE_0_KE_DECRYPT) +/* Each HPKE-0-KE recipient carries a fixed P-256 enc value and wrapped CEK. */ +#define WOLFCOSE_TOOL_HPKE_0_KE_ENCODED_MAX \ + ((size_t)WOLFCOSE_TOOL_MAX_MSG + 64u + \ + (128u * (size_t)WOLFCOSE_TOOL_MAX_HPKE_RECIPIENTS)) +#endif + +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) || \ + defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + #define WOLFCOSE_TOOL_MAX_COSE_MSG WOLFCOSE_TOOL_HPKE_0_KE_ENCODED_MAX +#elif defined(WOLFCOSE_HPKE_0_ENCRYPT) || defined(WOLFCOSE_HPKE_0_DECRYPT) + #define WOLFCOSE_TOOL_MAX_COSE_MSG WOLFCOSE_TOOL_HPKE_0_ENCODED_MAX +#else + #define WOLFCOSE_TOOL_MAX_COSE_MSG WOLFCOSE_TOOL_MAX_MSG +#endif #define EXIT_USAGE 1 #define EXIT_CRYPTO 2 @@ -108,7 +183,11 @@ static void usage(void) "Usage: wolfcose_tool <command> [options]\n" "\n" "Commands:\n" +#if defined(WOLFCOSE_HAVE_HPKE_0) + " keygen -a <alg> -o <keyfile> [-p <public-keyfile>]\n" +#else " keygen -a <alg> -o <keyfile>\n" +#endif " sign -k <keyfile> -a <alg> -i <payload> -o <cose_file>\n" " verify -k <keyfile> -i <cose_file>\n" " countersign -k <keyfile> -a <alg> -i <cose_file>" @@ -116,6 +195,20 @@ static void usage(void) " counterverify -k <keyfile> -i <cose_file> [--index <n>]\n" " enc -k <keyfile> -a <alg> -i <plaintext> -o <cose_file>\n" " dec -k <keyfile> -i <cose_file> -o <plaintext>\n" +#if defined(WOLFCOSE_HPKE_0_ENCRYPT) + " hpke0-enc -k <public-key> -i <plaintext> -o <cose_file>\n" +#endif +#if defined(WOLFCOSE_HPKE_0_DECRYPT) + " hpke0-dec -k <private-key> -i <cose_file> -o <plaintext>\n" +#endif +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) + " hpke-ke-enc -a <alg> -k <public-key> [-k <public-key> ...]" + " -i <plaintext> -o <cose_file>\n" +#endif +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + " hpke-ke-dec -k <private-key> [-r <recipient-index>]" + " -i <cose_file> -o <plaintext>\n" +#endif " mac -k <keyfile> -a <alg> -i <payload> -o <cose_file>\n" " macverify -k <keyfile> -i <cose_file>\n" " info -i <cose_file>\n" @@ -125,6 +218,13 @@ static void usage(void) "Algorithms: ES256, EdDSA, Ed448, PS256, PS384, PS512,\n" " ML-DSA-44, ML-DSA-65, ML-DSA-87,\n" " A128GCM, A192GCM, A256GCM, ChaCha20, AES-CCM,\n" +#if defined(WOLFCOSE_HPKE_0_ENCRYPT) || defined(WOLFCOSE_HPKE_0_DECRYPT) + " HPKE-0,\n" +#endif +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) || \ + defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + " HPKE-0-KE,\n" +#endif " HMAC256, HMAC384, HMAC512\n"); } @@ -195,6 +295,17 @@ static int parse_alg(const char* name, int32_t* alg) else if (strcmp(name, "HMAC512") == 0) { *alg = WOLFCOSE_ALG_HMAC512; } +#endif +#if defined(WOLFCOSE_HPKE_0_ENCRYPT) || defined(WOLFCOSE_HPKE_0_DECRYPT) + else if (strcmp(name, "HPKE-0") == 0) { + *alg = WOLFCOSE_ALG_HPKE_0; + } +#endif +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) || \ + defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + else if (strcmp(name, "HPKE-0-KE") == 0) { + *alg = WOLFCOSE_ALG_HPKE_0_KE; + } #endif else { fprintf(stderr, "Unknown algorithm: %s\n", name); @@ -203,12 +314,13 @@ static int parse_alg(const char* name, int32_t* alg) return 0; } -/* Read entire file into buffer, return bytes read */ +/* Read an entire file into buffer, rejecting data beyond the caller's bound. */ static int read_file(const char* path, uint8_t* buf, size_t bufSz, size_t* outLen) { FILE* f; size_t n; + int extra = EOF; f = fopen(path, "rb"); if (f == NULL) { @@ -216,27 +328,19 @@ static int read_file(const char* path, uint8_t* buf, size_t bufSz, return EXIT_IO; } n = fread(buf, 1, bufSz, f); - if (n == 0 && ferror(f)) { + if (n == bufSz) { + extra = fgetc(f); + } + if (ferror(f)) { fclose(f); fprintf(stderr, "Read error: %s\n", path); return EXIT_IO; } - /* A full buffer with bytes still unread would otherwise be processed as a - * silently truncated message and fail later as malformed CBOR. */ - if (n == bufSz) { - int next = fgetc(f); - - if (ferror(f)) { - fclose(f); - fprintf(stderr, "Read error: %s\n", path); - return EXIT_IO; - } - if (next != EOF) { - fclose(f); - fprintf(stderr, "File too large (limit %zu bytes): %s\n", - bufSz, path); - return EXIT_IO; - } + if (extra != EOF) { + fclose(f); + fprintf(stderr, "File too large (limit %zu bytes): %s\n", + bufSz, path); + return EXIT_IO; } fclose(f); *outLen = n; @@ -262,6 +366,482 @@ static int write_file(const char* path, const uint8_t* buf, size_t len) return 0; } +#if defined(WOLFCOSE_HAVE_HPKE_0) +#if (WOLFCOSE_TOOL_HAVE_POSIX_FS == 1) +/* Resolve an output path without allowing a final symlink. Existing paths + * resolve fully; a new leaf is made canonical by resolving its parent. This + * lets the key generator reject aliases before either destination is created. */ +static int tool_hpke_canonical_output_path(const char* path, + char* canonical, size_t canonicalSz) +{ + char parent[WOLFCOSE_TOOL_PATH_MAX]; + char resolvedParent[WOLFCOSE_TOOL_PATH_MAX]; + const char* leaf; + const char* slash; + size_t parentLen; + size_t resolvedParentLen; + size_t leafLen; + struct stat pathStat; + int ret = 0; + + if ((path == NULL) || (path[0] == '\0') || (canonical == NULL) || + (canonicalSz == 0u)) { + ret = -1; + } + else if ((lstat(path, &pathStat) == 0) && S_ISLNK(pathStat.st_mode)) { + fprintf(stderr, "Refusing HPKE key output symlink: %s\n", path); + ret = -1; + } + else if (realpath(path, canonical) != NULL) { + /* Existing non-symlink path resolved successfully. */ + } + else { + slash = strrchr(path, '/'); + if (slash == NULL) { + parent[0] = '.'; + parent[1] = '\0'; + leaf = path; + } + else if (slash == path) { + parent[0] = '/'; + parent[1] = '\0'; + leaf = slash + 1; + } + else { + parentLen = (size_t)(slash - path); + if (parentLen >= sizeof(parent)) { + ret = -1; + } + else { + (void)XMEMCPY(parent, path, parentLen); + parent[parentLen] = '\0'; + leaf = slash + 1; + } + } + + if ((ret == 0) && (leaf[0] == '\0')) { + ret = -1; + } + if ((ret == 0) && (realpath(parent, resolvedParent) == NULL)) { + ret = -1; + } + if (ret == 0) { + resolvedParentLen = strlen(resolvedParent); + leafLen = strlen(leaf); + if ((resolvedParentLen > (SIZE_MAX - leafLen - 2u)) || + ((resolvedParentLen + leafLen + 2u) > canonicalSz)) { + ret = -1; + } + else if ((resolvedParentLen == 1u) && + (resolvedParent[0] == '/')) { + (void)XMEMCPY(canonical, resolvedParent, + resolvedParentLen); + (void)XMEMCPY(&canonical[resolvedParentLen], leaf, leafLen); + canonical[resolvedParentLen + leafLen] = '\0'; + } + else { + (void)XMEMCPY(canonical, resolvedParent, + resolvedParentLen); + canonical[resolvedParentLen] = '/'; + (void)XMEMCPY(&canonical[resolvedParentLen + 1u], leaf, + leafLen); + canonical[resolvedParentLen + leafLen + 1u] = '\0'; + } + } + } + + if (ret != 0) { + fprintf(stderr, "Invalid HPKE key output path: %s\n", path); + } + return ret; +} + +/* Return one when paths name distinct, new outputs, zero when they collide, + * and negative when either path cannot be resolved safely. For two absent + * leaves, create a temporary zero-length reservation and ask the filesystem + * whether the second spelling resolves to it. */ +static int tool_hpke_key_paths_distinct(const char* privatePath, + const char* publicPath) +{ + char privateCanonical[WOLFCOSE_TOOL_PATH_MAX]; + char publicCanonical[WOLFCOSE_TOOL_PATH_MAX]; + struct stat privateStat; + struct stat publicStat; + int privateExists; + int publicExists; + int reservationFd = -1; + int ret; + + ret = tool_hpke_canonical_output_path(privatePath, privateCanonical, + sizeof(privateCanonical)); + if ((ret == 0) && (publicPath != NULL)) { + ret = tool_hpke_canonical_output_path(publicPath, publicCanonical, + sizeof(publicCanonical)); + } + if (ret != 0) { + return -1; + } + if ((publicPath != NULL) && + (strcmp(privateCanonical, publicCanonical) == 0)) { + return 0; + } + privateExists = (stat(privateCanonical, &privateStat) == 0) ? 1 : 0; + if ((privateExists == 0) && (errno != ENOENT)) { + return -1; + } + if (privateExists != 0) { + return -1; + } + if (publicPath == NULL) { + return 1; + } + publicExists = (stat(publicCanonical, &publicStat) == 0) ? 1 : 0; + if ((publicExists == 0) && (errno != ENOENT)) { + return -1; + } + if (publicExists != 0) { + return -1; + } + + reservationFd = open(privateCanonical, O_WRONLY | O_CREAT | O_EXCL, 0600); + if (reservationFd < 0) { + return -1; + } + if (close(reservationFd) != 0) { + (void)unlink(privateCanonical); + return -1; + } + if (stat(publicCanonical, &publicStat) == 0) { + ret = ((stat(privateCanonical, &privateStat) == 0) && + (privateStat.st_dev == publicStat.st_dev) && + (privateStat.st_ino == publicStat.st_ino)) ? 0 : -1; + } + else if (errno == ENOENT) { + ret = 1; + } + else { + ret = -1; + } + if (unlink(privateCanonical) != 0) { + ret = -1; + } + if (ret == 0) { + return 0; + } + if (ret < 0) { + return -1; + } + return 1; +} + +static int tool_hpke_write_all(int fd, const uint8_t* buf, size_t len) +{ + size_t offset = 0u; + + while (offset < len) { + ssize_t written = write(fd, &buf[offset], len - offset); + + if (written > 0) { + offset += (size_t)written; + } + else if ((written < 0) && (errno == EINTR)) { + continue; + } + else { + return -1; + } + } + return 0; +} + +/* Stage a key in the target directory so a link creates only a complete file. */ +static int tool_hpke_stage_key(const char* outputPath, const uint8_t* buf, + size_t len, char* temporaryPath, size_t temporaryPathSz) +{ + static const char suffix[] = ".wolfcose-tmp.XXXXXX"; + size_t outputPathLen; + int fd; + int ret; + + if ((outputPath == NULL) || (temporaryPath == NULL) || + (temporaryPathSz < sizeof(suffix))) { + return -1; + } + temporaryPath[0] = '\0'; + outputPathLen = strlen(outputPath); + if (outputPathLen > (temporaryPathSz - sizeof(suffix))) { + return -1; + } + (void)XMEMCPY(temporaryPath, outputPath, outputPathLen); + (void)XMEMCPY(&temporaryPath[outputPathLen], suffix, sizeof(suffix)); + + fd = mkstemp(temporaryPath); + if (fd < 0) { + temporaryPath[0] = '\0'; + return -1; + } + ret = tool_hpke_write_all(fd, buf, len); + if ((ret == 0) && (fsync(fd) != 0)) { + ret = -1; + } + if (close(fd) != 0) { + ret = -1; + } + if (ret != 0) { + (void)unlink(temporaryPath); + temporaryPath[0] = '\0'; + } + return ret; +} + +/* Publish complete new key files without truncating any existing destination. */ +static int tool_hpke_write_key_pair(const char* privatePath, + const uint8_t* privateBuf, size_t privateLen, const char* publicPath, + const uint8_t* publicBuf, size_t publicLen) +{ + char privateCanonical[WOLFCOSE_TOOL_PATH_MAX]; + char publicCanonical[WOLFCOSE_TOOL_PATH_MAX]; + char privateTemporary[WOLFCOSE_TOOL_PATH_MAX]; + char publicTemporary[WOLFCOSE_TOOL_PATH_MAX]; + int privateInstalled = 0; + int publicInstalled = 0; + int ret = EXIT_IO; + + privateTemporary[0] = '\0'; + publicTemporary[0] = '\0'; + if (tool_hpke_canonical_output_path(privatePath, privateCanonical, + sizeof(privateCanonical)) != 0) { + ret = EXIT_IO; + } + else { + ret = 0; + } + if ((ret == 0) && (publicPath != NULL) && + (tool_hpke_canonical_output_path(publicPath, publicCanonical, + sizeof(publicCanonical)) != 0)) { + ret = EXIT_IO; + } + if ((ret == 0) && + (tool_hpke_stage_key(privateCanonical, privateBuf, privateLen, + privateTemporary, + sizeof(privateTemporary)) != 0)) { + ret = EXIT_IO; + } + if ((ret == 0) && (publicPath != NULL) && + (tool_hpke_stage_key(publicCanonical, publicBuf, publicLen, + publicTemporary, sizeof(publicTemporary)) != 0)) { + ret = EXIT_IO; + } + if ((ret == 0) && (link(privateTemporary, privateCanonical) != 0)) { + ret = EXIT_IO; + } + if (ret == 0) { + privateInstalled = 1; + } + if ((ret == 0) && (publicPath != NULL) && + (link(publicTemporary, publicCanonical) != 0)) { + ret = EXIT_IO; + } + if ((ret == 0) && (publicPath != NULL)) { + publicInstalled = 1; + } + + if ((ret != 0) && (publicInstalled != 0)) { + (void)unlink(publicCanonical); + } + if ((ret != 0) && (privateInstalled != 0)) { + (void)unlink(privateCanonical); + } + if (privateTemporary[0] != '\0') { + (void)unlink(privateTemporary); + } + if (publicTemporary[0] != '\0') { + (void)unlink(publicTemporary); + } + return ret; +} +#else +static int tool_hpke_key_paths_distinct(const char* privatePath, + const char* publicPath) +{ + if (privatePath == NULL) { + return -1; + } + if ((publicPath != NULL) && (strcmp(privatePath, publicPath) == 0)) { + return 0; + } + return 1; +} + +static int tool_hpke_write_key_pair(const char* privatePath, + const uint8_t* privateBuf, size_t privateLen, const char* publicPath, + const uint8_t* publicBuf, size_t publicLen) +{ + (void)publicBuf; + (void)publicLen; + if (publicPath != NULL) { + fprintf(stderr, "HPKE public key export requires POSIX filesystem support\n"); + return EXIT_USAGE; + } + return write_file(privatePath, privateBuf, privateLen); +} +#endif /* WOLFCOSE_TOOL_HAVE_POSIX_FS */ + +/* Generate a P-256 HPKE key pair and optionally export its public half. */ +static int tool_hpke_keygen(int32_t alg, const char* outPath, + const char* publicPath) +{ + WC_RNG rng; + ecc_key ecc; + WOLFCOSE_KEY coseKey; + uint8_t privateBuf[WOLFCOSE_TOOL_MAX_KEY]; + uint8_t publicBuf[WOLFCOSE_TOOL_MAX_KEY]; + size_t privateLen = 0u; + size_t publicLen = 0u; + int rngInit = 0; + int eccInit = 0; + int ret; + + if (outPath == NULL) { + fprintf(stderr, "HPKE key generation requires a private output path\n"); + return EXIT_USAGE; + } +#if (WOLFCOSE_TOOL_HAVE_POSIX_FS == 0) + if (publicPath != NULL) { + fprintf(stderr, "HPKE public key export requires POSIX filesystem support\n"); + return EXIT_USAGE; + } +#endif + + ret = tool_hpke_key_paths_distinct(outPath, publicPath); + if (ret <= 0) { + if (ret == 0) { + fprintf(stderr, "HPKE private and public key paths must differ\n"); + return EXIT_USAGE; + } + fprintf(stderr, "HPKE key output paths must be new and non-symlinked\n"); + return EXIT_IO; + } + + ret = wc_InitRng(&rng); + if (ret == 0) { + rngInit = 1; + ret = wc_ecc_init(&ecc); + } + if (ret == 0) { + eccInit = 1; + ret = wc_ecc_make_key(&rng, 32, &ecc); + } + if (ret == 0) { + wc_CoseKey_Init(&coseKey); + ret = wc_CoseKey_SetEcc(&coseKey, WOLFCOSE_CRV_P256, &ecc); + coseKey.alg = alg; + } + if (ret == 0) { + ret = wc_CoseKey_Encode(&coseKey, privateBuf, sizeof(privateBuf), + &privateLen); + } + if ((ret == 0) && (publicPath != NULL)) { + ret = wc_CoseKey_Encode_ex(&coseKey, publicBuf, sizeof(publicBuf), + &publicLen, WOLFCOSE_KEY_PUBLIC_ONLY); + } + + if (eccInit != 0) { + wc_ecc_free(&ecc); + } + if (rngInit != 0) { + wc_FreeRng(&rng); + } + if (ret != 0) { + fprintf(stderr, "HPKE key generation failed: %d\n", ret); + tool_force_zero(privateBuf, sizeof(privateBuf)); + tool_force_zero(publicBuf, sizeof(publicBuf)); + return EXIT_CRYPTO; + } + + ret = tool_hpke_write_key_pair(outPath, privateBuf, privateLen, + publicPath, publicBuf, publicLen); + tool_force_zero(privateBuf, sizeof(privateBuf)); + tool_force_zero(publicBuf, sizeof(publicBuf)); + if (ret == 0) { + printf("Generated HPKE private key: %s (%zu bytes)\n", outPath, + privateLen); + if (publicPath != NULL) { + printf("Generated HPKE public key: %s (%zu bytes)\n", publicPath, + publicLen); + } + } + return ret; +} + +/* Decode an EC2 P-256 COSE_Key and enforce its HPKE algorithm binding. */ +static int tool_hpke_load_key(const char* path, int32_t alg, + WOLFCOSE_KEY* coseKey, ecc_key* ecc) +{ + uint8_t keyBuf[WOLFCOSE_TOOL_MAX_KEY]; + size_t keyLen = 0u; + int eccInit = 0; + int ret; + + ret = read_file(path, keyBuf, sizeof(keyBuf), &keyLen); + if (ret != 0) { + tool_force_zero(keyBuf, sizeof(keyBuf)); + return ret; + } + wc_CoseKey_Init(coseKey); + ret = wc_ecc_init(ecc); + if (ret == 0) { + eccInit = 1; + ret = wc_CoseKey_SetEcc(coseKey, WOLFCOSE_CRV_P256, ecc); + } + if (ret == 0) { + ret = wc_CoseKey_Decode(coseKey, keyBuf, keyLen); + } + tool_force_zero(keyBuf, sizeof(keyBuf)); + if ((ret == 0) && (coseKey->alg != alg)) { + fprintf(stderr, "HPKE key has an unexpected algorithm binding\n"); + ret = WOLFCOSE_E_COSE_BAD_ALG; + } + if (ret != 0) { + fprintf(stderr, "HPKE key decode failed: %d\n", ret); + if (eccInit != 0) { + wc_ecc_free(ecc); + } + return EXIT_CRYPTO; + } + return 0; +} + +/* Return the nonce size required by the selected content encryption suite. */ +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) +static int tool_content_nonce_len(int32_t alg, size_t* nonceLen) +{ + if ((alg == WOLFCOSE_ALG_AES_CCM_16_64_128) || + (alg == WOLFCOSE_ALG_AES_CCM_16_64_256) || + (alg == WOLFCOSE_ALG_AES_CCM_16_128_128) || + (alg == WOLFCOSE_ALG_AES_CCM_16_128_256)) { + *nonceLen = 13u; + } + else if ((alg == WOLFCOSE_ALG_AES_CCM_64_64_128) || + (alg == WOLFCOSE_ALG_AES_CCM_64_64_256) || + (alg == WOLFCOSE_ALG_AES_CCM_64_128_128) || + (alg == WOLFCOSE_ALG_AES_CCM_64_128_256)) { + *nonceLen = 7u; + } + else if ((alg == WOLFCOSE_ALG_A128GCM) || + (alg == WOLFCOSE_ALG_A192GCM) || + (alg == WOLFCOSE_ALG_A256GCM) || + (alg == WOLFCOSE_ALG_CHACHA20_POLY1305)) { + *nonceLen = 12u; + } + else { + return EXIT_USAGE; + } + return 0; +} +#endif /* WOLFCOSE_HPKE_0_KE_ENCRYPT */ +#endif /* WOLFCOSE_HAVE_HPKE_0 */ + /* ----- keygen: generate a COSE key and write to file ----- */ static int tool_keygen(int32_t alg, const char* algStr, const char* outPath) { @@ -1194,6 +1774,249 @@ static int tool_dec(const char* keyPath, const char* inPath, } #endif /* WOLFCOSE_HAVE_AESGCM || WOLFCOSE_HAVE_AESCCM || (WOLFCOSE_HAVE_CHACHA20) */ +/* ----- hpke0-enc: COSE HPKE-0 integrated encryption ----- */ +#if defined(WOLFCOSE_HPKE_0_ENCRYPT) +static int tool_hpke0_enc(const char* keyPath, const char* inPath, + const char* outPath) +{ + WOLFCOSE_KEY recipientKey; + ecc_key recipientEcc; + WC_RNG rng; + uint8_t msgBuf[WOLFCOSE_TOOL_MAX_MSG]; + uint8_t outBuf[WOLFCOSE_TOOL_HPKE_0_ENCODED_MAX]; + uint8_t scratch[WOLFCOSE_MAX_SCRATCH_SZ]; + size_t msgLen = 0u; + size_t outLen = 0u; + int eccLoaded = 0; + int rngInit = 0; + int ret; + + ret = tool_hpke_load_key(keyPath, WOLFCOSE_ALG_HPKE_0, &recipientKey, + &recipientEcc); + if (ret != 0) { + return ret; + } + eccLoaded = 1; + ret = read_file(inPath, msgBuf, sizeof(msgBuf), &msgLen); + if (ret == 0) { + ret = wc_InitRng(&rng); + if (ret == 0) { + rngInit = 1; + ret = wc_CoseHpkeEncrypt0_Encrypt(&recipientKey, NULL, 0u, + msgBuf, msgLen, NULL, 0u, NULL, NULL, 0u, scratch, + sizeof(scratch), outBuf, sizeof(outBuf), &outLen, &rng); + } + if (ret != 0) { + fprintf(stderr, "HPKE-0 encrypt failed: %d\n", ret); + ret = EXIT_CRYPTO; + } + } + if (ret == 0) { + ret = write_file(outPath, outBuf, outLen); + } + if (ret == 0) { + printf("HPKE-0 encrypted: %zu byte plaintext -> %zu byte " + "COSE_Encrypt0\n", msgLen, outLen); + } + + if (rngInit != 0) { + wc_FreeRng(&rng); + } + if (eccLoaded != 0) { + wc_ecc_free(&recipientEcc); + } + tool_force_zero(msgBuf, sizeof(msgBuf)); + return ret; +} +#endif /* WOLFCOSE_HPKE_0_ENCRYPT */ + +/* ----- hpke0-dec: COSE HPKE-0 integrated decryption ----- */ +#if defined(WOLFCOSE_HPKE_0_DECRYPT) +static int tool_hpke0_dec(const char* keyPath, const char* inPath, + const char* outPath) +{ + WOLFCOSE_KEY recipientKey; + ecc_key recipientEcc; + WOLFCOSE_HDR hdr; + uint8_t msgBuf[WOLFCOSE_TOOL_HPKE_0_ENCODED_MAX]; + uint8_t plainBuf[WOLFCOSE_TOOL_MAX_MSG]; + uint8_t scratch[WOLFCOSE_MAX_SCRATCH_SZ]; + size_t msgLen = 0u; + size_t plainLen = 0u; + int eccLoaded = 0; + int ret; + + ret = tool_hpke_load_key(keyPath, WOLFCOSE_ALG_HPKE_0, &recipientKey, + &recipientEcc); + if (ret != 0) { + return ret; + } + eccLoaded = 1; + ret = read_file(inPath, msgBuf, sizeof(msgBuf), &msgLen); + if (ret == 0) { + ret = wc_CoseHpkeEncrypt0_Decrypt(&recipientKey, msgBuf, msgLen, + NULL, 0u, NULL, 0u, scratch, sizeof(scratch), &hdr, plainBuf, + sizeof(plainBuf), &plainLen); + if (ret != 0) { + fprintf(stderr, "HPKE-0 decrypt failed: %d\n", ret); + ret = EXIT_CRYPTO; + } + } + if (ret == 0) { + ret = write_file(outPath, plainBuf, plainLen); + } + if (ret == 0) { + printf("HPKE-0 decrypted: %zu byte COSE_Encrypt0 -> %zu byte " + "plaintext\n", msgLen, plainLen); + } + + if (eccLoaded != 0) { + wc_ecc_free(&recipientEcc); + } + tool_force_zero(plainBuf, sizeof(plainBuf)); + return ret; +} +#endif /* WOLFCOSE_HPKE_0_DECRYPT */ + +/* ----- hpke-ke-enc: HPKE-0-KE multi-recipient COSE_Encrypt ----- */ +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) +static int tool_hpke_ke_enc(const char* const* keyPaths, size_t keyCount, + int32_t contentAlg, const char* inPath, + const char* outPath) +{ + WOLFCOSE_KEY recipientKey[WOLFCOSE_TOOL_MAX_HPKE_RECIPIENTS]; + WOLFCOSE_RECIPIENT recipients[WOLFCOSE_TOOL_MAX_HPKE_RECIPIENTS]; + ecc_key recipientEcc[WOLFCOSE_TOOL_MAX_HPKE_RECIPIENTS]; + WC_RNG rng; + uint8_t msgBuf[WOLFCOSE_TOOL_MAX_MSG]; + uint8_t outBuf[WOLFCOSE_TOOL_HPKE_0_KE_ENCODED_MAX]; + uint8_t scratch[WOLFCOSE_MAX_SCRATCH_SZ]; + uint8_t iv[13]; + size_t msgLen = 0u; + size_t outLen = 0u; + size_t ivLen = 0u; + size_t eccCount = 0u; + size_t i; + int rngInit = 0; + int ret; + + if ((keyCount == 0u) || (keyCount > WOLFCOSE_TOOL_MAX_HPKE_RECIPIENTS)) { + fprintf(stderr, "HPKE-0-KE supports 1 to %u recipient keys\n", + (unsigned int)WOLFCOSE_TOOL_MAX_HPKE_RECIPIENTS); + return EXIT_USAGE; + } + ret = tool_content_nonce_len(contentAlg, &ivLen); + if (ret != 0) { + fprintf(stderr, "Unsupported HPKE-0-KE content algorithm\n"); + return ret; + } + ret = read_file(inPath, msgBuf, sizeof(msgBuf), &msgLen); + if (ret != 0) { + tool_force_zero(msgBuf, sizeof(msgBuf)); + return ret; + } + (void)XMEMSET(recipients, 0, sizeof(recipients)); + for (i = 0u; (ret == 0) && (i < keyCount); i++) { + ret = tool_hpke_load_key(keyPaths[i], WOLFCOSE_ALG_HPKE_0_KE, + &recipientKey[i], &recipientEcc[i]); + if (ret == 0) { + eccCount++; + recipients[i].algId = WOLFCOSE_ALG_HPKE_0_KE; + recipients[i].key = &recipientKey[i]; + } + } + if (ret == 0) { + ret = wc_InitRng(&rng); + if (ret == 0) { + rngInit = 1; + ret = wc_RNG_GenerateBlock(&rng, iv, (word32)ivLen); + } + if (ret != 0) { + fprintf(stderr, "HPKE-0-KE RNG failed: %d\n", ret); + ret = EXIT_CRYPTO; + } + } + if (ret == 0) { + ret = wc_CoseEncrypt_Encrypt(recipients, keyCount, contentAlg, iv, + ivLen, msgBuf, msgLen, NULL, 0u, NULL, 0u, scratch, + sizeof(scratch), outBuf, sizeof(outBuf), &outLen, &rng); + if (ret != 0) { + fprintf(stderr, "HPKE-0-KE encrypt failed: %d\n", ret); + ret = EXIT_CRYPTO; + } + } + if (ret == 0) { + ret = write_file(outPath, outBuf, outLen); + } + if (ret == 0) { + printf("HPKE-0-KE encrypted: %zu byte plaintext -> %zu byte " + "COSE_Encrypt (%zu recipients)\n", msgLen, outLen, keyCount); + } + + if (rngInit != 0) { + wc_FreeRng(&rng); + } + while (eccCount > 0u) { + eccCount--; + wc_ecc_free(&recipientEcc[eccCount]); + } + tool_force_zero(msgBuf, sizeof(msgBuf)); + return ret; +} +#endif /* WOLFCOSE_HPKE_0_KE_ENCRYPT */ + +/* ----- hpke-ke-dec: HPKE-0-KE multi-recipient COSE_Encrypt decryption ----- */ +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) +static int tool_hpke_ke_dec(const char* keyPath, size_t recipientIndex, + const char* inPath, const char* outPath) +{ + WOLFCOSE_KEY recipientKey; + WOLFCOSE_RECIPIENT recipient; + WOLFCOSE_HDR hdr; + ecc_key recipientEcc; + uint8_t msgBuf[WOLFCOSE_TOOL_HPKE_0_KE_ENCODED_MAX]; + uint8_t plainBuf[WOLFCOSE_TOOL_MAX_MSG]; + uint8_t scratch[WOLFCOSE_MAX_SCRATCH_SZ]; + size_t msgLen = 0u; + size_t plainLen = 0u; + int eccLoaded = 0; + int ret; + + ret = tool_hpke_load_key(keyPath, WOLFCOSE_ALG_HPKE_0_KE, &recipientKey, + &recipientEcc); + if (ret != 0) { + return ret; + } + eccLoaded = 1; + ret = read_file(inPath, msgBuf, sizeof(msgBuf), &msgLen); + if (ret == 0) { + (void)XMEMSET(&recipient, 0, sizeof(recipient)); + recipient.algId = WOLFCOSE_ALG_HPKE_0_KE; + recipient.key = &recipientKey; + ret = wc_CoseEncrypt_Decrypt(&recipient, recipientIndex, msgBuf, + msgLen, NULL, 0u, NULL, 0u, scratch, sizeof(scratch), &hdr, + plainBuf, sizeof(plainBuf), &plainLen); + if (ret != 0) { + fprintf(stderr, "HPKE-0-KE decrypt failed: %d\n", ret); + ret = EXIT_CRYPTO; + } + } + if (ret == 0) { + ret = write_file(outPath, plainBuf, plainLen); + } + if (ret == 0) { + printf("HPKE-0-KE decrypted recipient %zu: %zu byte COSE_Encrypt -> " + "%zu byte plaintext\n", recipientIndex, msgLen, plainLen); + } + + if (eccLoaded != 0) { + wc_ecc_free(&recipientEcc); + } + tool_force_zero(plainBuf, sizeof(plainBuf)); + return ret; +} +#endif /* WOLFCOSE_HPKE_0_KE_DECRYPT */ + /* ----- mac: COSE_Mac0 create ----- */ #if defined(WOLFCOSE_HAVE_HMAC) static int tool_mac(const char* keyPath, int32_t alg, @@ -1285,7 +2108,7 @@ static int tool_macverify(const char* keyPath, const char* inPath) static int tool_info(const char* inPath) { int ret; - uint8_t msgBuf[WOLFCOSE_TOOL_MAX_MSG]; + uint8_t msgBuf[WOLFCOSE_TOOL_MAX_COSE_MSG]; size_t msgLen = 0; WOLFCOSE_CBOR_CTX ctx; WOLFCOSE_CBOR_ITEM item; @@ -1297,9 +2120,11 @@ static int tool_info(const char* inPath) printf("COSE message: %zu bytes\n", msgLen); - ctx.buf = msgBuf; - ctx.bufSz = msgLen; - ctx.idx = 0; + ret = wc_CBOR_DecoderInit(&ctx, msgBuf, msgLen); + if (ret != 0) { + fprintf(stderr, "CBOR decoder initialization failed: %d\n", ret); + return EXIT_CRYPTO; + } while (ctx.idx < ctx.bufSz) { size_t pos = ctx.idx; @@ -1726,6 +2551,154 @@ static int test_enc_roundtrip(const char* name, int32_t alg, } #endif +/* HPKE-0 integrated-encryption round trip. */ +#if defined(WOLFCOSE_HPKE_0_ENCRYPT) && defined(WOLFCOSE_HPKE_0_DECRYPT) +static int test_hpke0_roundtrip(void) +{ + static const uint8_t payload[] = "wolfCOSE HPKE-0 roundtrip"; + WC_RNG rng; + ecc_key recipientEcc; + WOLFCOSE_KEY recipientKey; + WOLFCOSE_HDR hdr; + uint8_t scratch[WOLFCOSE_MAX_SCRATCH_SZ]; + uint8_t cose[512]; + uint8_t plaintext[sizeof(payload)]; + size_t coseLen = 0u; + size_t plaintextLen = 0u; + int rngInit = 0; + int eccInit = 0; + int ret; + + printf(" %-12s enc/dec ... ", "HPKE-0"); + ret = wc_InitRng(&rng); + if (ret == 0) { + rngInit = 1; + ret = wc_ecc_init(&recipientEcc); + } + if (ret == 0) { + eccInit = 1; + ret = wc_ecc_make_key(&rng, 32, &recipientEcc); + } + if (ret == 0) { + wc_CoseKey_Init(&recipientKey); + ret = wc_CoseKey_SetEcc(&recipientKey, WOLFCOSE_CRV_P256, + &recipientEcc); + recipientKey.alg = WOLFCOSE_ALG_HPKE_0; + recipientKey.hasPrivate = 0u; + } + if (ret == 0) { + ret = wc_CoseHpkeEncrypt0_Encrypt(&recipientKey, NULL, 0u, payload, + sizeof(payload) - 1u, NULL, 0u, NULL, NULL, 0u, scratch, + sizeof(scratch), cose, sizeof(cose), &coseLen, &rng); + } + if (ret == 0) { + recipientKey.hasPrivate = 1u; + ret = wc_CoseHpkeEncrypt0_Decrypt(&recipientKey, cose, coseLen, + NULL, 0u, NULL, 0u, scratch, sizeof(scratch), &hdr, plaintext, + sizeof(plaintext), &plaintextLen); + } + if ((ret == 0) && + ((hdr.alg != WOLFCOSE_ALG_HPKE_0) || + (plaintextLen != (sizeof(payload) - 1u)) || + (memcmp(plaintext, payload, plaintextLen) != 0))) { + ret = -1; + } + if (eccInit != 0) { + wc_ecc_free(&recipientEcc); + } + if (rngInit != 0) { + wc_FreeRng(&rng); + } + printf("%s\n", ret == 0 ? "PASS" : "FAIL"); + return ret; +} +#endif /* WOLFCOSE_HPKE_0_ENCRYPT && WOLFCOSE_HPKE_0_DECRYPT */ + +/* HPKE-0-KE uses one independently protected CEK for each recipient. */ +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) && \ + defined(WOLFCOSE_HPKE_0_KE_DECRYPT) +static int test_hpke_ke_roundtrip(void) +{ + enum { HPKE_TEST_RECIPIENTS = 2 }; + static const uint8_t payload[] = "wolfCOSE HPKE-0-KE roundtrip"; + WC_RNG rng; + ecc_key recipientEcc[HPKE_TEST_RECIPIENTS]; + WOLFCOSE_KEY recipientKey[HPKE_TEST_RECIPIENTS]; + WOLFCOSE_RECIPIENT recipients[HPKE_TEST_RECIPIENTS]; + WOLFCOSE_HDR hdr; + uint8_t iv[12]; + uint8_t scratch[WOLFCOSE_MAX_SCRATCH_SZ]; + uint8_t cose[1024]; + uint8_t plaintext[sizeof(payload)]; + size_t coseLen = 0u; + size_t plaintextLen = 0u; + size_t eccCount = 0u; + size_t i; + int rngInit = 0; + int ret; + + printf(" %-12s enc/dec ... ", "HPKE-0-KE"); + (void)memset(recipients, 0, sizeof(recipients)); + ret = wc_InitRng(&rng); + if (ret == 0) { + rngInit = 1; + } + for (i = 0u; (ret == 0) && (i < HPKE_TEST_RECIPIENTS); i++) { + ret = wc_ecc_init(&recipientEcc[i]); + if (ret == 0) { + eccCount++; + ret = wc_ecc_make_key(&rng, 32, &recipientEcc[i]); + } + if (ret == 0) { + wc_CoseKey_Init(&recipientKey[i]); + ret = wc_CoseKey_SetEcc(&recipientKey[i], WOLFCOSE_CRV_P256, + &recipientEcc[i]); + recipientKey[i].alg = WOLFCOSE_ALG_HPKE_0_KE; + recipientKey[i].hasPrivate = 0u; + } + if (ret == 0) { + recipients[i].algId = WOLFCOSE_ALG_HPKE_0_KE; + recipients[i].key = &recipientKey[i]; + } + } + if (ret == 0) { + ret = wc_RNG_GenerateBlock(&rng, iv, (word32)sizeof(iv)); + } + if (ret == 0) { + ret = wc_CoseEncrypt_Encrypt(recipients, HPKE_TEST_RECIPIENTS, + WOLFCOSE_ALG_A128GCM, iv, sizeof(iv), payload, + sizeof(payload) - 1u, NULL, 0u, NULL, 0u, scratch, + sizeof(scratch), cose, sizeof(cose), &coseLen, &rng); + } + if (ret == 0) { + for (i = 0u; i < HPKE_TEST_RECIPIENTS; i++) { + recipientKey[i].hasPrivate = 1u; + } + } + for (i = 0u; (ret == 0) && (i < HPKE_TEST_RECIPIENTS); i++) { + plaintextLen = 0u; + ret = wc_CoseEncrypt_Decrypt(&recipients[i], i, cose, coseLen, + NULL, 0u, NULL, 0u, scratch, sizeof(scratch), &hdr, plaintext, + sizeof(plaintext), &plaintextLen); + if ((ret == 0) && + ((hdr.alg != WOLFCOSE_ALG_A128GCM) || + (plaintextLen != (sizeof(payload) - 1u)) || + (memcmp(plaintext, payload, plaintextLen) != 0))) { + ret = -1; + } + } + while (eccCount > 0u) { + eccCount--; + wc_ecc_free(&recipientEcc[eccCount]); + } + if (rngInit != 0) { + wc_FreeRng(&rng); + } + printf("%s\n", ret == 0 ? "PASS" : "FAIL"); + return ret; +} +#endif /* WOLFCOSE_HPKE_0_KE_ENCRYPT && WOLFCOSE_HPKE_0_KE_DECRYPT */ + /* MAC round-trip: keygen -> mac -> macverify -> check payload */ #if defined(WOLFCOSE_HAVE_HMAC) static int test_mac_roundtrip(const char* name, int32_t alg, size_t keyLen) @@ -1871,6 +2844,25 @@ static int tool_test(const char* filter) } #endif + /* --- Experimental COSE-HPKE --- */ +#if defined(WOLFCOSE_HPKE_0_ENCRYPT) && defined(WOLFCOSE_HPKE_0_DECRYPT) + if (all || strcmp(filter, "HPKE-0") == 0) { + tests++; + if (test_hpke0_roundtrip() != 0) { + failures++; + } + } +#endif +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) && \ + defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + if (all || strcmp(filter, "HPKE-0-KE") == 0) { + tests++; + if (test_hpke_ke_roundtrip() != 0) { + failures++; + } + } +#endif + /* --- COSE_Mac0 --- */ #if defined(WOLFCOSE_HAVE_HMAC) #ifdef WOLFCOSE_HAVE_HMAC256 @@ -1915,12 +2907,25 @@ int main(int argc, char* argv[]) const char* cmd; const char* algStr = NULL; const char* keyPath = NULL; +#if defined(WOLFCOSE_HAVE_HPKE_0) + const char* publicPath = NULL; +#endif const char* inPath = NULL; const char* outPath = NULL; const char* detachedPath = NULL; const char* aadPath = NULL; size_t counterIndex = 0u; int counterIndexSet = 0; +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) + const char* keyPaths[WOLFCOSE_TOOL_MAX_HPKE_RECIPIENTS]; +#endif +#if defined(WOLFCOSE_HAVE_HPKE_0) + size_t keyPathCount = 0u; +#endif +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + size_t recipientIndex = 0u; + int recipientIndexSet = 0; +#endif int32_t alg = 0; int i; @@ -1942,6 +2947,45 @@ int main(int argc, char* argv[]) } else if (strcmp(argv[i], "-k") == 0) { keyPath = argv[++i]; +#if defined(WOLFCOSE_HAVE_HPKE_0) + if (keyPathCount >= WOLFCOSE_TOOL_MAX_HPKE_RECIPIENTS) { + fprintf(stderr, "Too many -k recipient keys\n"); + return EXIT_USAGE; + } +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) + keyPaths[keyPathCount] = keyPath; +#endif + keyPathCount++; +#endif + } +#if defined(WOLFCOSE_HAVE_HPKE_0) + else if (strcmp(argv[i], "-p") == 0) { + if (strcmp(cmd, "keygen") == 0) { + publicPath = argv[++i]; + } + else { + detachedPath = argv[++i]; + } + } +#endif + else if (strcmp(argv[i], "-r") == 0) { +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + char* end = NULL; + unsigned long value; + + errno = 0; + value = strtoul(argv[++i], &end, 10); + if ((errno != 0) || (end == argv[i]) || (*end != '\0') || + (value >= WOLFCOSE_TOOL_MAX_HPKE_RECIPIENTS)) { + fprintf(stderr, "Invalid recipient index: %s\n", argv[i]); + return EXIT_USAGE; + } + recipientIndex = (size_t)value; + recipientIndexSet = 1; +#else + fprintf(stderr, "HPKE recipient selection is not built in\n"); + return EXIT_USAGE; +#endif } else if (strcmp(argv[i], "-i") == 0) { inPath = argv[++i]; @@ -1949,9 +2993,11 @@ int main(int argc, char* argv[]) else if (strcmp(argv[i], "-o") == 0) { outPath = argv[++i]; } +#if !defined(WOLFCOSE_HAVE_HPKE_0) else if (strcmp(argv[i], "-p") == 0) { detachedPath = argv[++i]; } +#endif else if (strcmp(argv[i], "--aad") == 0) { aadPath = argv[++i]; } @@ -1994,6 +3040,12 @@ int main(int argc, char* argv[]) fprintf(stderr, "--index applies only to counterverify\n"); return EXIT_USAGE; } +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + if ((recipientIndexSet != 0) && (strcmp(cmd, "hpke-ke-dec") != 0)) { + fprintf(stderr, "-r applies only to hpke-ke-dec\n"); + return EXIT_USAGE; + } +#endif /* Dispatch */ if (strcmp(cmd, "test") == 0) { @@ -2004,6 +3056,16 @@ int main(int argc, char* argv[]) fprintf(stderr, "keygen requires -a <alg> -o <keyfile>\n"); return EXIT_USAGE; } +#if defined(WOLFCOSE_HAVE_HPKE_0) + if ((alg == WOLFCOSE_ALG_HPKE_0) || + (alg == WOLFCOSE_ALG_HPKE_0_KE)) { + return tool_hpke_keygen(alg, outPath, publicPath); + } + if (publicPath != NULL) { + fprintf(stderr, "-p is only valid for HPKE key generation\n"); + return EXIT_USAGE; + } +#endif return tool_keygen(alg, algStr, outPath); } else if (strcmp(cmd, "sign") == 0) { @@ -2043,6 +3105,52 @@ int main(int argc, char* argv[]) detachedPath, aadPath); } #endif +#if defined(WOLFCOSE_HPKE_0_ENCRYPT) + else if (strcmp(cmd, "hpke0-enc") == 0) { + if ((keyPath == NULL) || (keyPathCount != 1u) || (inPath == NULL) || + (outPath == NULL)) { + fprintf(stderr, + "hpke0-enc requires -k <public-key> -i <input> -o <output>\n"); + return EXIT_USAGE; + } + return tool_hpke0_enc(keyPath, inPath, outPath); + } +#endif +#if defined(WOLFCOSE_HPKE_0_DECRYPT) + else if (strcmp(cmd, "hpke0-dec") == 0) { + if ((keyPath == NULL) || (keyPathCount != 1u) || (inPath == NULL) || + (outPath == NULL)) { + fprintf(stderr, + "hpke0-dec requires -k <private-key> -i <input> -o <output>\n"); + return EXIT_USAGE; + } + return tool_hpke0_dec(keyPath, inPath, outPath); + } +#endif +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) + else if (strcmp(cmd, "hpke-ke-enc") == 0) { + if ((keyPathCount == 0u) || (algStr == NULL) || (inPath == NULL) || + (outPath == NULL)) { + fprintf(stderr, + "hpke-ke-enc requires -a <alg> -k <public-key> " + "-i <input> -o <output>\n"); + return EXIT_USAGE; + } + return tool_hpke_ke_enc(keyPaths, keyPathCount, alg, inPath, outPath); + } +#endif +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + else if (strcmp(cmd, "hpke-ke-dec") == 0) { + if ((keyPath == NULL) || (keyPathCount != 1u) || (inPath == NULL) || + (outPath == NULL)) { + fprintf(stderr, + "hpke-ke-dec requires -k <private-key> -i <input> " + "-o <output>\n"); + return EXIT_USAGE; + } + return tool_hpke_ke_dec(keyPath, recipientIndex, inPath, outPath); + } +#endif #if defined(WOLFCOSE_HAVE_HMAC) else if (strcmp(cmd, "mac") == 0) { if (keyPath == NULL || algStr == NULL || inPath == NULL ||