From d720af1c4483a5c777c10b0e8e973ce39e4fb81e Mon Sep 17 00:00:00 2001 From: David Garske Date: Fri, 10 Jul 2026 09:23:43 -0700 Subject: [PATCH] macsec: clean-room 802.1AE software SecY with wolfDen wolfMKA control plane --- .github/workflows/supplicant.yml | 62 ++- Makefile | 113 ++++- src/macsec/fuzz_macsec_secy.c | 67 +++ src/macsec/macsec_crypto.c | 355 ++++++++++++++++ src/macsec/macsec_crypto.h | 157 +++++++ src/macsec/macsec_sa.c | 181 ++++++++ src/macsec/macsec_sa.h | 117 ++++++ src/macsec/macsec_secy.c | 340 +++++++++++++++ src/macsec/macsec_secy.h | 139 +++++++ src/macsec/macsec_test.h | 76 ++++ src/macsec/mka_wolfmka.c | 390 ++++++++++++++++++ src/macsec/mka_wolfmka.h | 114 +++++ src/macsec/test_macsec_crypto.c | 243 +++++++++++ src/macsec/test_macsec_sa.c | 224 ++++++++++ src/macsec/test_macsec_secy.c | 236 +++++++++++ src/port/rp2350_cyw43439/test_cyw43_sdpcm.c | 4 +- src/supplicant/eap.h | 1 + src/supplicant/supplicant_features.h | 12 + tools/macsec/macsec_probe.c | 164 ++++++++ tools/macsec/macsec_sta.c | 293 +++++++++++++ tools/macsec/run_macsec_mka_dataplane_test.sh | 120 ++++++ tools/macsec/run_macsec_mka_test.sh | 87 ++++ tools/macsec/run_macsec_static_test.sh | 163 ++++++++ 23 files changed, 3653 insertions(+), 5 deletions(-) create mode 100644 src/macsec/fuzz_macsec_secy.c create mode 100644 src/macsec/macsec_crypto.c create mode 100644 src/macsec/macsec_crypto.h create mode 100644 src/macsec/macsec_sa.c create mode 100644 src/macsec/macsec_sa.h create mode 100644 src/macsec/macsec_secy.c create mode 100644 src/macsec/macsec_secy.h create mode 100644 src/macsec/macsec_test.h create mode 100644 src/macsec/mka_wolfmka.c create mode 100644 src/macsec/mka_wolfmka.h create mode 100644 src/macsec/test_macsec_crypto.c create mode 100644 src/macsec/test_macsec_sa.c create mode 100644 src/macsec/test_macsec_secy.c create mode 100644 tools/macsec/macsec_probe.c create mode 100644 tools/macsec/macsec_sta.c create mode 100644 tools/macsec/run_macsec_mka_dataplane_test.sh create mode 100644 tools/macsec/run_macsec_mka_test.sh create mode 100644 tools/macsec/run_macsec_static_test.sh diff --git a/.github/workflows/supplicant.yml b/.github/workflows/supplicant.yml index e5eaff5d..0f8da37e 100644 --- a/.github/workflows/supplicant.yml +++ b/.github/workflows/supplicant.yml @@ -9,6 +9,8 @@ on: - 'src/supplicant/**' - 'tools/hostapd/**' - 'tools/wolfsta/**' + - 'src/macsec/**' + - 'tools/macsec/**' - 'Makefile' - '.github/workflows/supplicant.yml' pull_request: @@ -17,6 +19,8 @@ on: - 'src/supplicant/**' - 'tools/hostapd/**' - 'tools/wolfsta/**' + - 'src/macsec/**' + - 'tools/macsec/**' - 'Makefile' - '.github/workflows/supplicant.yml' @@ -30,6 +34,9 @@ env: # never cache-hit). Bump to refresh. WOLFSSL_REF: v5.9.1-stable WOLFSSL_PREFIX: /home/runner/wolfssl-install + # Build the whole suite with MACsec/MKA on so every object is compiled once + # with a consistent flag set (CMAC/GCM/AES-key-wrap come from --enable-all). + WOLFIP_ENABLE_MACSEC: 1 jobs: supplicant: @@ -45,7 +52,7 @@ jobs: run: | sudo apt-get update sudo apt-get install -y build-essential autoconf automake libtool pkg-config \ - hostapd dnsmasq socat iw rfkill \ + hostapd wpasupplicant dnsmasq socat iw rfkill tcpdump \ libnl-3-dev libnl-genl-3-dev # mac80211_hwsim lives in linux-modules-extra; best-effort. sudo apt-get install -y "linux-modules-extra-$(uname -r)" || true @@ -80,8 +87,10 @@ jobs: # against the cached wolfSSL (no sudo install needed; rpath is absolute # so the binaries resolve the lib even under sudo). - # Hard gate: in-process unit tests (no root / no radio). - - name: Supplicant unit tests + # Hard gate: in-process unit tests (no root / no radio). With + # WOLFIP_ENABLE_MACSEC=1 in env this also builds and runs the MACsec/MKA + # crypto, SecY, SA and protocol tests. + - name: Supplicant + MACsec unit tests run: make supplicant-tests # PEAP/MSCHAPv2 is off by default (pulls in deprecated MD4 + DES); the @@ -148,5 +157,52 @@ jobs: sudo ./tools/hostapd/run_hwsim_wolfsta_dhcp_test.sh sudo ./tools/hostapd/run_hwsim_wolfsta_dhcp_psk_test.sh + # Best-effort: MACsec interop needs the kernel `macsec` module (in-tree + # in mainline, but not loadable on every hosted runner). Probe and skip + # cleanly. Unlike hwsim this needs no radio, only veth. + - name: Probe kernel macsec + id: macsec + run: | + if sudo modprobe macsec 2>/dev/null; then + echo "available=yes" >> "$GITHUB_OUTPUT" + else + echo "available=no" >> "$GITHUB_OUTPUT" + echo "::warning::kernel macsec module unavailable; skipping macsec interop" + fi + + - name: Build MACsec SecY interop tool + if: steps.macsec.outputs.available == 'yes' + run: make build/macsec-probe + + # SecY framing vs the kernel MACsec module, both directions (static SAK). + - name: MACsec SecY kernel interop + if: steps.macsec.outputs.available == 'yes' + run: sudo bash tools/macsec/run_macsec_static_test.sh + + # NOTE: MKA control-plane interop (run_macsec_mka_test.sh / + # run_macsec_mka_dataplane_test.sh) needs the wolfDen wolfMKA library + # (WOLFMKA_DIR) with the L2-ICV interop fix (wolfSSL/wolfDen PR #2); it + # returns to CI once that library is available upstream. + + # Hard gate: the in-process unit tests rebuilt with AddressSanitizer + + # UBSan, to catch memory / undefined-behaviour bugs in the parsers. + # Runs from a clean tree (sanitizer objects must not be reused); placed + # last so it does not disturb the normal-build interop steps above. + - name: Supplicant + MACsec unit tests (ASan + UBSan) + run: | + sudo sysctl vm.mmap_rnd_bits=28 || true + make clean + make supplicant-tests-sanitize + + # Hard gate: short libFuzzer smoke over the SecY receive path. Needs + # clang for -fsanitize=fuzzer. + - name: MACsec SecY fuzz smoke + run: | + if ! command -v clang >/dev/null; then + echo "::warning::clang unavailable; skipping fuzz smoke"; exit 0 + fi + make macsec-fuzz + ./build/fuzz-macsec-secy -max_total_time=30 -rss_limit_mb=4096 + # NOTE: P-521 hunt-and-peck (run_hwsim_sae_softmac_g21_test.sh) has an # open hostapd interop gap - kept as a make target, not yet in CI. diff --git a/Makefile b/Makefile index ba1760bb..010318b3 100644 --- a/Makefile +++ b/Makefile @@ -284,6 +284,7 @@ WOLFIP_ENABLE_PEAP_MSCHAPV2 ?= 0 WOLFIP_ENABLE_SAE ?= 1 WOLFIP_ENABLE_SAE_H2E ?= 1 WOLFIP_ENABLE_SAE_HNP ?= 1 +WOLFIP_ENABLE_MACSEC ?= 0 ifneq ($(WOLFSSL_PREFIX),) WOLFSSL_CFLAGS := -I$(WOLFSSL_PREFIX)/include @@ -335,7 +336,40 @@ $(error WOLFIP_ENABLE_SAE_H2E=1 requires WOLFIP_ENABLE_SAE=1) endif endif -SUPPLICANT_OBJ := $(patsubst src/%.c,build/%.o,$(SUPPLICANT_SRC)) +# MACsec / MKA (IEEE 802.1AE + 802.1X-2010). Standalone module under +# src/macsec/ that reuses the supplicant crypto layer (AES key wrap, secure +# zero). Requires wolfSSL built with --enable-cmac (WOLFSSL_CMAC). +# The software SecY (macsec_crypto/macsec_secy/macsec_sa) is always built with +# MACsec. The MKA control plane is provided by the wolfDen wolfMKA library +# (https://github.com/wolfSSL/wolfDen/tree/main/mka) from a local clone (not a +# submodule): set WOLFMKA_DIR to build it. Without it, only the SecY data plane +# (usable with a static SAK) is built. +WOLFMKA_DIR ?= +WOLFMKA_OBJ := +WOLFIP_HAVE_WOLFMKA := + +ifeq ($(WOLFIP_ENABLE_MACSEC),1) +SUPPLICANT_SRC += src/macsec/macsec_crypto.c \ + src/macsec/macsec_secy.c \ + src/macsec/macsec_sa.c +CFLAGS += -DWOLFIP_ENABLE_MACSEC=1 -Isrc/macsec +ifneq ($(WOLFMKA_DIR),) +ifeq ($(wildcard $(WOLFMKA_DIR)/include/wolfmka/mka_kay.h),) +$(error WOLFMKA_DIR is set but does not point at a wolfMKA clone (got '$(WOLFMKA_DIR)')) +endif +WOLFIP_HAVE_WOLFMKA := 1 +SUPPLICANT_SRC += src/macsec/mka_wolfmka.c +# WOLFMKA_ICV_L2_ADDR makes wolfMKA include the L2 addresses (DA||SA||EtherType) +# in the MKA ICV, matching wpa_supplicant / the Linux kernel (802.1X-2010 +# 11.11.3); required to interoperate with them (wolfSSL/wolfDen PR #2). +CFLAGS += -DWOLFIP_MKA_WOLFMKA=1 -DWOLFMKA_ICV_L2_ADDR -I$(WOLFMKA_DIR)/include +# wolfMKA library objects live outside src/, compiled to build/wolfmka/. +WOLFMKA_OBJ := build/wolfmka/mka_crypto.o build/wolfmka/mka_kay.o \ + build/wolfmka/mka_pdu.o build/wolfmka/mka_status.o +endif +endif + +SUPPLICANT_OBJ := $(patsubst src/%.c,build/%.o,$(SUPPLICANT_SRC)) $(WOLFMKA_OBJ) # Header-dependency tracking for the supplicant + its host glue. Without # this, editing a supplicant header (e.g. a struct in supplicant.h) would @@ -351,6 +385,17 @@ build/supplicant/%.o: src/supplicant/%.c @echo "[CC] $<" @$(CC) $(CFLAGS) $(WOLFSSL_CFLAGS) $(NL80211_CFLAGS) -MMD -MP -Isrc/supplicant -c $< -o $@ +build/macsec/%.o: src/macsec/%.c + @mkdir -p `dirname $@` || true + @echo "[CC] $<" + @$(CC) $(CFLAGS) $(WOLFSSL_CFLAGS) -MMD -MP -Isrc/supplicant -Isrc/macsec -c $< -o $@ + +# wolfMKA library sources from the local clone ($(WOLFMKA_DIR)/src). +build/wolfmka/%.o: $(WOLFMKA_DIR)/src/%.c + @mkdir -p build/wolfmka || true + @echo "[CC] $<" + @$(CC) $(CFLAGS) $(WOLFSSL_CFLAGS) -I$(WOLFMKA_DIR)/include -c $< -o $@ + # WOLFSSL_LIBS / WOLFSSL_CFLAGS may already be set above when # WOLFSSL_PREFIX is provided. Otherwise default to pkg-config detection # and a plain -lwolfssl fallback. @@ -368,6 +413,59 @@ build/test-wpa-crypto: $(SUPPLICANT_OBJ) build/supplicant/test_wpa_crypto.o @echo "[LD] $@" @$(CC) $(CFLAGS) -o $@ $(BEGIN_GROUP) $(^) $(LDFLAGS) $(WOLFSSL_LIBS) $(END_GROUP) +build/test-macsec-crypto: $(SUPPLICANT_OBJ) build/macsec/test_macsec_crypto.o + @echo "[LD] $@" + @$(CC) $(CFLAGS) -o $@ $(BEGIN_GROUP) $(^) $(LDFLAGS) $(WOLFSSL_LIBS) $(END_GROUP) + +build/test-macsec-secy: $(SUPPLICANT_OBJ) build/macsec/test_macsec_secy.o + @echo "[LD] $@" + @$(CC) $(CFLAGS) -o $@ $(BEGIN_GROUP) $(^) $(LDFLAGS) $(WOLFSSL_LIBS) $(END_GROUP) + +build/test-macsec-sa: $(SUPPLICANT_OBJ) build/macsec/test_macsec_sa.o + @echo "[LD] $@" + @$(CC) $(CFLAGS) -o $@ $(BEGIN_GROUP) $(^) $(LDFLAGS) $(WOLFSSL_LIBS) $(END_GROUP) + +# macsec_probe: byte-level SecY cross-check tool used by the kernel-macsec +# interop scripts (tools/macsec/run_macsec_static_test.sh). +build/tools/macsec_probe.o: tools/macsec/macsec_probe.c + @mkdir -p build/tools || true + @echo "[CC] $<" + @$(CC) $(CFLAGS) $(WOLFSSL_CFLAGS) -Isrc/supplicant -Isrc/macsec -c $< -o $@ + +build/macsec-probe: $(SUPPLICANT_OBJ) build/tools/macsec_probe.o + @echo "[LD] $@" + @$(CC) $(CFLAGS) -o $@ $(BEGIN_GROUP) $(^) $(LDFLAGS) $(WOLFSSL_LIBS) $(END_GROUP) + +# macsec_sta: AF_PACKET host harness that runs the wolfMKA participant for +# interop against wpa_supplicant (run_macsec_mka_test.sh). Linux only; requires +# WOLFMKA_DIR (the wolfMKA library provides the MKA control plane). +build/tools/macsec_sta.o: tools/macsec/macsec_sta.c + @test -n "$(WOLFMKA_DIR)" || { echo "macsec-sta requires WOLFMKA_DIR="; exit 1; } + @mkdir -p build/tools || true + @echo "[CC] $<" + @$(CC) $(CFLAGS) $(WOLFSSL_CFLAGS) -Isrc/supplicant -Isrc/macsec -I$(WOLFMKA_DIR)/include -c $< -o $@ + +build/macsec-sta: $(SUPPLICANT_OBJ) build/tools/macsec_sta.o + @echo "[LD] $@" + @$(CC) $(CFLAGS) -o $@ $(BEGIN_GROUP) $(^) $(LDFLAGS) $(WOLFSSL_LIBS) $(END_GROUP) + +# libFuzzer harness for the network-facing SecY receive path. Needs clang +# (-fsanitize=fuzzer); the macsec sources are compiled straight into the +# harness so the instrumentation is consistent. Not built by default. Requires +# a CMAC-enabled wolfSSL (WOLFSSL_CFLAGS/LIBS). +FUZZ_CC ?= clang +FUZZ_FLAGS ?= -fsanitize=fuzzer,address,undefined -fno-sanitize-recover=all -g -O1 +FUZZ_MACSEC_SRC := src/macsec/macsec_crypto.c src/macsec/macsec_secy.c \ + src/macsec/macsec_sa.c src/supplicant/wpa_crypto.c + +build/fuzz-macsec-secy: src/macsec/fuzz_macsec_secy.c $(FUZZ_MACSEC_SRC) + @mkdir -p build || true + @echo "[FUZZ] $@" + @$(FUZZ_CC) $(FUZZ_FLAGS) $(WOLFSSL_CFLAGS) -Isrc/supplicant -Isrc/macsec \ + $(^) -o $@ $(WOLFSSL_LIBS) + +macsec-fuzz: build/fuzz-macsec-secy + build/test-supplicant-4way: $(SUPPLICANT_OBJ) build/supplicant/test_supplicant_4way.o @echo "[LD] $@" @$(CC) $(CFLAGS) -o $@ $(BEGIN_GROUP) $(^) $(LDFLAGS) $(WOLFSSL_LIBS) $(END_GROUP) @@ -535,10 +633,23 @@ endif ifeq ($(WOLFIP_ENABLE_SAE),1) SUPPLICANT_TEST_BINS += build/test-sae-crypto build/test-supplicant-sae endif +ifeq ($(WOLFIP_ENABLE_MACSEC),1) +SUPPLICANT_TEST_BINS += build/test-macsec-crypto build/test-macsec-secy \ + build/test-macsec-sa +endif supplicant-tests: $(SUPPLICANT_TEST_BINS) @for t in $(SUPPLICANT_TEST_BINS); do echo "==> $$t"; $$t || exit 1; done +# The same in-process unit tests built with AddressSanitizer + UBSan, to catch +# memory / undefined-behaviour bugs in the parsers. Target-specific flags flow +# to the object + link rules of the prerequisite bins. Run from a clean tree +# (sanitizer objects must not be reused by a normal build). +supplicant-tests-sanitize: CFLAGS += -fsanitize=address,undefined \ + -fno-sanitize-recover=all -g +supplicant-tests-sanitize: LDFLAGS += -fsanitize=address,undefined +supplicant-tests-sanitize: supplicant-tests + # Real-authenticator interop tests. Both require hostapd installed and # root (veth pair + AF_PACKET raw socket). Not part of supplicant-tests # because of those constraints. diff --git a/src/macsec/fuzz_macsec_secy.c b/src/macsec/fuzz_macsec_secy.c new file mode 100644 index 00000000..e3451faa --- /dev/null +++ b/src/macsec/fuzz_macsec_secy.c @@ -0,0 +1,67 @@ +/* fuzz_macsec_secy.c + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfIP. + * + * wolfIP 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 2 of the License, or + * (at your option) any later version. + * + * wolfIP 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +/* libFuzzer harness for the 802.1AE SecY receive path. Feeds arbitrary bytes + * to macsec_sectag_parse and macsec_validate: GCM authentication rejects + * random frames, but the SecTAG parse and the length / header / offset bounds + * math that run before decryption are fully exercised. Each confidentiality + * offset (0/30/50) is tried. Build: make WOLFIP_ENABLE_MACSEC=1 build/fuzz-macsec-secy */ + +#include +#include + +#include "macsec_secy.h" + +static const uint8_t FZ_SAK[16] = { + 0xad,0x7a,0x2b,0xd0,0x3e,0xac,0x83,0x5a, + 0x6f,0x62,0x0f,0xdc,0xb5,0x06,0xb3,0x45 +}; +static const uint8_t FZ_SCI[8] = { 0x02,0,0,0,0,0x22,0x00,0x01 }; + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + struct macsec_sectag tag; + struct macsec_validate_params vp; + uint8_t out[2048]; + size_t out_len; + size_t i; + static const size_t offsets[3] = { 0U, 30U, 50U }; + + if (size > 1600) { + return 0; + } + + /* SecTAG parser on the raw bytes (starting at the MACsec EtherType). */ + (void)macsec_sectag_parse(data, size, &tag); + + /* Full validate path for each configured confidentiality offset. */ + for (i = 0; i < 3; i++) { + memset(&vp, 0, sizeof(vp)); + vp.sak = FZ_SAK; + vp.sak_len = sizeof(FZ_SAK); + vp.sci = FZ_SCI; + vp.conf_offset = offsets[i]; + out_len = 0; + (void)macsec_validate(&vp, data, size, out, sizeof(out), &out_len, + &tag); + } + return 0; +} diff --git a/src/macsec/macsec_crypto.c b/src/macsec/macsec_crypto.c new file mode 100644 index 00000000..f61fff85 --- /dev/null +++ b/src/macsec/macsec_crypto.c @@ -0,0 +1,355 @@ +/* macsec_crypto.c + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfIP. + * + * wolfIP 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 2 of the License, or + * (at your option) any later version. + * + * wolfIP 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include "macsec_crypto.h" +#include "supplicant_features.h" +#include "wpa_crypto.h" /* wpa_aes_keywrap/unwrap, wpa_secure_zero */ + +#include + +#include +#include +#include + +#ifndef WOLFSSL_CMAC +#error "wolfIP MACsec requires wolfSSL built with WOLFSSL_CMAC (--enable-cmac)" +#endif + +/* AES-CMAC output block size (bits and bytes). The KDF counter mode of + * IEEE 802.1X-2010 6.2.1 emits one 128-bit block per iteration. */ +#define MACSEC_CMAC_BLOCK_BITS 128U +#define MACSEC_CMAC_BLOCK_LEN 16U + +/* A segment of the KDF context, so a caller can supply a logically + * concatenated context (e.g. ks_nonce || mi_list || kn) without assembling + * it into one contiguous buffer - important for the multipoint SAK where + * the Member Identifier list can be large. */ +struct macsec_kdf_seg { + const uint8_t *p; + size_t len; +}; + +/* Constant-time compare of two byte strings. Returns 0 when equal, -1 + * otherwise. Branch-free accumulate so timing does not leak the ICV. */ +static int macsec_const_compare(const uint8_t *a, const uint8_t *b, size_t n) +{ + uint8_t diff = 0; + size_t i; + + for (i = 0; i < n; i++) { + diff |= (uint8_t)(a[i] ^ b[i]); + } + return (diff == 0) ? 0 : -1; +} + +/* One AES-CMAC over the ordered segments, key selects AES-128/256 by length. + * out must hold MACSEC_CMAC_BLOCK_LEN bytes. */ +static int macsec_cmac_segments(const uint8_t *key, size_t key_len, + const struct macsec_kdf_seg *segs, size_t nseg, + uint8_t out[MACSEC_CMAC_BLOCK_LEN]) +{ + Cmac cmac; + word32 out_sz = MACSEC_CMAC_BLOCK_LEN; + size_t i; + int ret; + + ret = wc_InitCmac(&cmac, key, (word32)key_len, WC_CMAC_AES, NULL); + if (ret != 0) { + return ret; + } + for (i = 0; i < nseg; i++) { + if (segs[i].len == 0) { + continue; + } + ret = wc_CmacUpdate(&cmac, segs[i].p, (word32)segs[i].len); + if (ret != 0) { + /* Finalize into a scratch block to release any CMAC state. */ + (void)wc_CmacFinal(&cmac, out, &out_sz); + return ret; + } + } + return wc_CmacFinal(&cmac, out, &out_sz); +} + +/* IEEE 802.1X-2010 6.2.1 KDF over segmented context. Builds, per block i: + * i(1) || label || 0x00 || || out_bits(2, big-endian) + * and concatenates blocks until out_bits are produced. */ +static int macsec_kdf_core(const uint8_t *key, size_t key_len, + const char *label, + const struct macsec_kdf_seg *ctx_segs, + size_t ctx_nseg, + uint16_t out_bits, uint8_t *out) +{ + struct macsec_kdf_seg segs[8]; + uint8_t block[MACSEC_CMAC_BLOCK_LEN]; + uint8_t ctr; + uint8_t sep; + uint8_t len_be[2]; + size_t label_len; + size_t out_len; + size_t copied; + size_t n; + size_t i; + unsigned int blk; + int ret; + + if (key == NULL || label == NULL || out == NULL) { + return BAD_FUNC_ARG; + } + if ((out_bits & 0x7U) != 0 || out_bits == 0 + || out_bits > (2U * MACSEC_CMAC_BLOCK_BITS)) { + return BAD_FUNC_ARG; + } + /* Fixed leading (ctr, label, sep) + context segments + trailing length; + * bounded so the segs[] array cannot overflow. */ + if (ctx_nseg + 4U > (sizeof(segs) / sizeof(segs[0]))) { + return BAD_FUNC_ARG; + } + + label_len = strlen(label); + sep = 0x00; + out_len = (size_t)(out_bits / 8U); + len_be[0] = (uint8_t)((out_bits >> 8) & 0xFFU); + len_be[1] = (uint8_t)(out_bits & 0xFFU); + n = (size_t)((out_bits + MACSEC_CMAC_BLOCK_BITS - 1U) + / MACSEC_CMAC_BLOCK_BITS); + + copied = 0; + ret = 0; + for (blk = 1; blk <= n; blk++) { + size_t s = 0; + + ctr = (uint8_t)blk; + segs[s].p = &ctr; segs[s].len = 1; s++; + segs[s].p = (const uint8_t *)label; segs[s].len = label_len; s++; + segs[s].p = &sep; segs[s].len = 1; s++; + for (i = 0; i < ctx_nseg; i++) { + segs[s].p = ctx_segs[i].p; segs[s].len = ctx_segs[i].len; s++; + } + segs[s].p = len_be; segs[s].len = 2; s++; + + ret = macsec_cmac_segments(key, key_len, segs, s, block); + if (ret != 0) { + break; + } + { + size_t take = out_len - copied; + if (take > MACSEC_CMAC_BLOCK_LEN) { + take = MACSEC_CMAC_BLOCK_LEN; + } + memcpy(out + copied, block, take); + copied += take; + } + } + + wpa_secure_zero(block, sizeof(block)); + if (ret != 0) { + wpa_secure_zero(out, out_len); + } + return ret; +} + +int macsec_kdf(const uint8_t *key, size_t key_len, + const char *label, + const uint8_t *context, size_t ctx_len, + uint16_t out_bits, uint8_t *out) +{ + struct macsec_kdf_seg seg; + + seg.p = context; + seg.len = ctx_len; + return macsec_kdf_core(key, key_len, label, &seg, + (context != NULL && ctx_len > 0) ? 1U : 0U, + out_bits, out); +} + +int macsec_derive_cak(const uint8_t *msk, size_t msk_len, size_t cak_len, + const uint8_t peer_mac[MACSEC_MAC_LEN], + const uint8_t auth_mac[MACSEC_MAC_LEN], + uint8_t *cak) +{ + struct macsec_kdf_seg segs[2]; + + if (msk == NULL || peer_mac == NULL || auth_mac == NULL || cak == NULL) { + return BAD_FUNC_ARG; + } + if ((cak_len != MACSEC_KEY_LEN_128 && cak_len != MACSEC_KEY_LEN_256) + || msk_len < cak_len) { + return BAD_FUNC_ARG; + } + /* KDK is the leftmost cak_len bytes of the MSK. */ + segs[0].p = peer_mac; segs[0].len = MACSEC_MAC_LEN; + segs[1].p = auth_mac; segs[1].len = MACSEC_MAC_LEN; + return macsec_kdf_core(msk, cak_len, MACSEC_LABEL_EAP_CAK, + segs, 2U, (uint16_t)(cak_len * 8U), cak); +} + +int macsec_derive_ckn(const uint8_t *msk, size_t msk_len, + const uint8_t *eap_session_id, size_t session_id_len, + const uint8_t peer_mac[MACSEC_MAC_LEN], + const uint8_t auth_mac[MACSEC_MAC_LEN], + uint8_t *ckn, size_t ckn_len) +{ + struct macsec_kdf_seg segs[3]; + + if (msk == NULL || eap_session_id == NULL || peer_mac == NULL + || auth_mac == NULL || ckn == NULL) { + return BAD_FUNC_ARG; + } + if (ckn_len == 0 || ckn_len > MACSEC_CKN_MAX_LEN + || (ckn_len & 0x1U) != 0 || msk_len < MACSEC_KEY_LEN_128) { + return BAD_FUNC_ARG; + } + /* KDK is a 128-bit key: the leftmost 16 bytes of the MSK. */ + segs[0].p = eap_session_id; segs[0].len = session_id_len; + segs[1].p = peer_mac; segs[1].len = MACSEC_MAC_LEN; + segs[2].p = auth_mac; segs[2].len = MACSEC_MAC_LEN; + return macsec_kdf_core(msk, MACSEC_KEY_LEN_128, MACSEC_LABEL_EAP_CKN, + segs, 3U, (uint16_t)(ckn_len * 8U), ckn); +} + +/* Shared KEK/ICK derivation: both take the CAK as KDK and the leftmost 16 + * bytes of the CKN (zero-padded) as context, differing only in the label. */ +static int macsec_derive_kek_ick(const uint8_t *cak, size_t cak_len, + const uint8_t *ckn, size_t ckn_len, + const char *label, uint8_t *out) +{ + uint8_t ctx[16]; + + if (cak == NULL || ckn == NULL || out == NULL) { + return BAD_FUNC_ARG; + } + if (cak_len != MACSEC_KEY_LEN_128 && cak_len != MACSEC_KEY_LEN_256) { + return BAD_FUNC_ARG; + } + if (ckn_len == 0) { + return BAD_FUNC_ARG; + } + memset(ctx, 0, sizeof(ctx)); + memcpy(ctx, ckn, (ckn_len < sizeof(ctx)) ? ckn_len : sizeof(ctx)); + return macsec_kdf(cak, cak_len, label, ctx, sizeof(ctx), + (uint16_t)(cak_len * 8U), out); +} + +int macsec_derive_kek(const uint8_t *cak, size_t cak_len, + const uint8_t *ckn, size_t ckn_len, + uint8_t *kek) +{ + return macsec_derive_kek_ick(cak, cak_len, ckn, ckn_len, + MACSEC_LABEL_KEK, kek); +} + +int macsec_derive_ick(const uint8_t *cak, size_t cak_len, + const uint8_t *ckn, size_t ckn_len, + uint8_t *ick) +{ + return macsec_derive_kek_ick(cak, cak_len, ckn, ckn_len, + MACSEC_LABEL_ICK, ick); +} + +int macsec_generate_sak(const uint8_t *cak, size_t cak_len, size_t sak_len, + const uint8_t ks_nonce[MACSEC_KS_NONCE_LEN], + const uint8_t *mi_list, size_t mi_list_len, + uint32_t kn, uint8_t *sak) +{ + struct macsec_kdf_seg segs[3]; + uint8_t kn_be[MACSEC_KN_LEN]; + + if (cak == NULL || ks_nonce == NULL || mi_list == NULL || sak == NULL) { + return BAD_FUNC_ARG; + } + if (cak_len != MACSEC_KEY_LEN_128 && cak_len != MACSEC_KEY_LEN_256) { + return BAD_FUNC_ARG; + } + if (sak_len != MACSEC_KEY_LEN_128 && sak_len != MACSEC_KEY_LEN_256) { + return BAD_FUNC_ARG; + } + if (mi_list_len == 0 || (mi_list_len % MACSEC_MI_LEN) != 0) { + return BAD_FUNC_ARG; + } + kn_be[0] = (uint8_t)((kn >> 24) & 0xFFU); + kn_be[1] = (uint8_t)((kn >> 16) & 0xFFU); + kn_be[2] = (uint8_t)((kn >> 8) & 0xFFU); + kn_be[3] = (uint8_t)(kn & 0xFFU); + + segs[0].p = ks_nonce; segs[0].len = MACSEC_KS_NONCE_LEN; + segs[1].p = mi_list; segs[1].len = mi_list_len; + segs[2].p = kn_be; segs[2].len = MACSEC_KN_LEN; + return macsec_kdf_core(cak, cak_len, MACSEC_LABEL_SAK, + segs, 3U, (uint16_t)(sak_len * 8U), sak); +} + +int macsec_mkpdu_icv(const uint8_t *ick, size_t ick_len, + const uint8_t *mkpdu, size_t mkpdu_len, + uint8_t icv[MACSEC_ICV_LEN]) +{ + word32 out_sz = MACSEC_ICV_LEN; + + if (ick == NULL || mkpdu == NULL || icv == NULL) { + return BAD_FUNC_ARG; + } + if (ick_len != MACSEC_KEY_LEN_128 && ick_len != MACSEC_KEY_LEN_256) { + return BAD_FUNC_ARG; + } + return wc_AesCmacGenerate(icv, &out_sz, mkpdu, (word32)mkpdu_len, + ick, (word32)ick_len); +} + +int macsec_mkpdu_icv_verify(const uint8_t *ick, size_t ick_len, + const uint8_t *mkpdu, size_t mkpdu_len, + const uint8_t expected_icv[MACSEC_ICV_LEN]) +{ + uint8_t computed[MACSEC_ICV_LEN]; + int ret; + + if (expected_icv == NULL) { + return BAD_FUNC_ARG; + } + ret = macsec_mkpdu_icv(ick, ick_len, mkpdu, mkpdu_len, computed); + if (ret != 0) { + wpa_secure_zero(computed, sizeof(computed)); + return ret; + } + ret = macsec_const_compare(computed, expected_icv, MACSEC_ICV_LEN); + wpa_secure_zero(computed, sizeof(computed)); + return (ret == 0) ? 0 : -1; +} + +int macsec_wrap_sak(const uint8_t *kek, size_t kek_len, + const uint8_t *sak, size_t sak_len, + uint8_t *wrapped) +{ + if (sak_len != MACSEC_KEY_LEN_128 && sak_len != MACSEC_KEY_LEN_256) { + return BAD_FUNC_ARG; + } + return wpa_aes_keywrap(kek, kek_len, sak, sak_len, wrapped); +} + +int macsec_unwrap_sak(const uint8_t *kek, size_t kek_len, + const uint8_t *wrapped, size_t wrapped_len, + uint8_t *sak) +{ + if (wrapped_len != (MACSEC_KEY_LEN_128 + WPA_KEYWRAP_SEMIBLOCK) + && wrapped_len != (MACSEC_KEY_LEN_256 + WPA_KEYWRAP_SEMIBLOCK)) { + return BAD_FUNC_ARG; + } + return wpa_aes_keyunwrap(kek, kek_len, wrapped, wrapped_len, sak); +} diff --git a/src/macsec/macsec_crypto.h b/src/macsec/macsec_crypto.h new file mode 100644 index 00000000..7d1829b1 --- /dev/null +++ b/src/macsec/macsec_crypto.h @@ -0,0 +1,157 @@ +/* macsec_crypto.h + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfIP. + * + * wolfIP 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 2 of the License, or + * (at your option) any later version. + * + * wolfIP 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +/* Clean-room MACsec Key Agreement (MKA) key hierarchy, per IEEE Std + * 802.1X-2010. This module owns the cryptographic derivations only (the + * AES-CMAC KDF of 6.2.1, the CAK/CKN/KEK/ICK/SAK derivations of 9.3.3, the + * MKPDU ICV, and the AES Key Wrap of the SAK); the protocol state machine + * lives in mka.c and the 802.1AE frame transform in macsec_secy.c. + * + * The AES-CMAC PRF uses wolfCrypt's wc_AesCmacGenerate (WOLFSSL_CMAC), which + * supports both 128- and 256-bit keys. AES Key Wrap and secret zeroization + * reuse the shared helpers in the supplicant crypto layer (wpa_crypto.h). + */ + +#ifndef WOLFIP_MACSEC_CRYPTO_H +#define WOLFIP_MACSEC_CRYPTO_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Key and identifier sizes (IEEE 802.1X-2010 / 802.1AE). AES-128 variants + * use 16-byte keys, AES-256 variants 32. */ +#define MACSEC_KEY_LEN_128 16U +#define MACSEC_KEY_LEN_256 32U +#define MACSEC_KEY_LEN_MAX 32U /* CAK / KEK / ICK / SAK upper bound */ +#define MACSEC_ICV_LEN 16U /* MKPDU ICV and 802.1AE frame ICV */ +#define MACSEC_CKN_MAX_LEN 32U /* Connectivity Association Key Name */ +#define MACSEC_MI_LEN 12U /* Member Identifier (96 bits) */ +#define MACSEC_KS_NONCE_LEN 32U /* Key Server nonce for SAK generation */ +#define MACSEC_SCI_LEN 8U /* Secure Channel Identifier */ +#define MACSEC_KN_LEN 4U /* Key Number (SAK), big-endian */ +#define MACSEC_MAC_LEN 6U + +/* 802.1AE Cipher Suite identifiers (64-bit, big-endian on the wire). + * XPN suites are intentionally out of scope for this module. */ +#define MACSEC_CIPHER_GCM_AES_128 0x0080C20001000001ULL +#define MACSEC_CIPHER_GCM_AES_256 0x0080C20001000004ULL + +/* KDF label strings (IEEE 802.1X-2010). Exact ASCII, no NUL in the KDF + * input; the 0x00 separator is added by the KDF construction itself. */ +#define MACSEC_LABEL_EAP_CAK "IEEE8021 EAP CAK" +#define MACSEC_LABEL_EAP_CKN "IEEE8021 EAP CKN" +#define MACSEC_LABEL_KEK "IEEE8021 KEK" +#define MACSEC_LABEL_ICK "IEEE8021 ICK" +#define MACSEC_LABEL_SAK "IEEE8021 SAK" + +/* IEEE 802.1X-2010 6.2.1 KDF, AES-CMAC in NIST SP 800-108 counter mode. + * + * For each output block i = 1..ceil(out_bits/128): + * T_i = AES-CMAC(key, i(1 octet) || label || 0x00 || context || + * out_bits(2 octets, big-endian)) + * out receives the leftmost out_bits (rounded up to whole octets) of the + * concatenated T_i. key_len selects AES-128 (16) or AES-256 (32) CMAC. + * label is a C string (its bytes, excluding the terminating NUL, are hashed). + * out_bits must be a multiple of 8 and <= 256 (n <= 2). Returns 0 on success, + * negative wolfCrypt error otherwise. */ +int macsec_kdf(const uint8_t *key, size_t key_len, + const char *label, + const uint8_t *context, size_t ctx_len, + uint16_t out_bits, uint8_t *out); + +/* CAK from an EAP MSK (802.1X-2010 9.3.1). The KDK is the leftmost cak_len + * bytes of the MSK; context is peer_mac || auth_mac. cak_len is 16 or 32. + * + * NOTE: the two-MAC context ordering is not yet cross-checked against + * wpa_supplicant EAP-MKA (current interop uses a PSK CAK). The KDF core and + * the KEK/ICK-from-CAK path below are byte-verified against wpa_supplicant. */ +int macsec_derive_cak(const uint8_t *msk, size_t msk_len, size_t cak_len, + const uint8_t peer_mac[MACSEC_MAC_LEN], + const uint8_t auth_mac[MACSEC_MAC_LEN], + uint8_t *cak); + +/* CKN from an EAP MSK (802.1X-2010 9.3.1). Context is the EAP Session-Id + * followed by peer_mac || auth_mac. ckn_len is 1..32. + * + * NOTE: context ordering not yet cross-checked against wpa_supplicant (as + * for macsec_derive_cak above). */ +int macsec_derive_ckn(const uint8_t *msk, size_t msk_len, + const uint8_t *eap_session_id, size_t session_id_len, + const uint8_t peer_mac[MACSEC_MAC_LEN], + const uint8_t auth_mac[MACSEC_MAC_LEN], + uint8_t *ckn, size_t ckn_len); + +/* KEK (Key Encrypting Key) from the CAK (802.1X-2010 9.3.3). Context is the + * leftmost 16 bytes of the CKN (zero-padded if the CKN is shorter). Output + * length matches cak_len (16 or 32). */ +int macsec_derive_kek(const uint8_t *cak, size_t cak_len, + const uint8_t *ckn, size_t ckn_len, + uint8_t *kek); + +/* ICK (ICV Key) from the CAK (802.1X-2010 9.3.3). Same context rule as the + * KEK; used to compute/verify the MKPDU ICV. Output length matches cak_len. */ +int macsec_derive_ick(const uint8_t *cak, size_t cak_len, + const uint8_t *ckn, size_t ckn_len, + uint8_t *ick); + +/* SAK generation by the Key Server (802.1X-2010 9.3.3 / 9.8). Context is + * ks_nonce(32) || mi_list || kn(4, big-endian), where mi_list is the + * concatenated Member Identifiers of all live participants. sak_len is 16 + * (GCM-AES-128) or 32 (GCM-AES-256). */ +int macsec_generate_sak(const uint8_t *cak, size_t cak_len, size_t sak_len, + const uint8_t ks_nonce[MACSEC_KS_NONCE_LEN], + const uint8_t *mi_list, size_t mi_list_len, + uint32_t kn, uint8_t *sak); + +/* MKPDU ICV = AES-CMAC(ICK, mkpdu). The ICV parameter set body is excluded + * by the caller (it passes the MKPDU up to and including the ICV parameter + * set header, with the ICV octets themselves omitted). Truncated to 16 + * bytes. ick_len is 16 or 32. */ +int macsec_mkpdu_icv(const uint8_t *ick, size_t ick_len, + const uint8_t *mkpdu, size_t mkpdu_len, + uint8_t icv[MACSEC_ICV_LEN]); + +/* Constant-time ICV verify. Returns 0 on match, -1 on mismatch, negative + * wolfCrypt error on failure to compute. */ +int macsec_mkpdu_icv_verify(const uint8_t *ick, size_t ick_len, + const uint8_t *mkpdu, size_t mkpdu_len, + const uint8_t expected_icv[MACSEC_ICV_LEN]); + +/* Wrap / unwrap the SAK under the KEK for the Distributed SAK parameter set + * (AES Key Wrap, RFC 3394). wrapped holds sak_len + 8 bytes; unwrap reverses + * it. sak_len is 16 or 32. Returns 0 on success. */ +int macsec_wrap_sak(const uint8_t *kek, size_t kek_len, + const uint8_t *sak, size_t sak_len, + uint8_t *wrapped); + +int macsec_unwrap_sak(const uint8_t *kek, size_t kek_len, + const uint8_t *wrapped, size_t wrapped_len, + uint8_t *sak); + +#ifdef __cplusplus +} +#endif + +#endif /* WOLFIP_MACSEC_CRYPTO_H */ diff --git a/src/macsec/macsec_sa.c b/src/macsec/macsec_sa.c new file mode 100644 index 00000000..8b4a5faf --- /dev/null +++ b/src/macsec/macsec_sa.c @@ -0,0 +1,181 @@ +/* macsec_sa.c + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfIP. + * + * wolfIP 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 2 of the License, or + * (at your option) any later version. + * + * wolfIP 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include "macsec_sa.h" +#include "supplicant_features.h" +#include "wpa_crypto.h" /* wpa_secure_zero */ + +#include + +static int macsec_sa_key_ok(size_t sak_len) +{ + return (sak_len == MACSEC_KEY_LEN_128 || sak_len == MACSEC_KEY_LEN_256); +} + +int macsec_tx_sc_set_key(struct macsec_tx_sc *sc, + const uint8_t *sak, size_t sak_len, + const uint8_t sci[MACSEC_SCI_LEN], uint8_t an, + uint8_t encrypt, size_t conf_offset, + uint8_t include_sci, uint32_t initial_pn) +{ + if (sc == NULL || sak == NULL || sci == NULL || !macsec_sa_key_ok(sak_len)) { + return MACSEC_RX_BAD_ARG; + } + if (an > MACSEC_AN_MASK || initial_pn == 0 + || (conf_offset != 0 && conf_offset != 30 && conf_offset != 50)) { + return MACSEC_RX_BAD_ARG; + } + memset(sc, 0, sizeof(*sc)); + memcpy(sc->sak, sak, sak_len); + sc->sak_len = sak_len; + memcpy(sc->sci, sci, MACSEC_SCI_LEN); + sc->an = an; + sc->next_pn = initial_pn; + sc->conf_offset = conf_offset; + sc->encrypt = encrypt ? 1U : 0U; + sc->include_sci = include_sci ? 1U : 0U; + sc->in_use = 1U; + return 0; +} + +int macsec_rx_sc_set_key(struct macsec_rx_sc *sc, + const uint8_t *sak, size_t sak_len, + const uint8_t peer_sci[MACSEC_SCI_LEN], uint8_t an, + uint8_t replay_protect, uint32_t replay_window, + size_t conf_offset) +{ + if (sc == NULL || sak == NULL || peer_sci == NULL + || !macsec_sa_key_ok(sak_len)) { + return MACSEC_RX_BAD_ARG; + } + if (an > MACSEC_AN_MASK + || (conf_offset != 0 && conf_offset != 30 && conf_offset != 50)) { + return MACSEC_RX_BAD_ARG; + } + memset(sc, 0, sizeof(*sc)); + memcpy(sc->sak, sak, sak_len); + sc->sak_len = sak_len; + memcpy(sc->sci, peer_sci, MACSEC_SCI_LEN); + sc->an = an; + sc->lowest_pn = 1U; /* PN 0 is invalid */ + sc->replay_window = replay_window; + sc->conf_offset = conf_offset; + sc->replay_protect = replay_protect ? 1U : 0U; + sc->in_use = 1U; + return 0; +} + +int macsec_tx(struct macsec_tx_sc *sc, + const uint8_t da[MACSEC_MAC_LEN], + const uint8_t sa[MACSEC_MAC_LEN], + const uint8_t *payload, size_t payload_len, + uint8_t *out, size_t out_cap, size_t *out_len) +{ + struct macsec_protect_params p; + int ret; + + if (sc == NULL || !sc->in_use) { + return MACSEC_RX_BAD_ARG; + } + /* PN exhausted: next_pn is parked at 0 once the ceiling has been used + * (see below); a fresh SAK is required before it can wrap (802.1AE). */ + if (sc->next_pn == 0U) { + return MACSEC_RX_BAD_ARG; + } + + memset(&p, 0, sizeof(p)); + p.da = da; + p.sa = sa; + p.sci = sc->sci; + p.sak = sc->sak; + p.sak_len = sc->sak_len; + p.conf_offset = sc->conf_offset; + p.pn = sc->next_pn; + p.an = sc->an; + p.encrypt = sc->encrypt; + p.include_sci = sc->include_sci; + + ret = macsec_protect(&p, payload, payload_len, out, out_cap, out_len); + if (ret != 0) { + return ret; + } + /* Advance only after a successful protect; stop at the ceiling so the + * next call reports exhaustion rather than wrapping to 0. */ + if (sc->next_pn == MACSEC_PN_MAX) { + sc->next_pn = 0U; /* mark exhausted */ + } + else { + sc->next_pn++; + } + return 0; +} + +int macsec_rx(struct macsec_rx_sc *sc, + const uint8_t *frame, size_t frame_len, + uint8_t *out, size_t out_cap, size_t *out_len) +{ + struct macsec_validate_params vp; + struct macsec_sectag tag; + int ret; + + if (sc == NULL || !sc->in_use || frame == NULL || out == NULL + || out_len == NULL) { + return MACSEC_RX_BAD_ARG; + } + + memset(&vp, 0, sizeof(vp)); + vp.sak = sc->sak; + vp.sak_len = sc->sak_len; + vp.sci = sc->sci; + vp.conf_offset = sc->conf_offset; + + /* Authenticate first: the replay window must only ever be tested and + * advanced on an authenticated PN, so a forged PN cannot move it. */ + ret = macsec_validate(&vp, frame, frame_len, out, out_cap, out_len, &tag); + if (ret != 0) { + /* macsec_validate() returns -1 for an ICV/GCM authentication failure + * and a distinct negative (BAD_FUNC_ARG) for a malformed/truncated + * frame; keep them separate so callers can tell a forged frame from a + * parse error. */ + return (ret == -1) ? MACSEC_RX_AUTH_FAIL : MACSEC_RX_BAD_ARG; + } + if (sc->replay_protect && tag.pn < sc->lowest_pn) { + wpa_secure_zero(out, *out_len); + *out_len = 0; + return MACSEC_RX_REPLAY; + } + + /* Advance the window: lowest acceptable PN becomes pn + 1 - window, + * floored at 1, and never moves backwards. */ + if (sc->replay_protect) { + uint32_t new_low; + if (tag.pn >= sc->replay_window) { + new_low = tag.pn - sc->replay_window + 1U; + } + else { + new_low = 1U; + } + if (new_low > sc->lowest_pn) { + sc->lowest_pn = new_low; + } + } + return MACSEC_RX_OK; +} diff --git a/src/macsec/macsec_sa.h b/src/macsec/macsec_sa.h new file mode 100644 index 00000000..d9a25931 --- /dev/null +++ b/src/macsec/macsec_sa.h @@ -0,0 +1,117 @@ +/* macsec_sa.h + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfIP. + * + * wolfIP 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 2 of the License, or + * (at your option) any later version. + * + * wolfIP 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +/* Stateful 802.1AE Secure Association layer. Wraps the stateless SecY + * transform (macsec_secy.c) with the per-SA packet-number counter (transmit) + * and the replay window (receive) that a real SecY maintains. This is the + * seam the wolfIP datapath drives: outbound frames go through macsec_tx() + * before ll->send, inbound MACsec frames (EtherType 0x88E5) through + * macsec_rx() after the link-layer demux. The SAK is installed here by the + * MKA install_sak callback (see mka.h). + */ + +#ifndef WOLFIP_MACSEC_SA_H +#define WOLFIP_MACSEC_SA_H + +#include +#include + +#include "macsec_secy.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* Return codes for macsec_rx(). */ +#define MACSEC_RX_OK 0 +#define MACSEC_RX_AUTH_FAIL (-1) /* ICV / GCM authentication failed */ +#define MACSEC_RX_REPLAY (-2) /* PN below the replay window */ +#define MACSEC_RX_BAD_ARG (-3) + +/* PN exhaustion: 0xFFFFFFFF is the last usable 32-bit packet number; a fresh + * SAK must be installed before it wraps (XPN is out of scope). */ +#define MACSEC_PN_MAX 0xFFFFFFFFUL + +/* Transmit Secure Channel + its current Secure Association. */ +struct macsec_tx_sc { + uint8_t sak[MACSEC_KEY_LEN_MAX]; + size_t sak_len; /* 16 or 32; 0 = no key installed */ + uint8_t sci[MACSEC_SCI_LEN]; /* our SCI */ + uint32_t next_pn; /* next packet number to transmit */ + size_t conf_offset; /* 0/30/50 */ + uint8_t an; /* Association Number */ + uint8_t encrypt; /* 1 = confidentiality, 0 = integrity */ + uint8_t include_sci; /* set SC bit / include SCI in SecTAG */ + uint8_t in_use; +}; + +/* Receive Secure Channel + its current Secure Association. */ +struct macsec_rx_sc { + uint8_t sak[MACSEC_KEY_LEN_MAX]; + size_t sak_len; + uint8_t sci[MACSEC_SCI_LEN]; /* peer SCI (nonce fallback) */ + uint32_t lowest_pn; /* replay window lower bound */ + uint32_t replay_window; /* frames of reorder tolerated */ + size_t conf_offset; + uint8_t an; + uint8_t replay_protect; + uint8_t in_use; +}; + +/* Install a SAK on the transmit SC. initial_pn is normally 1 (PN 0 is + * invalid). encrypt selects confidentiality vs integrity-only; conf_offset is + * 0/30/50. Returns 0 on success. */ +int macsec_tx_sc_set_key(struct macsec_tx_sc *sc, + const uint8_t *sak, size_t sak_len, + const uint8_t sci[MACSEC_SCI_LEN], uint8_t an, + uint8_t encrypt, size_t conf_offset, + uint8_t include_sci, uint32_t initial_pn); + +/* Install a SAK on the receive SC. replay_protect enables the window; + * replay_window is the number of out-of-order frames tolerated below the + * highest accepted PN. Returns 0 on success. */ +int macsec_rx_sc_set_key(struct macsec_rx_sc *sc, + const uint8_t *sak, size_t sak_len, + const uint8_t peer_sci[MACSEC_SCI_LEN], uint8_t an, + uint8_t replay_protect, uint32_t replay_window, + size_t conf_offset); + +/* Protect payload (a user MSDU starting at its EtherType) into a MACsec frame + * using the SC's next PN, then advance the counter. Returns 0 on success, + * MACSEC_RX_* / negative on error (including PN exhaustion -> rekey needed). */ +int macsec_tx(struct macsec_tx_sc *sc, + const uint8_t da[MACSEC_MAC_LEN], + const uint8_t sa[MACSEC_MAC_LEN], + const uint8_t *payload, size_t payload_len, + uint8_t *out, size_t out_cap, size_t *out_len); + +/* Validate a received MACsec frame and enforce the replay window. On success + * the recovered MSDU is written to out and *out_len set. Returns + * MACSEC_RX_OK, MACSEC_RX_AUTH_FAIL, MACSEC_RX_REPLAY, or MACSEC_RX_BAD_ARG. */ +int macsec_rx(struct macsec_rx_sc *sc, + const uint8_t *frame, size_t frame_len, + uint8_t *out, size_t out_cap, size_t *out_len); + +#ifdef __cplusplus +} +#endif + +#endif /* WOLFIP_MACSEC_SA_H */ diff --git a/src/macsec/macsec_secy.c b/src/macsec/macsec_secy.c new file mode 100644 index 00000000..de0a923a --- /dev/null +++ b/src/macsec/macsec_secy.c @@ -0,0 +1,340 @@ +/* macsec_secy.c + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfIP. + * + * wolfIP 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 2 of the License, or + * (at your option) any later version. + * + * wolfIP 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include "macsec_secy.h" +#include "supplicant_features.h" +#include "wpa_crypto.h" /* wpa_secure_zero */ + +#include + +#include +#include +#include + +#ifndef HAVE_AESGCM +#error "wolfIP MACsec SecY requires wolfSSL built with HAVE_AESGCM (--enable-aesgcm)" +#endif + +#define MACSEC_ETH_ADDR_PAIR_LEN 12U /* DA(6) + SA(6) */ + +/* conf_offset is limited to the 802.1AE-permitted values. */ +static int macsec_offset_valid(size_t off) +{ + return (off == 0U || off == 30U || off == 50U); +} + +/* Assemble the 12-byte GCM nonce SCI(8) || PN(4, big-endian). */ +static void macsec_build_nonce(uint8_t nonce[MACSEC_GCM_NONCE_LEN], + const uint8_t sci[MACSEC_SCI_LEN], uint32_t pn) +{ + memcpy(nonce, sci, MACSEC_SCI_LEN); + nonce[8] = (uint8_t)((pn >> 24) & 0xFFU); + nonce[9] = (uint8_t)((pn >> 16) & 0xFFU); + nonce[10] = (uint8_t)((pn >> 8) & 0xFFU); + nonce[11] = (uint8_t)(pn & 0xFFU); +} + +int macsec_sectag_build(uint8_t *out, size_t out_cap, + const struct macsec_protect_params *p, + size_t secure_data_len) +{ + uint8_t tci_an; + size_t len; + + if (out == NULL || p == NULL) { + return BAD_FUNC_ARG; + } + if (p->an > MACSEC_AN_MASK) { + return BAD_FUNC_ARG; + } + len = p->include_sci ? MACSEC_SECTAG_MAX_LEN : MACSEC_SECTAG_MIN_LEN; + if (out_cap < len) { + return BAD_FUNC_ARG; + } + + tci_an = (uint8_t)(p->an & MACSEC_AN_MASK); /* V bit stays 0 */ + if (p->end_station) { + tci_an |= MACSEC_TCI_ES; + } + if (p->include_sci) { + tci_an |= MACSEC_TCI_SC; + } + if (p->scb) { + tci_an |= MACSEC_TCI_SCB; + } + if (p->encrypt) { + /* Confidentiality: both E (encrypted) and C (changed text) set. */ + tci_an |= (uint8_t)(MACSEC_TCI_E | MACSEC_TCI_C); + } + + out[0] = (uint8_t)((MACSEC_ETHERTYPE >> 8) & 0xFFU); + out[1] = (uint8_t)(MACSEC_ETHERTYPE & 0xFFU); + out[2] = tci_an; + out[3] = (secure_data_len < MACSEC_MIN_SECURE_DATA) + ? (uint8_t)secure_data_len : 0U; + out[4] = (uint8_t)((p->pn >> 24) & 0xFFU); + out[5] = (uint8_t)((p->pn >> 16) & 0xFFU); + out[6] = (uint8_t)((p->pn >> 8) & 0xFFU); + out[7] = (uint8_t)(p->pn & 0xFFU); + if (p->include_sci) { + memcpy(out + 8, p->sci, MACSEC_SCI_LEN); + } + return (int)len; +} + +int macsec_sectag_parse(const uint8_t *in, size_t in_len, + struct macsec_sectag *out) +{ + uint8_t tci_an; + + if (in == NULL || out == NULL) { + return BAD_FUNC_ARG; + } + if (in_len < MACSEC_SECTAG_MIN_LEN) { + return BAD_FUNC_ARG; + } + if (in[0] != (uint8_t)((MACSEC_ETHERTYPE >> 8) & 0xFFU) + || in[1] != (uint8_t)(MACSEC_ETHERTYPE & 0xFFU)) { + return BAD_FUNC_ARG; + } + tci_an = in[2]; + if ((tci_an & MACSEC_TCI_V) != 0U) { + return BAD_FUNC_ARG; /* version must be 0 */ + } + + memset(out, 0, sizeof(*out)); + out->tci_an = tci_an; + out->sl = in[3]; + out->pn = ((uint32_t)in[4] << 24) | ((uint32_t)in[5] << 16) + | ((uint32_t)in[6] << 8) | (uint32_t)in[7]; + if ((tci_an & MACSEC_TCI_SC) != 0U) { + if (in_len < MACSEC_SECTAG_MAX_LEN) { + return BAD_FUNC_ARG; + } + out->sci_present = 1U; + memcpy(out->sci, in + 8, MACSEC_SCI_LEN); + out->len = MACSEC_SECTAG_MAX_LEN; + } + else { + out->len = MACSEC_SECTAG_MIN_LEN; + } + return 0; +} + +int macsec_protect(const struct macsec_protect_params *p, + const uint8_t *payload, size_t payload_len, + uint8_t *out, size_t out_cap, size_t *out_len) +{ + Aes aes; + uint8_t nonce[MACSEC_GCM_NONCE_LEN]; + uint8_t tag[MACSEC_ICV_LEN]; + size_t sectag_len; + size_t hdr_len; + size_t cleartext_len; + size_t enc_len; + size_t total; + int ret; + int built; + + if (p == NULL || payload == NULL || out == NULL || out_len == NULL) { + return BAD_FUNC_ARG; + } + if (p->da == NULL || p->sa == NULL || p->sci == NULL || p->sak == NULL) { + return BAD_FUNC_ARG; + } + if (p->sak_len != MACSEC_KEY_LEN_128 && p->sak_len != MACSEC_KEY_LEN_256) { + return BAD_FUNC_ARG; + } + if (p->encrypt && !macsec_offset_valid(p->conf_offset)) { + return BAD_FUNC_ARG; + } + if (out_cap < MACSEC_ETH_ADDR_PAIR_LEN) { + return BAD_FUNC_ARG; + } + + /* Build the SecTAG directly in place after the DA/SA pair. */ + built = macsec_sectag_build(out + MACSEC_ETH_ADDR_PAIR_LEN, + out_cap - MACSEC_ETH_ADDR_PAIR_LEN, + p, payload_len); + if (built < 0) { + return built; + } + sectag_len = (size_t)built; + hdr_len = MACSEC_ETH_ADDR_PAIR_LEN + sectag_len; + + /* cleartext_len: encrypted frames leave conf_offset octets in the clear + * (bounded by the payload); integrity-only frames keep the whole MSDU. */ + if (p->encrypt) { + cleartext_len = (p->conf_offset < payload_len) + ? p->conf_offset : payload_len; + } + else { + cleartext_len = payload_len; + } + enc_len = payload_len - cleartext_len; + + total = hdr_len + payload_len + MACSEC_ICV_LEN; + if (out_cap < total) { + return BAD_FUNC_ARG; + } + + /* SecTAG is already at out[12]; write the DA || SA pair in front of it. */ + memcpy(out, p->da, MACSEC_MAC_LEN); + memcpy(out + MACSEC_MAC_LEN, p->sa, MACSEC_MAC_LEN); + + /* Secure Data = [cleartext prefix] || [ciphertext]. Copy the cleartext + * prefix; the AAD is exactly out[0 .. hdr_len + cleartext_len). */ + memcpy(out + hdr_len, payload, cleartext_len); + + macsec_build_nonce(nonce, p->sci, p->pn); + + ret = wc_AesInit(&aes, NULL, INVALID_DEVID); + if (ret != 0) { + return ret; + } + ret = wc_AesGcmSetKey(&aes, p->sak, (word32)p->sak_len); + if (ret != 0) { + wc_AesFree(&aes); + return ret; + } + ret = wc_AesGcmEncrypt(&aes, + out + hdr_len + cleartext_len, + payload + cleartext_len, (word32)enc_len, + nonce, MACSEC_GCM_NONCE_LEN, + tag, MACSEC_ICV_LEN, + out, (word32)(hdr_len + cleartext_len)); + wc_AesFree(&aes); + if (ret != 0) { + wpa_secure_zero(nonce, sizeof(nonce)); + return ret; + } + + memcpy(out + hdr_len + payload_len, tag, MACSEC_ICV_LEN); + *out_len = total; + + wpa_secure_zero(nonce, sizeof(nonce)); + wpa_secure_zero(tag, sizeof(tag)); + return 0; +} + +int macsec_validate(const struct macsec_validate_params *p, + const uint8_t *frame, size_t frame_len, + uint8_t *out_payload, size_t out_cap, + size_t *out_payload_len, + struct macsec_sectag *out_tag) +{ + struct macsec_sectag tag; + Aes aes; + uint8_t nonce[MACSEC_GCM_NONCE_LEN]; + const uint8_t *sci; + const uint8_t *icv; + size_t hdr_len; + size_t sd_len; + size_t cleartext_len; + size_t enc_len; + int encrypted; + int ret; + + if (p == NULL || frame == NULL || out_payload == NULL + || out_payload_len == NULL || out_tag == NULL) { + return BAD_FUNC_ARG; + } + if (p->sak == NULL + || (p->sak_len != MACSEC_KEY_LEN_128 + && p->sak_len != MACSEC_KEY_LEN_256)) { + return BAD_FUNC_ARG; + } + if (!macsec_offset_valid(p->conf_offset)) { + return BAD_FUNC_ARG; + } + if (frame_len < MACSEC_ETH_ADDR_PAIR_LEN + MACSEC_SECTAG_MIN_LEN + + MACSEC_ICV_LEN) { + return BAD_FUNC_ARG; + } + + ret = macsec_sectag_parse(frame + MACSEC_ETH_ADDR_PAIR_LEN, + frame_len - MACSEC_ETH_ADDR_PAIR_LEN, &tag); + if (ret != 0) { + return ret; + } + hdr_len = MACSEC_ETH_ADDR_PAIR_LEN + tag.len; + if (frame_len < hdr_len + MACSEC_ICV_LEN) { + return BAD_FUNC_ARG; + } + sd_len = frame_len - hdr_len - MACSEC_ICV_LEN; + icv = frame + hdr_len + sd_len; + + /* Nonce SCI: the SecTAG's if present, otherwise the SC's configured SCI. */ + if (tag.sci_present) { + sci = tag.sci; + } + else if (p->sci != NULL) { + sci = p->sci; + } + else { + return BAD_FUNC_ARG; + } + + encrypted = ((tag.tci_an & MACSEC_TCI_E) != 0U); + if (encrypted) { + cleartext_len = (p->conf_offset < sd_len) ? p->conf_offset : sd_len; + } + else { + cleartext_len = sd_len; + } + enc_len = sd_len - cleartext_len; + + if (out_cap < sd_len) { + return BAD_FUNC_ARG; + } + + /* Copy the cleartext prefix; AAD is frame[0 .. hdr_len + cleartext_len). */ + memcpy(out_payload, frame + hdr_len, cleartext_len); + + macsec_build_nonce(nonce, sci, tag.pn); + + ret = wc_AesInit(&aes, NULL, INVALID_DEVID); + if (ret != 0) { + return ret; + } + ret = wc_AesGcmSetKey(&aes, p->sak, (word32)p->sak_len); + if (ret != 0) { + wc_AesFree(&aes); + return ret; + } + ret = wc_AesGcmDecrypt(&aes, + out_payload + cleartext_len, + frame + hdr_len + cleartext_len, (word32)enc_len, + nonce, MACSEC_GCM_NONCE_LEN, + icv, MACSEC_ICV_LEN, + frame, (word32)(hdr_len + cleartext_len)); + wc_AesFree(&aes); + wpa_secure_zero(nonce, sizeof(nonce)); + if (ret != 0) { + /* ICV mismatch (AES_GCM_AUTH_E) or crypto error: scrub output. */ + wpa_secure_zero(out_payload, sd_len); + return -1; + } + + *out_payload_len = sd_len; + memcpy(out_tag, &tag, sizeof(tag)); + return 0; +} diff --git a/src/macsec/macsec_secy.h b/src/macsec/macsec_secy.h new file mode 100644 index 00000000..6836ba8f --- /dev/null +++ b/src/macsec/macsec_secy.h @@ -0,0 +1,139 @@ +/* macsec_secy.h + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfIP. + * + * wolfIP 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 2 of the License, or + * (at your option) any later version. + * + * wolfIP 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +/* Clean-room 802.1AE SecY frame transform. These are stateless functions: + * macsec_protect() turns a user MSDU (its Ethernet payload, starting at the + * original EtherType) into a MACsec frame (DA || SA || SecTAG || Secure Data + * || ICV), and macsec_validate() reverses it. Cipher Suites GCM-AES-128 and + * GCM-AES-256 with 32-bit packet numbers (XPN out of scope). Confidentiality + * (encrypt) with offset 0/30/50, and integrity-only, are supported. + * + * Secure Association state (PN counters, replay window, key install) is owned + * by the caller / higher layer; this module only does the per-frame crypto so + * it can be exercised with known-answer vectors. + */ + +#ifndef WOLFIP_MACSEC_SECY_H +#define WOLFIP_MACSEC_SECY_H + +#include +#include + +#include "macsec_crypto.h" /* key/SCI/ICV sizes */ + +#ifdef __cplusplus +extern "C" { +#endif + +#define MACSEC_ETHERTYPE 0x88E5U +#define MACSEC_SECTAG_MIN_LEN 8U /* EtherType(2)+TCI/AN(1)+SL(1)+PN(4) */ +#define MACSEC_SECTAG_MAX_LEN 16U /* + SCI(8) when the SC bit is set */ +#define MACSEC_MIN_SECURE_DATA 48U /* below this, SL carries the length */ + +/* SecTAG TCI bit masks (the high 6 bits of the TCI/AN octet). */ +#define MACSEC_TCI_V 0x80U /* Version (always 0) */ +#define MACSEC_TCI_ES 0x40U /* End Station */ +#define MACSEC_TCI_SC 0x20U /* SCI present in the SecTAG */ +#define MACSEC_TCI_SCB 0x10U /* EPON Single Copy Broadcast */ +#define MACSEC_TCI_E 0x08U /* Encryption (confidentiality) */ +#define MACSEC_TCI_C 0x04U /* Changed Text */ +#define MACSEC_AN_MASK 0x03U /* Association Number (low 2 bits) */ + +/* GCM nonce = SCI(8) || PN(4). */ +#define MACSEC_GCM_NONCE_LEN 12U + +/* Decoded SecTAG (zero-copy view, SCI copied out since it is small). */ +struct macsec_sectag { + uint32_t pn; /* packet number */ + size_t len; /* 8 or 16 (incl. MACsec EtherType) */ + uint8_t tci_an; /* raw TCI/AN octet */ + uint8_t sl; /* short length (0 if >= 48) */ + uint8_t sci_present; /* 1 if SC bit set */ + uint8_t sci[MACSEC_SCI_LEN]; +}; + +/* Per-frame protect parameters. sak_len selects GCM-AES-128 (16) or -256 + * (32). When encrypt is 0 the frame is integrity-only (TCI E=0,C=0) and + * conf_offset is ignored; when encrypt is 1 the first conf_offset octets of + * the user data stay in cleartext and are authenticated, the rest encrypted. + * conf_offset must be 0, 30, or 50. include_sci sets the SC bit and copies + * sci into the SecTAG; sci is used for the GCM nonce regardless. */ +struct macsec_protect_params { + const uint8_t *da; /* 6 */ + const uint8_t *sa; /* 6 */ + const uint8_t *sci; /* 8 */ + const uint8_t *sak; + size_t sak_len; + size_t conf_offset; + uint32_t pn; + uint8_t an; /* 0..3 */ + uint8_t encrypt; /* 1 = confidentiality, 0 = integrity */ + uint8_t include_sci; + uint8_t end_station; /* TCI ES bit */ + uint8_t scb; /* TCI SCB bit */ +}; + +/* Per-frame validate parameters. sci is the Secure Channel's SCI used for the + * GCM nonce when the received SecTAG omits it; when the SecTAG carries an SCI + * that one is used instead. conf_offset must match the negotiated value. */ +struct macsec_validate_params { + const uint8_t *sak; + const uint8_t *sci; /* 8, fallback nonce SCI (may be NULL)*/ + size_t sak_len; + size_t conf_offset; +}; + +/* Build a SecTAG at out (capacity out_cap). Returns the SecTAG length written + * (8 without SCI, 16 with SCI; the MACsec EtherType is included) or a negative + * error. secure_data_len sets the SL field. */ +int macsec_sectag_build(uint8_t *out, size_t out_cap, + const struct macsec_protect_params *p, + size_t secure_data_len); + +/* Parse a SecTAG from in (starting at the MACsec EtherType). Returns 0 on + * success, negative on malformed input. */ +int macsec_sectag_parse(const uint8_t *in, size_t in_len, + struct macsec_sectag *out); + +/* Protect a user MSDU into a MACsec frame. payload starts at the original + * EtherType (it becomes the first octets of the Secure Data). out receives + * DA || SA || SecTAG || Secure Data || ICV; *out_len is set on success. + * Returns 0 on success, negative wolfCrypt/arg error otherwise. */ +int macsec_protect(const struct macsec_protect_params *p, + const uint8_t *payload, size_t payload_len, + uint8_t *out, size_t out_cap, size_t *out_len); + +/* Validate and recover a MACsec frame. frame is DA || SA || SecTAG || + * Secure Data || ICV. On success the recovered user MSDU (starting at the + * original EtherType) is written to out_payload, *out_payload_len set, the + * decoded SecTAG returned via *out_tag (for PN / AN / replay handling by the + * caller). Returns 0 on success, -1 on ICV failure, negative on arg error. */ +int macsec_validate(const struct macsec_validate_params *p, + const uint8_t *frame, size_t frame_len, + uint8_t *out_payload, size_t out_cap, + size_t *out_payload_len, + struct macsec_sectag *out_tag); + +#ifdef __cplusplus +} +#endif + +#endif /* WOLFIP_MACSEC_SECY_H */ diff --git a/src/macsec/macsec_test.h b/src/macsec/macsec_test.h new file mode 100644 index 00000000..50e9e40e --- /dev/null +++ b/src/macsec/macsec_test.h @@ -0,0 +1,76 @@ +/* macsec_test.h + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfIP. + * + * wolfIP 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 2 of the License, or + * (at your option) any later version. + * + * wolfIP 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +/* Shared helpers for the standalone macsec test binaries: a pass/fail + * reporter, a hex compare, and hex/payload builders. Header-only (static + * inline) so each test stays a single self-contained executable. */ + +#ifndef WOLFIP_MACSEC_TEST_H +#define WOLFIP_MACSEC_TEST_H + +#include +#include +#include + +static inline int expect_true(int cond, const char *label) +{ + printf(cond ? " [OK] %s\n" : " [FAIL] %s\n", label); + return cond ? 0 : 1; +} + +static inline int hex_eq(const uint8_t *got, const uint8_t *expect, size_t n, + const char *label) +{ + size_t i; + if (memcmp(got, expect, n) == 0) { + printf(" [OK] %s\n", label); + return 0; + } + printf(" [FAIL] %s\n got: ", label); + for (i = 0; i < n; i++) printf("%02x", got[i]); + printf("\n expect: "); + for (i = 0; i < n; i++) printf("%02x", expect[i]); + printf("\n"); + return 1; +} + +static inline size_t macsec_unhex(const char *h, uint8_t *o, size_t cap) +{ + size_t n = 0; + unsigned v; + while (h[0] && h[1] && n < cap) { + if (sscanf(h, "%2x", &v) != 1) break; + o[n++] = (uint8_t)v; + h += 2; + } + return n; +} + +/* Deterministic MSDU: an IPv4 EtherType (0x0800) followed by a seeded ramp. */ +static inline void fill_payload(uint8_t *p, size_t n, uint8_t seed) +{ + size_t i; + p[0] = 0x08; + p[1] = 0x00; + for (i = 2; i < n; i++) p[i] = (uint8_t)(seed + i); +} + +#endif /* WOLFIP_MACSEC_TEST_H */ diff --git a/src/macsec/mka_wolfmka.c b/src/macsec/mka_wolfmka.c new file mode 100644 index 00000000..45958a75 --- /dev/null +++ b/src/macsec/mka_wolfmka.c @@ -0,0 +1,390 @@ +/* mka_wolfmka.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfIP. + * + * wolfIP 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 2 of the License, or + * (at your option) any later version. + * + * wolfIP 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include "mka_wolfmka.h" +#include "supplicant_features.h" +#include "macsec_crypto.h" /* macsec_derive_cak / macsec_derive_ckn */ +#include "wpa_crypto.h" /* wpa_secure_zero */ + +#include +#include +#include + +#include +#include +#include + +/* ---- MkaSecyOps over the wolfIP software SecY (macsec_secy/macsec_sa) ---- + * + * wolfMKA calls set_cipher_suite before the SAs are installed, then + * install_tx_sa / install_rx_sa with the agreed SAK, then enable_transmit / + * enable_receive. The per-frame protect/validate stays in macsec_secy.c on the + * wolfIP datapath; these callbacks only program the keys. */ + +static int secy_create_tx_sc(void *ctx, const uint8_t sci[MKA_SCI_LEN]) +{ + (void)ctx; (void)sci; /* the TX channel is set up on install_tx_sa */ + return 0; +} + +static int secy_create_rx_sc(void *ctx, const uint8_t sci[MKA_SCI_LEN]) +{ + (void)ctx; (void)sci; + return 0; +} + +static int secy_install_tx_sa(void *ctx, const uint8_t sci[MKA_SCI_LEN], + uint8_t an, uint32_t kn, const uint8_t *sak, + size_t sak_len) +{ + struct mka_wolfmka *m = (struct mka_wolfmka *)ctx; + (void)kn; + if (m->tx == NULL) { + return -1; + } + if (macsec_tx_sc_set_key(m->tx, sak, sak_len, sci, an, m->encrypt, + m->conf_offset, 1 /* include_sci */, + 1 /* initial PN */) != 0) { + return -1; + } + m->installed = 1; /* a SAK has been agreed and installed */ + return 0; +} + +static int secy_install_rx_sa(void *ctx, const uint8_t sci[MKA_SCI_LEN], + uint8_t an, uint32_t kn, const uint8_t *sak, + size_t sak_len) +{ + struct mka_wolfmka *m = (struct mka_wolfmka *)ctx; + (void)kn; + if (m->rx == NULL) { + return -1; + } + if (macsec_rx_sc_set_key(m->rx, sak, sak_len, sci, an, + 1 /* replay_protect */, 0 /* window */, + m->conf_offset) != 0) { + return -1; + } + return 0; +} + +static int secy_enable_transmit(void *ctx, uint8_t an, bool enable) +{ + (void)ctx; (void)an; (void)enable; + return 0; /* the software SecY protects once the key is set */ +} + +static int secy_enable_receive(void *ctx, const uint8_t sci[MKA_SCI_LEN], + uint8_t an, bool enable) +{ + (void)ctx; (void)sci; (void)an; (void)enable; + return 0; +} + +static int secy_delete_sa(void *ctx, const uint8_t *sci, uint8_t an) +{ + struct mka_wolfmka *m = (struct mka_wolfmka *)ctx; + (void)an; + /* Scrub the SAK as the SA is torn down so no key material survives in the + * caller-owned SC and a re-enable cannot reuse a stale key without a + * fresh install. */ + if (sci == NULL) { + if (m->tx != NULL) { + wpa_secure_zero(m->tx->sak, sizeof(m->tx->sak)); + m->tx->sak_len = 0; + m->tx->in_use = 0; + } + } + else if (m->rx != NULL) { + wpa_secure_zero(m->rx->sak, sizeof(m->rx->sak)); + m->rx->sak_len = 0; + m->rx->in_use = 0; + } + return 0; +} + +static int secy_get_next_pn(void *ctx, uint8_t an, uint64_t *next_pn) +{ + struct mka_wolfmka *m = (struct mka_wolfmka *)ctx; + (void)an; + if (m->tx == NULL || next_pn == NULL) { + return -1; + } + *next_pn = (uint64_t)m->tx->next_pn; + return 0; +} + +static int secy_set_lowest_pn(void *ctx, const uint8_t sci[MKA_SCI_LEN], + uint8_t an, uint64_t lowest_pn) +{ + struct mka_wolfmka *m = (struct mka_wolfmka *)ctx; + (void)sci; (void)an; + if (m->rx == NULL) { + return -1; + } + if (lowest_pn > m->rx->lowest_pn) { + m->rx->lowest_pn = (uint32_t)lowest_pn; + } + return 0; +} + +static int secy_set_cipher_suite(void *ctx, const uint8_t sci[MKA_SCI_LEN], + uint64_t cipher_suite, uint32_t ssci, + uint8_t confidentiality_offset) +{ + struct mka_wolfmka *m = (struct mka_wolfmka *)ctx; + (void)sci; (void)cipher_suite; (void)ssci; + + if (confidentiality_offset == MKA_CONF_OFFSET_NONE) { + m->encrypt = 0; /* integrity only */ + m->conf_offset = 0; + } + else { + m->encrypt = 1; + if (confidentiality_offset == MKA_CONF_OFFSET_30) { + m->conf_offset = 30; + } + else if (confidentiality_offset == MKA_CONF_OFFSET_50) { + m->conf_offset = 50; + } + else { + m->conf_offset = 0; + } + } + return 0; +} + +/* ---- event hooks (diagnostics, opt-in via WOLFIP_MKA_WOLFMKA_DEBUG) ---- */ +#ifdef WOLFIP_MKA_WOLFMKA_DEBUG +static void ev_peer_added(void *ctx, const uint8_t mi[MKA_MI_LEN], + const uint8_t sci[MKA_SCI_LEN]) +{ (void)ctx; (void)mi; (void)sci; fprintf(stderr, "[wolfmka] peer_added\n"); } +static void ev_peer_live(void *ctx, const uint8_t mi[MKA_MI_LEN]) +{ (void)ctx; (void)mi; fprintf(stderr, "[wolfmka] peer_live\n"); } +static void ev_key_server_elected(void *ctx, const uint8_t sci[MKA_SCI_LEN], + bool is_self) +{ (void)ctx; (void)sci; fprintf(stderr, "[wolfmka] key_server_elected self=%d\n", is_self); } +static void ev_sak_installed(void *ctx, uint32_t kn, uint8_t an, + uint64_t cipher_suite) +{ (void)ctx; (void)cipher_suite; fprintf(stderr, "[wolfmka] sak_installed kn=%u an=%u\n", kn, an); } +static void ev_auth_fail(void *ctx) +{ (void)ctx; fprintf(stderr, "[wolfmka] auth_fail (ICV)\n"); } + +static void mka_wolfmka_event_ops(MkaEventOps *ev, struct mka_wolfmka *m) +{ + memset(ev, 0, sizeof(*ev)); + ev->peer_added = ev_peer_added; + ev->peer_live = ev_peer_live; + ev->key_server_elected = ev_key_server_elected; + ev->sak_installed = ev_sak_installed; + ev->auth_fail = ev_auth_fail; + ev->ctx = m; +} +#endif + +static void mka_wolfmka_secy_ops(MkaSecyOps *ops, struct mka_wolfmka *m) +{ + memset(ops, 0, sizeof(*ops)); + ops->create_tx_sc = secy_create_tx_sc; + ops->create_rx_sc = secy_create_rx_sc; + ops->install_tx_sa = secy_install_tx_sa; + ops->install_rx_sa = secy_install_rx_sa; + ops->enable_transmit = secy_enable_transmit; + ops->enable_receive = secy_enable_receive; + ops->delete_sa = secy_delete_sa; + ops->get_next_pn = secy_get_next_pn; + ops->set_lowest_pn = secy_set_lowest_pn; + ops->set_cipher_suite = secy_set_cipher_suite; + ops->ctx = m; +} + +/* ---- transmit callback bridge ---- + * + * wolfMKA hands us the bare EAPOL-MKA PDU (from the EAPOL version octet); it + * reconstructs the Ethernet DA/SA internally for the ICV. We prepend the L2 + * header (DA = PAE group, SA = our MAC, EtherType 0x888E) so the integrator's + * send() and mka_wolfmka_rx() both deal in whole L2 frames. */ + +static const uint8_t MKA_PAE_GROUP[6] = { 0x01, 0x80, 0xC2, 0x00, 0x00, 0x03 }; +#define MKA_L2_HDR_LEN 14U + +static int mka_wolfmka_send(void *ctx, const uint8_t *pdu, size_t len) +{ + struct mka_wolfmka *m = (struct mka_wolfmka *)ctx; + uint8_t frame[1600]; + + if (m->send == NULL || (MKA_L2_HDR_LEN + len) > sizeof(frame)) { + return -1; + } + memcpy(frame, MKA_PAE_GROUP, 6); + memcpy(frame + 6, m->src_mac, 6); + frame[12] = 0x88; + frame[13] = 0x8E; + memcpy(frame + MKA_L2_HDR_LEN, pdu, len); + return m->send(m->send_ctx, frame, MKA_L2_HDR_LEN + len); +} + +/* ---- public API ---- */ + +int mka_wolfmka_init_psk(struct mka_wolfmka *m, + mka_wolfmka_send_fn send, void *send_ctx, + const uint8_t *cak, size_t cak_len, + const uint8_t *ckn, size_t ckn_len, + const uint8_t sci[8], uint8_t priority, + uint8_t key_server_capable, size_t sak_len, + struct macsec_tx_sc *tx, struct macsec_rx_sc *rx) +{ + MkaParticipantConfig cfg; + + if (m == NULL || cak == NULL || ckn == NULL || sci == NULL) { + return -1; + } + if (cak_len > MKA_MAX_CAK_LEN || ckn_len == 0 || ckn_len > MKA_MAX_CKN_LEN) { + return -1; + } + if (sak_len != 16 && sak_len != 32) { + return -1; + } + + memset(m, 0, sizeof(*m)); + m->send = send; + m->send_ctx = send_ctx; + m->tx = tx; + m->rx = rx; + memcpy(m->src_mac, sci, 6); /* L2 source address for outbound frames */ + m->encrypt = 1; /* default; refined by set_cipher_suite */ + m->conf_offset = 0; + + m->rng = wm_Crypto_RandomInit(); + m->cmac = wm_Crypto_CmacInit(); + if (m->cmac == NULL) { /* CMAC workspace is mandatory */ + mka_wolfmka_free(m); + return -1; + } + + memset(&cfg, 0, sizeof(cfg)); + memcpy(cfg.cak.cak, cak, cak_len); + cfg.cak.cak_len = cak_len; + memcpy(cfg.cak.ckn, ckn, ckn_len); + cfg.cak.ckn_len = ckn_len; + memcpy(cfg.sci, sci, MKA_SCI_LEN); + cfg.cipher_suite = 0; /* 0 => GCM-AES-128 default */ + cfg.sak_len = (uint8_t)sak_len; + cfg.priority = priority; + cfg.macsec_capability = MKA_CAP_INTEGRITY_CONF; /* int + conf, offset 0 */ + cfg.confidentiality_offset = MKA_CONF_OFFSET_0; + cfg.macsec_desired = true; + cfg.key_server_capable = key_server_capable ? true : false; + cfg.role = MKA_ROLE_AUTO; + cfg.send = mka_wolfmka_send; + cfg.send_ctx = m; + cfg.rng = m->rng; + cfg.cmac = m->cmac; + mka_wolfmka_secy_ops(&cfg.secy, m); +#ifdef WOLFIP_MKA_WOLFMKA_DEBUG + mka_wolfmka_event_ops(&cfg.events, m); +#endif + + if (wm_Participant_Init(&m->p, &cfg) != MKA_OK) { + mka_wolfmka_free(m); + return -1; + } + return 0; +} + +int mka_wolfmka_init_eap(struct mka_wolfmka *m, + mka_wolfmka_send_fn send, void *send_ctx, + const uint8_t *msk, size_t msk_len, size_t cak_len, + const uint8_t *eap_session_id, size_t session_id_len, + const uint8_t peer_mac[6], const uint8_t auth_mac[6], + const uint8_t sci[8], uint8_t priority, + uint8_t key_server_capable, + struct macsec_tx_sc *tx, struct macsec_rx_sc *rx) +{ + uint8_t cak[MACSEC_KEY_LEN_MAX]; + uint8_t ckn[MACSEC_KEY_LEN_MAX]; + int ret; + + if (msk == NULL || eap_session_id == NULL) { + return -1; + } + if (cak_len != MACSEC_KEY_LEN_128 && cak_len != MACSEC_KEY_LEN_256) { + return -1; + } + if (macsec_derive_cak(msk, msk_len, cak_len, peer_mac, auth_mac, cak) != 0) { + return -1; + } + if (macsec_derive_ckn(msk, msk_len, eap_session_id, session_id_len, + peer_mac, auth_mac, ckn, cak_len) != 0) { + wpa_secure_zero(cak, sizeof(cak)); + return -1; + } + ret = mka_wolfmka_init_psk(m, send, send_ctx, cak, cak_len, ckn, cak_len, + sci, priority, key_server_capable, cak_len, + tx, rx); + wpa_secure_zero(cak, sizeof(cak)); + wpa_secure_zero(ckn, sizeof(ckn)); + return ret; +} + +int mka_wolfmka_rx(struct mka_wolfmka *m, const uint8_t *frame, size_t len, + uint32_t now_ms) +{ + if (m == NULL || frame == NULL || len <= MKA_L2_HDR_LEN) { + return -1; + } + /* Strip the L2 header: wolfMKA parses from the EAPOL PDU. */ + return (wm_Participant_Receive(&m->p, frame + MKA_L2_HDR_LEN, + len - MKA_L2_HDR_LEN, now_ms) == MKA_OK) + ? 0 : -1; +} + +int mka_wolfmka_tick(struct mka_wolfmka *m, uint32_t now_ms) +{ + bool sent = false; + if (m == NULL) { + return -1; + } + return (wm_Participant_Run(&m->p, now_ms, &sent) == MKA_OK) ? 0 : -1; +} + +int mka_wolfmka_installed(const struct mka_wolfmka *m) +{ + return (m != NULL && m->installed) ? 1 : 0; +} + +void mka_wolfmka_free(struct mka_wolfmka *m) +{ + if (m == NULL) { + return; + } + wm_Participant_Free(&m->p); + if (m->cmac != NULL) { + wm_Crypto_CmacFree(m->cmac); + } + if (m->rng != NULL) { + wm_Crypto_RandomFree(m->rng); + } + /* Zeroise the wrapper: it holds no key material of its own, but scrubbing + * clears the participant state and the tx/rx SC pointers so nothing stale + * is reused after teardown. */ + wpa_secure_zero(m, sizeof(*m)); +} diff --git a/src/macsec/mka_wolfmka.h b/src/macsec/mka_wolfmka.h new file mode 100644 index 00000000..a80723f9 --- /dev/null +++ b/src/macsec/mka_wolfmka.h @@ -0,0 +1,114 @@ +/* mka_wolfmka.h + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfIP. + * + * wolfIP 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 2 of the License, or + * (at your option) any later version. + * + * wolfIP 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +/* Adapter that runs the wolfDen wolfMKA KaY (MACsec Key Agreement control + * plane) as a wolfIP MKA backend, driving wolfIP's software SecY + * (macsec_secy.c / macsec_sa.c) as the data plane. Built when WOLFMKA_DIR + * points at a local wolfMKA clone (https://github.com/wolfSSL/wolfDen/tree/main/mka). + * + * The adapter wires three seams: wolfMKA's transmit callback to a wolfIP L2 + * send, wolfMKA's MkaSecyOps to the caller's software SecY transmit/receive + * Secure Channels, and wolfMKA's RNG/CMAC workspace pools to wolfSSL. The + * integrator feeds received EAPOL-MKA frames to mka_wolfmka_rx() and calls + * mka_wolfmka_tick() each poll iteration. + */ + +#ifndef WOLFIP_MKA_WOLFMKA_H +#define WOLFIP_MKA_WOLFMKA_H + +#include +#include + +#include "macsec_sa.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Transmit an EAPOL-MKA frame (full L2 frame, dst = 01:80:C2:00:00:03). + * Returns 0 on success. */ +typedef int (*mka_wolfmka_send_fn)(void *ctx, const uint8_t *frame, size_t len); + +/* One wolfMKA-backed MKA participant plus the wolfIP glue. Statically + * allocatable. The tx/rx Secure Channels are owned by the caller (the wolfIP + * datapath); wolfMKA installs SAKs into them through the SecY callbacks. */ +struct mka_wolfmka { + MkaParticipant p; + MkaRandom *rng; + MkaCmacCtx *cmac; + struct macsec_tx_sc *tx; /* software SecY transmit channel */ + struct macsec_rx_sc *rx; /* software SecY receive channel */ + mka_wolfmka_send_fn send; + void *send_ctx; + uint8_t src_mac[6]; /* our MAC (SCI[0:6]) for the L2 header */ + size_t conf_offset; /* 0/30/50 octets, from set_cipher_suite */ + uint8_t encrypt; /* 0 = integrity only */ + uint8_t installed; /* a transmit SA has been enabled */ +}; + +/* Initialise with a pre-shared CAK/CKN. sci is our Secure Channel Identifier + * (MAC || port). priority is the Key Server priority; key_server_capable lets + * us be elected. tx/rx are the caller's software SecY channels. Returns 0 on + * success, negative on error. */ +int mka_wolfmka_init_psk(struct mka_wolfmka *m, + mka_wolfmka_send_fn send, void *send_ctx, + const uint8_t *cak, size_t cak_len, + const uint8_t *ckn, size_t ckn_len, + const uint8_t sci[8], uint8_t priority, + uint8_t key_server_capable, size_t sak_len, + struct macsec_tx_sc *tx, struct macsec_rx_sc *rx); + +/* Initialise from an EAP-TLS MSK. Derives the CAK and CKN (802.1X-2010 9.3.1) + * from the MSK, the two peer MAC addresses, and the EAP Session-Id using + * wolfIP's key hierarchy (macsec_crypto.c), then behaves as + * mka_wolfmka_init_psk. cak_len is 16 or 32. Returns 0 on success. + * + * NOTE: the CAK/CKN-from-MSK context ordering is not yet cross-checked against + * wpa_supplicant EAP-MKA; two wolfIP/wolfMKA peers sharing an MSK agree, but + * interop with wpa's EAP path is unverified (see macsec_crypto.h). */ +int mka_wolfmka_init_eap(struct mka_wolfmka *m, + mka_wolfmka_send_fn send, void *send_ctx, + const uint8_t *msk, size_t msk_len, size_t cak_len, + const uint8_t *eap_session_id, size_t session_id_len, + const uint8_t peer_mac[6], const uint8_t auth_mac[6], + const uint8_t sci[8], uint8_t priority, + uint8_t key_server_capable, + struct macsec_tx_sc *tx, struct macsec_rx_sc *rx); + +/* Feed a received EAPOL-MKA frame (full L2 frame). Returns 0 on success. */ +int mka_wolfmka_rx(struct mka_wolfmka *m, const uint8_t *frame, size_t len, + uint32_t now_ms); + +/* Periodic service: runs timers, may transmit MKPDUs and install SAKs. */ +int mka_wolfmka_tick(struct mka_wolfmka *m, uint32_t now_ms); + +/* 1 once a transmit SA has been agreed and enabled (for status / tests). */ +int mka_wolfmka_installed(const struct mka_wolfmka *m); + +/* Tear down and zeroise. */ +void mka_wolfmka_free(struct mka_wolfmka *m); + +#ifdef __cplusplus +} +#endif + +#endif /* WOLFIP_MKA_WOLFMKA_H */ diff --git a/src/macsec/test_macsec_crypto.c b/src/macsec/test_macsec_crypto.c new file mode 100644 index 00000000..b05371ed --- /dev/null +++ b/src/macsec/test_macsec_crypto.c @@ -0,0 +1,243 @@ +/* test_macsec_crypto.c + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * Stand-alone test for src/macsec/macsec_crypto.c. Verifies: + * 1. SAK wrap/unwrap: RFC 3394 4.1 vector + 128/256-bit round-trip. + * 2. KDF construction: macsec_kdf output for a single 128-bit block equals + * a direct AES-CMAC over the exact IEEE 802.1X-2010 6.2.1 input + * (i || label || 0x00 || context || len_bits), locking the byte layout. + * 3. KDF properties: determinism, label independence, and that a 256-bit + * output's two blocks differ (counter increments). + * 4. KEK vs ICK independence (same CAK/CKN, different label -> different key). + * 5. MKPDU ICV compute / constant-time verify + tamper rejection. + * 6. SAK generation: determinism, KN sensitivity, length. + * + * The exact CAK/CKN/KEK/ICK/SAK byte values are locked against captured + * wpa_supplicant / kernel-macsec vectors in the M4 interop tests. + */ + +#include +#include +#include + +#include "supplicant_features.h" /* load wolfSSL config before wolfcrypt */ +#include + +#include "macsec_crypto.h" +#include "macsec_test.h" + +static int test_sak_wrap(void) +{ + static const uint8_t kek[16] = { + 0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07, + 0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f + }; + static const uint8_t sak[16] = { + 0x00,0x11,0x22,0x33,0x44,0x55,0x66,0x77, + 0x88,0x99,0xaa,0xbb,0xcc,0xdd,0xee,0xff + }; + static const uint8_t expect_wrap[24] = { + 0x1f,0xa6,0x8b,0x0a,0x81,0x12,0xb4,0x47, + 0xae,0xf3,0x4b,0xd8,0xfb,0x5a,0x7b,0x82, + 0x9d,0x3e,0x86,0x23,0x71,0xd2,0xcf,0xe5 + }; + uint8_t kek256[32]; + uint8_t sak256[32]; + uint8_t wrapped[40]; + uint8_t recovered[32]; + int fails = 0; + size_t i; + + printf("Test 1: SAK wrap/unwrap (RFC 3394 4.1 + round-trip)\n"); + if (macsec_wrap_sak(kek, sizeof(kek), sak, sizeof(sak), wrapped) != 0) { + printf(" [FAIL] macsec_wrap_sak (128) error\n"); + return 1; + } + fails += hex_eq(wrapped, expect_wrap, sizeof(expect_wrap), + "GCM-AES-128 SAK wrap matches RFC 3394"); + if (macsec_unwrap_sak(kek, sizeof(kek), wrapped, 24, recovered) != 0) { + printf(" [FAIL] macsec_unwrap_sak (128) error\n"); + fails++; + } + else { + fails += hex_eq(recovered, sak, sizeof(sak), "128-bit SAK round-trip"); + } + + for (i = 0; i < sizeof(kek256); i++) { kek256[i] = (uint8_t)i; } + for (i = 0; i < sizeof(sak256); i++) { sak256[i] = (uint8_t)(0x80 + i); } + if (macsec_wrap_sak(kek256, sizeof(kek256), sak256, sizeof(sak256), + wrapped) != 0 + || macsec_unwrap_sak(kek256, sizeof(kek256), wrapped, 40, + recovered) != 0) { + printf(" [FAIL] 256-bit SAK wrap/unwrap error\n"); + fails++; + } + else { + fails += hex_eq(recovered, sak256, sizeof(sak256), + "256-bit SAK round-trip"); + } + return fails; +} + +static int test_kdf_construction(void) +{ + /* Verify macsec_kdf assembles i || label || 0x00 || context || len_bits + * exactly, by comparing a single 128-bit block against a direct CMAC. */ + static const uint8_t key[16] = { + 0x60,0x3d,0xeb,0x10,0x15,0xca,0x71,0xbe, + 0x2b,0x73,0xae,0xf0,0x85,0x7d,0x77,0x81 + }; + static const char label[] = "IEEE8021 ICK"; + static const uint8_t ctx[16] = { + 0xaa,0xbb,0xcc,0xdd,0xee,0xff,0x00,0x11, + 0x22,0x33,0x44,0x55,0x66,0x77,0x88,0x99 + }; + uint8_t input[64]; + uint8_t expect[16]; + uint8_t got[16]; + word32 expect_sz = sizeof(expect); + size_t n = 0; + int fails = 0; + + printf("Test 2: KDF input assembly vs direct AES-CMAC (single block)\n"); + input[n++] = 0x01; /* i = 1 */ + memcpy(input + n, label, strlen(label)); n += strlen(label); + input[n++] = 0x00; /* separator */ + memcpy(input + n, ctx, sizeof(ctx)); n += sizeof(ctx); + input[n++] = 0x00; /* len_bits hi (128)*/ + input[n++] = 0x80; /* len_bits lo */ + + if (wc_AesCmacGenerate(expect, &expect_sz, input, (word32)n, + key, sizeof(key)) != 0) { + printf(" [FAIL] reference wc_AesCmacGenerate error\n"); + return 1; + } + if (macsec_kdf(key, sizeof(key), label, ctx, sizeof(ctx), 128, got) != 0) { + printf(" [FAIL] macsec_kdf error\n"); + return 1; + } + fails += hex_eq(got, expect, sizeof(expect), + "macsec_kdf(128) == direct CMAC of spec input"); + return fails; +} + +static int test_kdf_properties(void) +{ + static const uint8_t key[16] = { + 0x11,0x22,0x33,0x44,0x55,0x66,0x77,0x88, + 0x99,0xaa,0xbb,0xcc,0xdd,0xee,0xff,0x00 + }; + static const uint8_t ctx[8] = { 1,2,3,4,5,6,7,8 }; + uint8_t a[16], b[16], c256[32], d[16]; + int fails = 0; + + printf("Test 3: KDF determinism / label independence / counter\n"); + macsec_kdf(key, sizeof(key), "IEEE8021 KEK", ctx, sizeof(ctx), 128, a); + macsec_kdf(key, sizeof(key), "IEEE8021 KEK", ctx, sizeof(ctx), 128, b); + fails += expect_true(memcmp(a, b, 16) == 0, "deterministic"); + macsec_kdf(key, sizeof(key), "IEEE8021 ICK", ctx, sizeof(ctx), 128, d); + fails += expect_true(memcmp(a, d, 16) != 0, "different label -> different"); + macsec_kdf(key, sizeof(key), "IEEE8021 KEK", ctx, sizeof(ctx), 256, c256); + fails += expect_true(memcmp(c256, c256 + 16, 16) != 0, + "256-bit output blocks differ (counter)"); + return fails; +} + +static int test_kek_ick(void) +{ + static const uint8_t cak[16] = { + 0xde,0xad,0xbe,0xef,0xde,0xad,0xbe,0xef, + 0xca,0xfe,0xba,0xbe,0xca,0xfe,0xba,0xbe + }; + static const uint8_t ckn[16] = { + 0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08, + 0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f,0x10 + }; + uint8_t kek[16], ick[16]; + int fails = 0; + + printf("Test 4: KEK / ICK derivation independence\n"); + if (macsec_derive_kek(cak, sizeof(cak), ckn, sizeof(ckn), kek) != 0 + || macsec_derive_ick(cak, sizeof(cak), ckn, sizeof(ckn), ick) != 0) { + printf(" [FAIL] derive error\n"); + return 1; + } + fails += expect_true(memcmp(kek, ick, 16) != 0, "KEK != ICK"); + return fails; +} + +static int test_mkpdu_icv(void) +{ + static const uint8_t ick[16] = { + 0xa1,0xb2,0xc3,0xd4,0xe5,0xf6,0x07,0x18, + 0x29,0x3a,0x4b,0x5c,0x6d,0x7e,0x8f,0x90 + }; + uint8_t mkpdu[80]; + uint8_t icv[16]; + size_t i; + int fails = 0; + + printf("Test 5: MKPDU ICV compute / verify / tamper\n"); + for (i = 0; i < sizeof(mkpdu); i++) { mkpdu[i] = (uint8_t)(i * 7U); } + + if (macsec_mkpdu_icv(ick, sizeof(ick), mkpdu, sizeof(mkpdu), icv) != 0) { + printf(" [FAIL] icv compute error\n"); + return 1; + } + fails += expect_true( + macsec_mkpdu_icv_verify(ick, sizeof(ick), mkpdu, sizeof(mkpdu), icv) + == 0, "ICV verifies"); + mkpdu[10] ^= 0x01; + fails += expect_true( + macsec_mkpdu_icv_verify(ick, sizeof(ick), mkpdu, sizeof(mkpdu), icv) + != 0, "tampered MKPDU rejected"); + return fails; +} + +static int test_sak_generation(void) +{ + static const uint8_t cak[16] = { + 0x00,0x11,0x22,0x33,0x44,0x55,0x66,0x77, + 0x88,0x99,0xaa,0xbb,0xcc,0xdd,0xee,0xff + }; + uint8_t nonce[MACSEC_KS_NONCE_LEN]; + uint8_t mi_list[2 * MACSEC_MI_LEN]; + uint8_t sak_a[16], sak_b[16], sak_c[16]; + size_t i; + int fails = 0; + + printf("Test 6: SAK generation determinism / KN sensitivity\n"); + for (i = 0; i < sizeof(nonce); i++) { nonce[i] = (uint8_t)i; } + for (i = 0; i < sizeof(mi_list); i++) { mi_list[i] = (uint8_t)(0x40 + i); } + + if (macsec_generate_sak(cak, sizeof(cak), 16, nonce, mi_list, + sizeof(mi_list), 1, sak_a) != 0 + || macsec_generate_sak(cak, sizeof(cak), 16, nonce, mi_list, + sizeof(mi_list), 1, sak_b) != 0 + || macsec_generate_sak(cak, sizeof(cak), 16, nonce, mi_list, + sizeof(mi_list), 2, sak_c) != 0) { + printf(" [FAIL] SAK generate error\n"); + return 1; + } + fails += expect_true(memcmp(sak_a, sak_b, 16) == 0, "same inputs -> same SAK"); + fails += expect_true(memcmp(sak_a, sak_c, 16) != 0, "different KN -> different SAK"); + return fails; +} + +int main(void) +{ + int fails = 0; + setvbuf(stdout, NULL, _IONBF, 0); + + fails += test_sak_wrap(); + fails += test_kdf_construction(); + fails += test_kdf_properties(); + fails += test_kek_ick(); + fails += test_mkpdu_icv(); + fails += test_sak_generation(); + + printf("\n%s: macsec_crypto (%d failure%s)\n", + fails == 0 ? "PASS" : "FAIL", fails, fails == 1 ? "" : "s"); + return fails == 0 ? 0 : 1; +} diff --git a/src/macsec/test_macsec_sa.c b/src/macsec/test_macsec_sa.c new file mode 100644 index 00000000..34ed37ef --- /dev/null +++ b/src/macsec/test_macsec_sa.c @@ -0,0 +1,224 @@ +/* test_macsec_sa.c + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * Stand-alone loopback test for src/macsec/macsec_sa.c (the stateful SecY SA + * layer). A transmit SC and a receive SC share a SAK; frames are protected by + * the TX SC and validated by the RX SC. Verifies: + * 1. In-order delivery of a burst (PN increments, all accepted). + * 2. Replay window: reorder within the window accepted, below-window + * rejected as replay. + * 3. Strict mode (window 0): duplicates / old PNs rejected. + * 4. Integrity-only loopback. + * 5. Tamper -> authentication failure. + * 6. PN exhaustion -> transmit refused (rekey needed). + */ + +#include +#include +#include + +#include "macsec_sa.h" +#include "macsec_test.h" + +#define BURST 8 +#define FRAME_CAP 256 + +static const uint8_t DA[6] = { 0x02,0,0,0,0,0xbb }; +static const uint8_t SA[6] = { 0x02,0,0,0,0,0xaa }; +static const uint8_t SCI[8] = { 0x02,0,0,0,0,0xaa,0x00,0x01 }; +static const uint8_t SAK[16] = { + 0xad,0x7a,0x2b,0xd0,0x3e,0xac,0x83,0x5a, + 0x6f,0x62,0x0f,0xdc,0xb5,0x06,0xb3,0x45 +}; + +/* Protect a burst of frames (PN 1..BURST) into frames[]/lens[]. */ +static int make_burst(struct macsec_tx_sc *tx, + uint8_t frames[BURST][FRAME_CAP], size_t lens[BURST]) +{ + uint8_t payload[48]; + int i; + for (i = 0; i < BURST; i++) { + fill_payload(payload, sizeof(payload), (uint8_t)i); + if (macsec_tx(tx, DA, SA, payload, sizeof(payload), + frames[i], FRAME_CAP, &lens[i]) != 0) { + return -1; + } + } + return 0; +} + +static int test_in_order(void) +{ + struct macsec_tx_sc tx; + struct macsec_rx_sc rx; + uint8_t frames[BURST][FRAME_CAP]; + size_t lens[BURST]; + uint8_t rec[FRAME_CAP]; + size_t rlen; + int i, fails = 0, ok = 1; + + printf("Test 1: in-order burst loopback\n"); + macsec_tx_sc_set_key(&tx, SAK, 16, SCI, 0, 1 /*encrypt*/, 0, 1, 1); + macsec_rx_sc_set_key(&rx, SAK, 16, SCI, 0, 1 /*replay*/, 4, 0); + if (make_burst(&tx, frames, lens) != 0) { + printf(" [FAIL] burst protect error\n"); return 1; + } + for (i = 0; i < BURST; i++) { + if (macsec_rx(&rx, frames[i], lens[i], rec, sizeof(rec), &rlen) + != MACSEC_RX_OK) { + ok = 0; + } + } + fails += expect_true(ok, "all in-order frames accepted"); + fails += expect_true(tx.next_pn == (uint32_t)(BURST + 1), + "TX PN advanced past the burst"); + return fails; +} + +static int test_replay_window(void) +{ + struct macsec_tx_sc tx; + struct macsec_rx_sc rx; + uint8_t frames[BURST][FRAME_CAP]; + size_t lens[BURST]; + uint8_t rec[FRAME_CAP]; + size_t rlen; + int fails = 0; + + printf("Test 2: replay window (reorder in, old out)\n"); + macsec_tx_sc_set_key(&tx, SAK, 16, SCI, 0, 1, 0, 1, 1); + macsec_rx_sc_set_key(&rx, SAK, 16, SCI, 0, 1 /*replay*/, 4 /*window*/, 0); + make_burst(&tx, frames, lens); /* frames[i] carries PN i+1 */ + + /* Accept PN 5 first (index 4). */ + fails += expect_true( + macsec_rx(&rx, frames[4], lens[4], rec, sizeof(rec), &rlen) + == MACSEC_RX_OK, "PN 5 accepted"); + /* PN 3 is within window (5-4+1=2 .. ) -> accepted. */ + fails += expect_true( + macsec_rx(&rx, frames[2], lens[2], rec, sizeof(rec), &rlen) + == MACSEC_RX_OK, "PN 3 within window accepted"); + /* PN 1 is below the window lower bound -> replay. */ + fails += expect_true( + macsec_rx(&rx, frames[0], lens[0], rec, sizeof(rec), &rlen) + == MACSEC_RX_REPLAY, "PN 1 below window rejected"); + return fails; +} + +static int test_strict_mode(void) +{ + struct macsec_tx_sc tx; + struct macsec_rx_sc rx; + uint8_t frames[BURST][FRAME_CAP]; + size_t lens[BURST]; + uint8_t rec[FRAME_CAP]; + size_t rlen; + int fails = 0; + + printf("Test 3: strict mode (window 0) duplicate rejection\n"); + macsec_tx_sc_set_key(&tx, SAK, 16, SCI, 0, 1, 0, 1, 1); + macsec_rx_sc_set_key(&rx, SAK, 16, SCI, 0, 1, 0 /*window=0*/, 0); + make_burst(&tx, frames, lens); + + fails += expect_true( + macsec_rx(&rx, frames[2], lens[2], rec, sizeof(rec), &rlen) + == MACSEC_RX_OK, "PN 3 accepted"); + fails += expect_true( + macsec_rx(&rx, frames[2], lens[2], rec, sizeof(rec), &rlen) + == MACSEC_RX_REPLAY, "duplicate PN 3 rejected"); + fails += expect_true( + macsec_rx(&rx, frames[1], lens[1], rec, sizeof(rec), &rlen) + == MACSEC_RX_REPLAY, "older PN 2 rejected"); + fails += expect_true( + macsec_rx(&rx, frames[3], lens[3], rec, sizeof(rec), &rlen) + == MACSEC_RX_OK, "newer PN 4 accepted"); + return fails; +} + +static int test_integrity_only(void) +{ + struct macsec_tx_sc tx; + struct macsec_rx_sc rx; + uint8_t payload[48]; + uint8_t frame[FRAME_CAP]; + uint8_t rec[FRAME_CAP]; + size_t flen, rlen; + int fails = 0; + + printf("Test 4: integrity-only loopback\n"); + fill_payload(payload, sizeof(payload), 0x40); + macsec_tx_sc_set_key(&tx, SAK, 16, SCI, 0, 0 /*integrity*/, 0, 1, 1); + macsec_rx_sc_set_key(&rx, SAK, 16, SCI, 0, 1, 4, 0); + if (macsec_tx(&tx, DA, SA, payload, sizeof(payload), frame, + sizeof(frame), &flen) != 0) { + printf(" [FAIL] tx error\n"); return 1; + } + fails += expect_true( + macsec_rx(&rx, frame, flen, rec, sizeof(rec), &rlen) == MACSEC_RX_OK + && rlen == sizeof(payload) + && memcmp(rec, payload, sizeof(payload)) == 0, + "integrity-only frame recovered"); + return fails; +} + +static int test_tamper(void) +{ + struct macsec_tx_sc tx; + struct macsec_rx_sc rx; + uint8_t payload[48]; + uint8_t frame[FRAME_CAP]; + uint8_t rec[FRAME_CAP]; + size_t flen, rlen; + int fails = 0; + + printf("Test 5: tamper -> auth fail\n"); + fill_payload(payload, sizeof(payload), 0x20); + macsec_tx_sc_set_key(&tx, SAK, 16, SCI, 0, 1, 0, 1, 1); + macsec_rx_sc_set_key(&rx, SAK, 16, SCI, 0, 1, 4, 0); + macsec_tx(&tx, DA, SA, payload, sizeof(payload), frame, sizeof(frame), + &flen); + frame[flen - 2] ^= 0x01; /* corrupt the ICV */ + fails += expect_true( + macsec_rx(&rx, frame, flen, rec, sizeof(rec), &rlen) + == MACSEC_RX_AUTH_FAIL, "tampered frame -> AUTH_FAIL"); + return fails; +} + +static int test_pn_exhaustion(void) +{ + struct macsec_tx_sc tx; + uint8_t payload[48]; + uint8_t frame[FRAME_CAP]; + size_t flen; + int fails = 0; + + printf("Test 6: PN exhaustion\n"); + fill_payload(payload, sizeof(payload), 0x10); + macsec_tx_sc_set_key(&tx, SAK, 16, SCI, 0, 1, 0, 1, MACSEC_PN_MAX); + fails += expect_true( + macsec_tx(&tx, DA, SA, payload, sizeof(payload), frame, + sizeof(frame), &flen) == 0, "last PN transmitted"); + fails += expect_true( + macsec_tx(&tx, DA, SA, payload, sizeof(payload), frame, + sizeof(frame), &flen) == MACSEC_RX_BAD_ARG, + "transmit refused after PN exhaustion"); + return fails; +} + +int main(void) +{ + int fails = 0; + setvbuf(stdout, NULL, _IONBF, 0); + + fails += test_in_order(); + fails += test_replay_window(); + fails += test_strict_mode(); + fails += test_integrity_only(); + fails += test_tamper(); + fails += test_pn_exhaustion(); + + printf("\n%s: macsec_sa (%d failure%s)\n", + fails == 0 ? "PASS" : "FAIL", fails, fails == 1 ? "" : "s"); + return fails == 0 ? 0 : 1; +} diff --git a/src/macsec/test_macsec_secy.c b/src/macsec/test_macsec_secy.c new file mode 100644 index 00000000..b18b7f97 --- /dev/null +++ b/src/macsec/test_macsec_secy.c @@ -0,0 +1,236 @@ +/* test_macsec_secy.c + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * Stand-alone test for src/macsec/macsec_secy.c. Verifies: + * 1. SecTAG build/parse field round-trip (TCI bits, AN, PN, SL, SCI). + * 2. protect/validate round-trip: encrypt (GCM-AES-128 and -256), with and + * without an in-tag SCI. + * 3. Integrity-only round-trip (secure data left in cleartext on the wire). + * 4. Confidentiality offset 30 / 50 (prefix cleartext, remainder encrypted). + * 5. Tamper rejection: ciphertext, ICV, DA (AAD), PN, and wrong SAK. + * + * Exact 802.1AE frame KATs are locked against captured Linux `ip macsec` + * output in the M4 interop tests. + */ + +#include +#include +#include + +#include "macsec_secy.h" +#include "macsec_test.h" + +static const uint8_t g_da[6] = { 0x01,0x80,0xc2,0x00,0x00,0x03 }; +static const uint8_t g_sa[6] = { 0x02,0x00,0x00,0x00,0x00,0x22 }; +static const uint8_t g_sci[8] = { 0x02,0x00,0x00,0x00,0x00,0x22,0x00,0x01 }; +static const uint8_t g_sak128[16] = { + 0xad,0x7a,0x2b,0xd0,0x3e,0xac,0x83,0x5a, + 0x6f,0x62,0x0f,0xdc,0xb5,0x06,0xb3,0x45 +}; +static const uint8_t g_sak256[32] = { + 0xe3,0xc0,0x8a,0x8f,0x06,0xc6,0xe3,0xad, + 0x95,0xa7,0x05,0x57,0xb2,0x3f,0x75,0x48, + 0x3c,0xe3,0x30,0x21,0xa9,0xc7,0x2b,0x70, + 0x25,0x66,0x62,0x04,0xc6,0x9c,0x0b,0x72 +}; + +static void base_protect_params(struct macsec_protect_params *pp, + const uint8_t *sak, size_t sak_len, + int encrypt, size_t offset, int include_sci) +{ + memset(pp, 0, sizeof(*pp)); + pp->da = g_da; + pp->sa = g_sa; + pp->sci = g_sci; + pp->sak = sak; + pp->sak_len = sak_len; + pp->conf_offset = offset; + pp->pn = 0x00000001U; + pp->an = 1; + pp->encrypt = (uint8_t)encrypt; + pp->include_sci = (uint8_t)include_sci; +} + +static int roundtrip(const uint8_t *sak, size_t sak_len, int encrypt, + size_t offset, int include_sci, const char *what) +{ + struct macsec_protect_params pp; + struct macsec_validate_params vp; + struct macsec_sectag tag; + uint8_t payload[64]; + uint8_t frame[128]; + uint8_t recovered[128]; + size_t frame_len = 0, rec_len = 0; + int fails = 0; + + fill_payload(payload, sizeof(payload), 0); + base_protect_params(&pp, sak, sak_len, encrypt, offset, include_sci); + + if (macsec_protect(&pp, payload, sizeof(payload), frame, sizeof(frame), + &frame_len) != 0) { + printf(" [FAIL] %s: protect error\n", what); + return 1; + } + + memset(&vp, 0, sizeof(vp)); + vp.sak = sak; + vp.sak_len = sak_len; + vp.sci = g_sci; /* fallback when tag omits SCI */ + vp.conf_offset = offset; + + if (macsec_validate(&vp, frame, frame_len, recovered, sizeof(recovered), + &rec_len, &tag) != 0) { + printf(" [FAIL] %s: validate error\n", what); + return 1; + } + fails += expect_true(rec_len == sizeof(payload) + && memcmp(recovered, payload, sizeof(payload)) == 0, + what); + return fails; +} + +static int test_sectag(void) +{ + struct macsec_protect_params pp; + struct macsec_sectag tag; + uint8_t buf[MACSEC_SECTAG_MAX_LEN]; + int built; + int fails = 0; + + printf("Test 1: SecTAG build/parse\n"); + base_protect_params(&pp, g_sak128, 16, 1 /*encrypt*/, 0, 1 /*sci*/); + pp.pn = 0x11223344U; + pp.an = 2; + + built = macsec_sectag_build(buf, sizeof(buf), &pp, 40 /* < 48 -> SL */); + fails += expect_true(built == (int)MACSEC_SECTAG_MAX_LEN, "SCI tag len 16"); + if (macsec_sectag_parse(buf, sizeof(buf), &tag) != 0) { + printf(" [FAIL] parse error\n"); + return fails + 1; + } + fails += expect_true(tag.pn == 0x11223344U, "PN round-trip"); + fails += expect_true((tag.tci_an & MACSEC_AN_MASK) == 2, "AN round-trip"); + fails += expect_true(tag.sci_present + && memcmp(tag.sci, g_sci, 8) == 0, "SCI round-trip"); + fails += expect_true((tag.tci_an & MACSEC_TCI_E) && (tag.tci_an & MACSEC_TCI_C), + "E and C set for encrypt"); + fails += expect_true(tag.sl == 40, "SL carries short length"); + return fails; +} + +static int test_roundtrips(void) +{ + int fails = 0; + printf("Test 2: protect/validate round-trips\n"); + fails += roundtrip(g_sak128, 16, 1, 0, 1, "encrypt-128 with SCI"); + fails += roundtrip(g_sak256, 32, 1, 0, 1, "encrypt-256 with SCI"); + fails += roundtrip(g_sak128, 16, 1, 0, 0, "encrypt-128 no SCI"); + fails += roundtrip(g_sak128, 16, 0, 0, 1, "integrity-only-128"); + fails += roundtrip(g_sak128, 16, 1, 30, 1, "encrypt offset 30"); + fails += roundtrip(g_sak128, 16, 1, 50, 1, "encrypt offset 50"); + return fails; +} + +static int test_onwire_semantics(void) +{ + struct macsec_protect_params pp; + uint8_t payload[64]; + uint8_t frame[128]; + size_t frame_len = 0; + size_t hdr_len; + int fails = 0; + + printf("Test 3: on-wire cleartext/ciphertext placement\n"); + fill_payload(payload, sizeof(payload), 0); + + /* Integrity-only: secure data must equal the plaintext on the wire. */ + base_protect_params(&pp, g_sak128, 16, 0, 0, 1); + macsec_protect(&pp, payload, sizeof(payload), frame, sizeof(frame), + &frame_len); + hdr_len = 12 + MACSEC_SECTAG_MAX_LEN; + fails += expect_true(memcmp(frame + hdr_len, payload, sizeof(payload)) == 0, + "integrity-only leaves data in clear"); + + /* Offset 30: first 30 octets cleartext, remainder encrypted. */ + base_protect_params(&pp, g_sak128, 16, 1, 30, 1); + macsec_protect(&pp, payload, sizeof(payload), frame, sizeof(frame), + &frame_len); + fails += expect_true(memcmp(frame + hdr_len, payload, 30) == 0, + "offset prefix stays cleartext"); + fails += expect_true(memcmp(frame + hdr_len + 30, payload + 30, + sizeof(payload) - 30) != 0, + "offset remainder is encrypted"); + return fails; +} + +static int test_tamper(void) +{ + struct macsec_protect_params pp; + struct macsec_validate_params vp; + struct macsec_sectag tag; + uint8_t payload[64]; + uint8_t frame[128]; + uint8_t good[128]; + uint8_t recovered[128]; + size_t frame_len = 0, rec_len = 0; + size_t hdr_len = 12 + MACSEC_SECTAG_MAX_LEN; + int fails = 0; + + printf("Test 4: tamper / wrong-key rejection\n"); + fill_payload(payload, sizeof(payload), 0); + base_protect_params(&pp, g_sak128, 16, 1, 0, 1); + macsec_protect(&pp, payload, sizeof(payload), frame, sizeof(frame), + &frame_len); + memcpy(good, frame, frame_len); + + memset(&vp, 0, sizeof(vp)); + vp.sak = g_sak128; vp.sak_len = 16; vp.sci = g_sci; vp.conf_offset = 0; + + /* Sanity: unmodified frame validates. */ + fails += expect_true(macsec_validate(&vp, good, frame_len, recovered, + sizeof(recovered), &rec_len, &tag) == 0, + "clean frame validates"); + + /* Ciphertext bit flip. */ + memcpy(frame, good, frame_len); frame[hdr_len + 4] ^= 0x01; + fails += expect_true(macsec_validate(&vp, frame, frame_len, recovered, + sizeof(recovered), &rec_len, &tag) != 0, + "ciphertext tamper rejected"); + /* ICV bit flip. */ + memcpy(frame, good, frame_len); frame[frame_len - 1] ^= 0x80; + fails += expect_true(macsec_validate(&vp, frame, frame_len, recovered, + sizeof(recovered), &rec_len, &tag) != 0, + "ICV tamper rejected"); + /* DA (AAD) change. */ + memcpy(frame, good, frame_len); frame[0] ^= 0x01; + fails += expect_true(macsec_validate(&vp, frame, frame_len, recovered, + sizeof(recovered), &rec_len, &tag) != 0, + "DA (AAD) tamper rejected"); + /* PN change (alters GCM nonce). */ + memcpy(frame, good, frame_len); frame[12 + 7] ^= 0x01; + fails += expect_true(macsec_validate(&vp, frame, frame_len, recovered, + sizeof(recovered), &rec_len, &tag) != 0, + "PN tamper rejected"); + /* Wrong SAK. */ + memcpy(frame, good, frame_len); vp.sak = g_sak256; vp.sak_len = 32; + fails += expect_true(macsec_validate(&vp, frame, frame_len, recovered, + sizeof(recovered), &rec_len, &tag) != 0, + "wrong SAK rejected"); + return fails; +} + +int main(void) +{ + int fails = 0; + setvbuf(stdout, NULL, _IONBF, 0); + + fails += test_sectag(); + fails += test_roundtrips(); + fails += test_onwire_semantics(); + fails += test_tamper(); + + printf("\n%s: macsec_secy (%d failure%s)\n", + fails == 0 ? "PASS" : "FAIL", fails, fails == 1 ? "" : "s"); + return fails == 0 ? 0 : 1; +} diff --git a/src/port/rp2350_cyw43439/test_cyw43_sdpcm.c b/src/port/rp2350_cyw43439/test_cyw43_sdpcm.c index 7f1a75bc..d2917ded 100644 --- a/src/port/rp2350_cyw43439/test_cyw43_sdpcm.c +++ b/src/port/rp2350_cyw43439/test_cyw43_sdpcm.c @@ -102,7 +102,9 @@ static int test_credit(void) static int test_iovar_build(void) { uint8_t out[128]; - uint8_t val[4] = { 1, 2, 3, 4 }; + /* Sized for the largest vlen exercised below (13); the first four octets + * carry the checked pattern. */ + uint8_t val[16] = { 1, 2, 3, 4 }; uint32_t outlen = 0; int fails = 0; printf("Test 4: iovar build + overflow guard\n"); diff --git a/src/supplicant/eap.h b/src/supplicant/eap.h index 29b6d44f..ea748283 100644 --- a/src/supplicant/eap.h +++ b/src/supplicant/eap.h @@ -35,6 +35,7 @@ #define EAPOL_TYPE_EAPOL_START 0x01U #define EAPOL_TYPE_EAPOL_LOGOFF 0x02U #define EAPOL_TYPE_KEY_DESCRIPTOR 0x03U /* same as EAPOL-Key */ +#define EAPOL_TYPE_MKA 0x05U /* EAPOL-MKA (802.1X-2010) */ #define EAP_CODE_REQUEST 0x01U #define EAP_CODE_RESPONSE 0x02U diff --git a/src/supplicant/supplicant_features.h b/src/supplicant/supplicant_features.h index 306af3b5..9fb31a2e 100644 --- a/src/supplicant/supplicant_features.h +++ b/src/supplicant/supplicant_features.h @@ -48,6 +48,18 @@ #endif #endif +/* MACsec / MKA (IEEE 802.1AE + 802.1X-2010) needs AES-CMAC for the KDF and + * MKPDU ICV, AES-GCM for the SecY data plane, and RFC 3394 AES key wrap for + * the SAK. */ +#if defined(WOLFIP_ENABLE_MACSEC) && WOLFIP_ENABLE_MACSEC +#if !defined(WOLFSSL_CMAC) || !defined(HAVE_AESGCM) \ + || !defined(HAVE_AES_KEYWRAP) +#warning "wolfIP MACsec disabled (needs WOLFSSL_CMAC + HAVE_AESGCM + HAVE_AES_KEYWRAP)" +#undef WOLFIP_ENABLE_MACSEC +#define WOLFIP_ENABLE_MACSEC 0 +#endif +#endif + /* WPA2-PSK is the unconditional baseline and cannot be turned off: the * 4-way handshake needs AES (CCMP / key-wrap / CMAC), HMAC, SHA-1, * SHA-256, PBKDF2, and RFC 3394 AES key wrap. */ diff --git a/tools/macsec/macsec_probe.c b/tools/macsec/macsec_probe.c new file mode 100644 index 00000000..f99ae32a --- /dev/null +++ b/tools/macsec/macsec_probe.c @@ -0,0 +1,164 @@ +/* macsec_probe.c + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfIP. + * + * wolfIP 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 2 of the License, or + * (at your option) any later version. + * + * wolfIP 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +/* macsec_probe - a byte-level cross-check tool for the wolfIP SecY against the + * Linux kernel MACsec module. It applies the wolfIP 802.1AE transform to a + * single frame: + * + * macsec_probe protect \ + * + * -> prints the resulting MACsec frame (DA||SA||SecTAG||data||ICV) hex. + * + * macsec_probe validate + * -> decrypts/authenticates a MACsec frame and prints the recovered + * MSDU hex, or "FAIL" on ICV/replay error. + * + * The interop scripts capture a frame the kernel produced (tcpdump) and feed + * it to "validate", and feed a "protect" frame back into the kernel, to lock + * the wolfIP framing (SecTAG layout, SCI||PN nonce, DA||SA||SecTAG AAD) to the + * kernel byte for byte. include_sci is always set (the kernel default). + */ + +#include +#include +#include +#include + +#include "macsec_secy.h" + +#define MAX_BUF 2048 + +static int hex2bin(const char *hex, uint8_t *out, size_t out_cap, size_t *out_len) +{ + size_t hl = strlen(hex); + size_t i; + if ((hl & 1U) != 0 || (hl / 2U) > out_cap) { + return -1; + } + for (i = 0; i < hl; i += 2) { + unsigned v; + if (sscanf(hex + i, "%2x", &v) != 1) { + return -1; + } + out[i / 2] = (uint8_t)v; + } + *out_len = hl / 2U; + return 0; +} + +static void print_hex(const uint8_t *b, size_t n) +{ + size_t i; + for (i = 0; i < n; i++) { + printf("%02x", b[i]); + } + printf("\n"); +} + +static int do_protect(int argc, char **argv) +{ + struct macsec_protect_params p; + uint8_t sak[32], sci[8], da[6], sa[6]; + uint8_t payload[MAX_BUF], frame[MAX_BUF]; + size_t sak_len, sci_len, da_len, sa_len, pl_len, frame_len = 0; + + if (argc != 11) { + fprintf(stderr, "usage: protect sak sci an pn enc off da sa payload\n"); + return 2; + } + if (hex2bin(argv[2], sak, sizeof(sak), &sak_len) + || hex2bin(argv[3], sci, sizeof(sci), &sci_len) + || hex2bin(argv[8], da, sizeof(da), &da_len) + || hex2bin(argv[9], sa, sizeof(sa), &sa_len) + || hex2bin(argv[10], payload, sizeof(payload), &pl_len) + || sci_len != 8 || da_len != 6 || sa_len != 6) { + fprintf(stderr, "bad hex arg\n"); + return 2; + } + memset(&p, 0, sizeof(p)); + p.da = da; + p.sa = sa; + p.sci = sci; + p.sak = sak; + p.sak_len = sak_len; + p.an = (uint8_t)atoi(argv[4]); + p.pn = (uint32_t)strtoul(argv[5], NULL, 10); + p.encrypt = (uint8_t)atoi(argv[6]); + p.conf_offset = (size_t)atoi(argv[7]); + p.include_sci = 1; + + if (macsec_protect(&p, payload, pl_len, frame, sizeof(frame), + &frame_len) != 0) { + printf("FAIL\n"); + return 1; + } + print_hex(frame, frame_len); + return 0; +} + +static int do_validate(int argc, char **argv) +{ + struct macsec_validate_params vp; + struct macsec_sectag tag; + uint8_t sak[32], sci[8], frame[MAX_BUF], out[MAX_BUF]; + size_t sak_len, sci_len, frame_len, out_len = 0; + + if (argc != 6) { + fprintf(stderr, "usage: validate sak peer_sci off frame\n"); + return 2; + } + if (hex2bin(argv[2], sak, sizeof(sak), &sak_len) + || hex2bin(argv[3], sci, sizeof(sci), &sci_len) + || hex2bin(argv[5], frame, sizeof(frame), &frame_len) + || sci_len != 8) { + fprintf(stderr, "bad hex arg\n"); + return 2; + } + memset(&vp, 0, sizeof(vp)); + vp.sak = sak; + vp.sak_len = sak_len; + vp.sci = sci; + vp.conf_offset = (size_t)atoi(argv[4]); + + if (macsec_validate(&vp, frame, frame_len, out, sizeof(out), &out_len, + &tag) != 0) { + printf("FAIL\n"); + return 1; + } + print_hex(out, out_len); + return 0; +} + +int main(int argc, char **argv) +{ + if (argc < 2) { + fprintf(stderr, "usage: %s protect|validate ...\n", argv[0]); + return 2; + } + if (strcmp(argv[1], "protect") == 0) { + return do_protect(argc, argv); + } + if (strcmp(argv[1], "validate") == 0) { + return do_validate(argc, argv); + } + fprintf(stderr, "unknown command '%s'\n", argv[1]); + return 2; +} diff --git a/tools/macsec/macsec_sta.c b/tools/macsec/macsec_sta.c new file mode 100644 index 00000000..25a2c5b4 --- /dev/null +++ b/tools/macsec/macsec_sta.c @@ -0,0 +1,293 @@ +/* macsec_sta.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfIP. + * + * wolfIP 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 2 of the License, or + * (at your option) any later version. + * + * wolfIP 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +/* macsec_sta - a host harness that runs the wolfDen wolfMKA control plane + * (via the wolfIP adapter) over an AF_PACKET raw socket, driving wolfIP's + * software SecY, for interop against Linux wpa_supplicant's MKA. It carries + * EAPOL-MKA frames (EtherType 0x888E to the PAE group address), and prints + * SAK-INSTALLED once a SAK is agreed. With MACSEC_KERNEL_DEV set it also + * programs the SAK into that kernel macsec device for a data-plane test. + * + * macsec_sta [timeout_s] + * + * Linux only; built with WOLFMKA_DIR. Used by the run_macsec_mka_*.sh tests. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mka_wolfmka.h" + +static int g_fd = -1; +static int g_ifindex; + +static uint64_t now_ms(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000U + (uint64_t)(ts.tv_nsec / 1000000U); +} + +static size_t unhex(const char *h, uint8_t *o, size_t cap) +{ + size_t n = 0; + unsigned v; + while (h[0] && h[1] && n < cap) { + if (sscanf(h, "%2x", &v) != 1) { /* stop on the first non-hex pair */ + break; + } + o[n++] = (uint8_t)v; + h += 2; + } + return n; +} + +/* Accept only characters valid in a Linux interface name; MACSEC_KERNEL_DEV is + * spliced into an "ip macsec" command line, so reject anything a shell could + * treat specially. */ +static int iface_name_ok(const char *s) +{ + size_t i, n; + char c; + if (s == NULL) { + return 0; + } + n = strlen(s); + if (n == 0 || n >= IFNAMSIZ) { + return 0; + } + for (i = 0; i < n; i++) { + c = s[i]; + if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') + || (c >= '0' && c <= '9') || c == '_' || c == '.' || c == '-')) { + return 0; + } + } + return 1; +} + +static void run_ip(const char *cmd) +{ + if (system(cmd) != 0) { + fprintf(stderr, "warning: command failed: %s\n", cmd); + } +} + +static int cb_send(void *ctx, const uint8_t *frame, size_t len) +{ + struct sockaddr_ll sll; + (void)ctx; + memset(&sll, 0, sizeof(sll)); + sll.sll_family = AF_PACKET; + sll.sll_ifindex = g_ifindex; + sll.sll_halen = 6; + memcpy(sll.sll_addr, frame, 6); /* destination MAC */ + if (sendto(g_fd, frame, len, 0, (struct sockaddr *)&sll, sizeof(sll)) < 0) { + fprintf(stderr, "sendto: %s\n", strerror(errno)); + return -1; + } + return 0; +} + +static void print_sak(size_t sak_len, uint8_t an, const uint8_t *sak, + const uint8_t sci[8]) +{ + size_t i; + printf("SAK-INSTALLED len=%zu an=%u sak=", sak_len, an); + for (i = 0; i < sak_len; i++) printf("%02x", sak[i]); + printf(" sci="); + for (i = 0; i < 8; i++) printf("%02x", sci[i]); + printf("\n"); + fflush(stdout); +} + +/* ---- backend glue (wolfMKA) ---- */ + +static struct mka_wolfmka g_m; +static struct macsec_tx_sc g_tx; +static struct macsec_rx_sc g_rx; + +/* Program the agreed SAK into a kernel macsec device (data-plane demo). The + * adapter already installed the SAK into the software SecY channels, so we + * read it back from there. The device must already exist (ip link add ... + * type macsec). Returns 0 on success. */ +static void hexenc(char *out, const uint8_t *b, size_t n) +{ + size_t i; + for (i = 0; i < n; i++) { sprintf(out + 2 * i, "%02x", b[i]); } + out[2 * n] = '\0'; +} + +static void backend_program_kernel(const char *dev) +{ + char cmd[600], txsak[65], rxsak[65], rxsci[17]; + if (!iface_name_ok(dev)) { + fprintf(stderr, "refusing unsafe MACSEC_KERNEL_DEV '%s'\n", + dev != NULL ? dev : "(null)"); + return; + } + hexenc(txsak, g_tx.sak, g_tx.sak_len); + hexenc(rxsak, g_rx.sak, g_rx.sak_len); + hexenc(rxsci, g_rx.sci, 8); + snprintf(cmd, sizeof cmd, + "ip macsec add %s tx sa %u pn 1 on key 01 %s", dev, g_tx.an, txsak); + run_ip(cmd); + snprintf(cmd, sizeof cmd, "ip macsec add %s rx sci %s on", dev, rxsci); + run_ip(cmd); + snprintf(cmd, sizeof cmd, + "ip macsec add %s rx sci %s sa %u pn 1 on key 02 %s", + dev, rxsci, g_rx.an, rxsak); + run_ip(cmd); + printf("MACSEC-UP dev=%s\n", dev); + fflush(stdout); +} + +static int backend_init(const uint8_t *cak, size_t cak_len, + const uint8_t *ckn, size_t ckn_len, + const uint8_t sci[8], uint8_t prio) +{ + memset(&g_tx, 0, sizeof(g_tx)); + memset(&g_rx, 0, sizeof(g_rx)); + return mka_wolfmka_init_psk(&g_m, cb_send, NULL, cak, cak_len, ckn, ckn_len, + sci, prio, 1 /* key_server_capable */, + 16 /* GCM-AES-128 */, &g_tx, &g_rx); +} +static void backend_rx(const uint8_t *f, size_t n, uint64_t t) +{ (void)mka_wolfmka_rx(&g_m, f, n, (uint32_t)t); } +static void backend_tick(uint64_t t) { (void)mka_wolfmka_tick(&g_m, (uint32_t)t); } +static int backend_installed(void) { return mka_wolfmka_installed(&g_m); } +static void backend_report(void) +{ print_sak(g_tx.sak_len, g_tx.an, g_tx.sak, g_tx.sci); } +static void backend_free(void) { mka_wolfmka_free(&g_m); } + +static void pump(uint8_t *buf, size_t cap) +{ + struct pollfd pfd; + pfd.fd = g_fd; pfd.events = POLLIN; pfd.revents = 0; + if (poll(&pfd, 1, 200) > 0 && (pfd.revents & POLLIN)) { + ssize_t n = recv(g_fd, buf, cap, 0); + /* EAPOL-MKA only: EtherType 0x888E, packet type 5. */ + if (n >= 18 && buf[12] == 0x88 && buf[13] == 0x8E && buf[15] == 5) { + backend_rx(buf, (size_t)n, now_ms()); + } + } + backend_tick(now_ms()); +} + +int main(int argc, char **argv) +{ + struct ifreq ifr; + struct sockaddr_ll sll; + uint8_t cak[32], ckn[32], sci[8], mac[6], buf[2048]; + size_t cak_len, ckn_len; + uint64_t deadline; + int timeout_s = 15; + int prio; + + if (argc < 5) { + fprintf(stderr, "usage: %s ifname cak_hex ckn_hex priority [timeout_s]\n", + argv[0]); + return 2; + } + cak_len = unhex(argv[2], cak, sizeof(cak)); + ckn_len = unhex(argv[3], ckn, sizeof(ckn)); + prio = atoi(argv[4]); + if (argc >= 6) timeout_s = atoi(argv[5]); + + /* CAK must be a full 16- or 32-byte hex string; CKN 1..32 bytes. Require + * the whole argument to be consumed so a bad hex tail is not silently + * accepted with unintended key bytes. */ + if ((cak_len != 16 && cak_len != 32) || strlen(argv[2]) != cak_len * 2) { + fprintf(stderr, "cak must be 16 or 32 bytes of hex\n"); + return 2; + } + if (ckn_len == 0 || strlen(argv[3]) != ckn_len * 2) { + fprintf(stderr, "ckn must be 1..32 bytes of hex\n"); + return 2; + } + + g_fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); + if (g_fd < 0) { perror("socket"); return 1; } + memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifr_name, argv[1], IFNAMSIZ - 1); + if (ioctl(g_fd, SIOCGIFINDEX, &ifr) < 0) { perror("SIOCGIFINDEX"); return 1; } + g_ifindex = ifr.ifr_ifindex; + if (ioctl(g_fd, SIOCGIFHWADDR, &ifr) < 0) { perror("SIOCGIFHWADDR"); return 1; } + memcpy(mac, ifr.ifr_hwaddr.sa_data, 6); + + memset(&sll, 0, sizeof(sll)); + sll.sll_family = AF_PACKET; + sll.sll_ifindex = g_ifindex; + sll.sll_protocol = htons(ETH_P_ALL); + if (bind(g_fd, (struct sockaddr *)&sll, sizeof(sll)) < 0) { + perror("bind"); return 1; + } + + memcpy(sci, mac, 6); sci[6] = 0x00; sci[7] = 0x01; /* SCI = MAC || port 1 */ + + if (backend_init(cak, cak_len, ckn, ckn_len, sci, (uint8_t)prio) != 0) { + fprintf(stderr, "backend init failed\n"); close(g_fd); return 1; + } + printf("macsec_sta: if=%s prio=%d mac=%02x%02x%02x%02x%02x%02x backend=wolfmka\n", + argv[1], prio, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + fflush(stdout); + backend_tick(now_ms()); /* emit the first MKPDU */ + + deadline = now_ms() + (uint64_t)timeout_s * 1000U; + { + const char *kdev = getenv("MACSEC_KERNEL_DEV"); + while (now_ms() < deadline) { + pump(buf, sizeof(buf)); + if (backend_installed()) { + /* In kernel mode program the SAK into the macsec device and + * keep MKA alive to the deadline so a ping can flow; otherwise + * just let the peer finish and exit. */ + uint64_t until = (kdev != NULL) ? deadline : (now_ms() + 1500U); + backend_report(); + if (kdev != NULL) { + backend_program_kernel(kdev); + } + while (now_ms() < until) { + pump(buf, sizeof(buf)); + } + backend_free(); + close(g_fd); + return 0; + } + } + } + fprintf(stderr, "macsec_sta: no SAK agreed within %ds\n", timeout_s); + backend_free(); + close(g_fd); + return 1; +} diff --git a/tools/macsec/run_macsec_mka_dataplane_test.sh b/tools/macsec/run_macsec_mka_dataplane_test.sh new file mode 100644 index 00000000..1b66b00c --- /dev/null +++ b/tools/macsec/run_macsec_mka_dataplane_test.sh @@ -0,0 +1,120 @@ +#!/bin/bash +# run_macsec_mka_dataplane_test.sh +# +# Copyright (C) 2006-2026 wolfSSL Inc. +# +# End-to-end MACsec data-plane test: the wolfIP MKA participant (build/macsec-sta, +# wolfMKA backend) agrees a SAK with Linux wpa_supplicant, programs the agreed +# SAK into a kernel macsec device, and a ping is exchanged over the encrypted +# link. Proves the whole stack: wolfMKA control plane + kernel SecY data plane + +# wpa_supplicant interop. +# +# Requires root (veth + wpa_supplicant + kernel macsec) and build/macsec-sta +# built with WOLFMKA_DIR set. Run on Linux: +# sudo bash tools/macsec/run_macsec_mka_dataplane_test.sh + +set -u +REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +STA="$REPO_ROOT/build/macsec-sta" + +NSA=md-a; NSB=md-b; VA=mdv-a; VB=mdv-b +CAK="0123456789abcdef0123456789abcdef" +CKN="000102030405060708090a0b0c0d0e0f" +IP_A=10.9.9.1; IP_B=10.9.9.2 + +cleanup() { + pkill -f "wpa_supplicant.*$VA" 2>/dev/null + ip netns del "$NSA" 2>/dev/null + ip netns del "$NSB" 2>/dev/null +} +trap cleanup EXIT INT TERM + +[ "$(id -u)" = 0 ] || { echo "must run as root" >&2; exit 1; } +[ -x "$STA" ] || { echo "missing $STA (WOLFIP_ENABLE_MACSEC=1 WOLFMKA_DIR=... build/macsec-sta)" >&2; exit 1; } +command -v wpa_supplicant >/dev/null || { echo "wpa_supplicant not installed" >&2; exit 1; } +modprobe macsec 2>/dev/null + +cleanup; sleep 0.3 +set -e +ip netns add "$NSA"; ip netns add "$NSB" +ip link add "$VA" netns "$NSA" type veth peer name "$VB" netns "$NSB" +ip -n "$NSA" link set "$VA" up +ip -n "$NSB" link set "$VB" up +set +e + +WPACONF=$(mktemp) +cat > "$WPACONF" </dev/null | awk -F': ' 'NR==1{print $2}' | cut -d@ -f1) +if [ -z "$WPA_MACSEC" ]; then + echo "FAIL: wpa_supplicant did not create a macsec device"; exit 1 +fi +ip -n "$NSA" addr add "$IP_A/24" dev "$WPA_MACSEC" 2>/dev/null +ip -n "$NSA" link set "$WPA_MACSEC" up +echo "[dp] wpa macsec dev=$WPA_MACSEC ip=$IP_A" + +# wolfIP side: create the kernel macsec device that macsec_sta will key. +ip -n "$NSB" link add link "$VB" macsec0 type macsec encrypt on +ip -n "$NSB" link set macsec0 up +ip -n "$NSB" addr add "$IP_B/24" dev macsec0 +echo "[dp] wolfIP macsec dev=macsec0 ip=$IP_B" + +echo "[dp] running macsec_sta (wolfMKA, kernel SecY) on $VB" +STA_OUT=$(mktemp) +ip netns exec "$NSB" env MACSEC_KERNEL_DEV=macsec0 \ + "$STA" "$VB" "$CAK" "$CKN" 20 25 > "$STA_OUT" 2>&1 & +STA_PID=$! + +# Wait for the SAK to be programmed into the kernel device. +for i in $(seq 1 20); do + grep -q "MACSEC-UP" "$STA_OUT" && break + sleep 0.5 +done + +echo "----- macsec_sta output -----"; cat "$STA_OUT"; echo "-----------------------------" +if ! grep -q "MACSEC-UP" "$STA_OUT"; then + echo "FAIL: macsec_sta never programmed the kernel"; kill "$STA_PID" 2>/dev/null; exit 1 +fi + +echo "[dp] ping $IP_A over the encrypted link" +PINGLOG=$(mktemp) +if ip netns exec "$NSB" ping -c3 -W2 "$IP_A" >"$PINGLOG" 2>&1; then + PING_OK=1 +else + PING_OK=0 +fi +sed 's/^/ /' "$PINGLOG" + +echo "[dp] macsec counters (wolfIP side, header + values):" +ip -n "$NSB" -s macsec show 2>/dev/null | grep -iA1 "OutPktsEncrypted\|InPktsOK" | sed 's/^/ /' + +kill "$STA_PID" 2>/dev/null +rm -f "$WPACONF" "$WPALOG" "$STA_OUT" "$PINGLOG" + +echo +if [ "$PING_OK" = 1 ]; then + echo "PASS: ping succeeded over the MACsec link (wolfMKA + kernel SecY + wpa_supplicant)" + exit 0 +else + echo "FAIL: ping did not succeed over the MACsec link" + exit 1 +fi diff --git a/tools/macsec/run_macsec_mka_test.sh b/tools/macsec/run_macsec_mka_test.sh new file mode 100644 index 00000000..09c943af --- /dev/null +++ b/tools/macsec/run_macsec_mka_test.sh @@ -0,0 +1,87 @@ +#!/bin/bash +# run_macsec_mka_test.sh +# +# Copyright (C) 2006-2025 wolfSSL Inc. +# +# MKA control-plane interop between the wolfIP MKA participant (build/macsec-sta) +# and Linux wpa_supplicant (driver macsec_linux), over a veth pair. Uses a +# static pre-shared CAK/CKN. wpa_supplicant has the higher priority (lower +# value) so it is elected Key Server, generates the SAK, and distributes it; +# the wolfIP participant must discover it, become mutually live, unwrap the SAK with the +# shared KEK, and install it. Success = wolfmka prints SAK-INSTALLED (exit 0). +# +# Requires root (veth + wpa_supplicant + macsec). Run on Linux: +# sudo bash tools/macsec/run_macsec_mka_test.sh + +set -u +REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +STA="$REPO_ROOT/build/macsec-sta" + +NSA=mk2-a; NSB=mk2-b; VA=mk2v-a; VB=mk2v-b +CAK="0123456789abcdef0123456789abcdef" +CKN="000102030405060708090a0b0c0d0e0f" + +cleanup() { + pkill -f "wpa_supplicant.*$VA" 2>/dev/null + ip netns del "$NSA" 2>/dev/null + ip netns del "$NSB" 2>/dev/null +} +trap cleanup EXIT INT TERM + +[ "$(id -u)" = 0 ] || { echo "must run as root" >&2; exit 1; } +[ -x "$STA" ] || { echo "missing $STA (make WOLFIP_ENABLE_MACSEC=1 WOLFMKA_DIR=... build/macsec-sta)" >&2; exit 1; } +command -v wpa_supplicant >/dev/null || { echo "wpa_supplicant not installed" >&2; exit 1; } +modprobe macsec 2>/dev/null + +cleanup; sleep 0.3 +set -e +ip netns add "$NSA"; ip netns add "$NSB" +ip link add "$VA" netns "$NSA" type veth peer name "$VB" netns "$NSB" +ip -n "$NSA" link set "$VA" up +ip -n "$NSB" link set "$VB" up +set +e + +WPACONF=$(mktemp) +cat > "$WPACONF" <&1) +RC=$? +echo "----- macsec_sta output -----" +echo "$OUT" +echo "--------------------------" + +echo "[macsec-mka] wpa_supplicant MKA log (key lines):" +grep -iE "live peer|peer detected|new SAK|distribut|Key Server|CKN|create MKA|MKPDU" "$WPALOG" \ + 2>/dev/null | tail -20 | sed 's/^/ /' +echo "[macsec-mka] kernel macsec on wpa side:" +ip -n "$NSA" macsec show 2>/dev/null | sed 's/^/ /' || echo " (none)" + +rm -f "$WPACONF" "$WPALOG" + +echo +if [ "$RC" -eq 0 ] && echo "$OUT" | grep -q "SAK-INSTALLED"; then + echo "PASS: wolfIP MKA agreed a SAK with wpa_supplicant" + exit 0 +else + echo "FAIL: no SAK agreement (macsec_sta rc=$RC)" + exit 1 +fi diff --git a/tools/macsec/run_macsec_static_test.sh b/tools/macsec/run_macsec_static_test.sh new file mode 100644 index 00000000..00e2e7ba --- /dev/null +++ b/tools/macsec/run_macsec_static_test.sh @@ -0,0 +1,163 @@ +#!/bin/bash +# run_macsec_static_test.sh +# +# Copyright (C) 2006-2025 wolfSSL Inc. +# +# Byte-level interop between the wolfIP software SecY and the Linux kernel +# MACsec module, using a static (pre-shared) SAK - no MKA, no wpa_supplicant. +# Proves the wolfIP 802.1AE framing (SecTAG layout, SCI||PN nonce, +# DA||SA||SecTAG AAD, GCM-AES-128) matches the kernel byte for byte, in both +# directions: +# +# 1. kernel -> wolfIP: the kernel encrypts a frame; macsec-probe validates +# it and recovers the plaintext MSDU. +# 2. wolfIP -> kernel: macsec-probe protects a frame; it is injected on the +# wire and the kernel's InPktsOK counter must increment. +# +# Requires root (network namespaces + kernel macsec). Run on Linux: +# sudo WOLFSSL_PREFIX=/path/to/wolfssl bash tools/macsec/run_macsec_static_test.sh +# +# The build/macsec-probe binary must exist (make WOLFIP_ENABLE_MACSEC=1 +# build/macsec-probe). + +set -u + +REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +PROBE="$REPO_ROOT/build/macsec-probe" + +NS_K="mk-kern" +NS_W="mk-wolf" +VETH_K="mkv-k" +VETH_W="mkv-w" + +# Static test vectors. +SAK="ad7a2bd03eac835a6f620fdcb506b345" # GCM-AES-128 SAK +KEY_ID_TX="01" +KEY_ID_RX="02" +WOLF_SCI="0200000000330001" # SCI wolfIP uses for TX +WOLF_MAC="020000000033" + +PASS=0 +FAIL=0 + +log() { echo "[macsec-static] $*"; } +ok() { echo " [OK] $*"; PASS=$((PASS+1)); } +bad() { echo " [FAIL] $*"; FAIL=$((FAIL+1)); } + +cleanup() { + ip netns del "$NS_K" 2>/dev/null + ip netns del "$NS_W" 2>/dev/null +} +trap cleanup EXIT INT TERM + +if [ "$(id -u)" != "0" ]; then + echo "must run as root (network namespaces + kernel macsec)" >&2 + exit 1 +fi +if [ ! -x "$PROBE" ]; then + echo "missing $PROBE - run: make WOLFIP_ENABLE_MACSEC=1 build/macsec-probe" >&2 + exit 1 +fi +if ! modprobe macsec 2>/dev/null && ! lsmod | grep -q '^macsec'; then + echo "kernel macsec module unavailable" >&2 + exit 1 +fi + +cleanup +set -e +ip netns add "$NS_K" +ip netns add "$NS_W" +ip link add "$VETH_K" netns "$NS_K" type veth peer name "$VETH_W" netns "$NS_W" +ip -n "$NS_K" link set "$VETH_K" up +ip -n "$NS_W" link set "$VETH_W" up + +# Kernel MACsec device on the kernel side, full encryption, static TX SA. +ip -n "$NS_K" link add link "$VETH_K" macsec0 type macsec encrypt on +ip -n "$NS_K" macsec add macsec0 tx sa 0 pn 1 on key "$KEY_ID_TX" "$SAK" +# RX SC/SA for the wolfIP peer (for the reverse direction). +ip -n "$NS_K" macsec add macsec0 rx sci "$WOLF_SCI" on +ip -n "$NS_K" macsec add macsec0 rx sci "$WOLF_SCI" sa 0 pn 1 on key "$KEY_ID_RX" "$SAK" +ip -n "$NS_K" link set macsec0 up +ip -n "$NS_K" addr add 10.9.9.1/24 dev macsec0 +set +e + +# Kernel's own SCI = veth-k MAC || port 0001. +KMAC=$(ip -n "$NS_K" link show "$VETH_K" | awk '/link\/ether/{print $2}' | tr -d ':') +KSCI="${KMAC}0001" +log "kernel SCI=$KSCI wolfIP SCI=$WOLF_SCI" + +# ---- Direction 1: kernel -> wolfIP ---- +log "capturing a kernel-encrypted frame..." +CAP=$(mktemp) +ip netns exec "$NS_W" timeout 4 tcpdump -i "$VETH_W" -c1 -xx -nn 'ether proto 0x88e5' \ + > "$CAP" 2>/dev/null & +TCPD=$! +sleep 0.5 +# Trigger traffic through macsec0 (ARP for the unused .2 goes out encrypted). +ip -n "$NS_K" neigh flush all 2>/dev/null +ip netns exec "$NS_K" ping -c2 -W1 10.9.9.2 >/dev/null 2>&1 +wait "$TCPD" 2>/dev/null +FRAME=$(grep '0x' "$CAP" | sed 's/^.*: //; s/ //g' | tr -d '\n') +rm -f "$CAP" + +if [ -z "$FRAME" ]; then + bad "no kernel MACsec frame captured" +else + log "captured frame: ${FRAME:0:48}... (${#FRAME} hex chars)" + OUT=$("$PROBE" validate "$SAK" "$KSCI" 0 "$FRAME") + if [ "$OUT" != "FAIL" ] && [ -n "$OUT" ]; then + ok "wolfIP validated a kernel-encrypted frame (MSDU=${OUT:0:16}...)" + else + bad "wolfIP could not validate the kernel frame" + fi +fi + +# Sum every InPktsOK counter in `ip -s macsec show`. The output is a +# two-line format: a "stats:" header line of labels, then a line of values. +# Find InPktsOK's column in the header (the value line lacks the leading +# "stats:" token, hence the -1) and read that column from the next line. +macsec_inpktsok() { + ip -n "$NS_K" -s macsec show 2>/dev/null | awk ' + $1 == "stats:" { + col = 0; + for (i = 2; i <= NF; i++) if ($i == "InPktsOK") col = i - 1; + if (getline == 1 && col > 0) s += $col; + next; + } + END { print s + 0 }' +} + +# ---- Direction 2: wolfIP -> kernel ---- +log "protecting a frame with wolfIP and injecting it..." +# Inner MSDU: an ARP-shaped payload is fine; the kernel just needs to +# authenticate + decrypt and count it. DA must be the kernel veth MAC. +KMAC_COLON=$(ip -n "$NS_K" link show "$VETH_K" | awk '/link\/ether/{print $2}') +DA_HEX=$(echo "$KMAC_COLON" | tr -d ':') +PAYLOAD="0800450000200001000040007ce70a0909020a090901" # dummy IPv4-ish +WFRAME=$("$PROBE" protect "$SAK" "$WOLF_SCI" 0 1 1 0 "$DA_HEX" "$WOLF_MAC" "$PAYLOAD") + +BEFORE=$(macsec_inpktsok) +ip netns exec "$NS_W" python3 - "$VETH_W" "$WFRAME" <<'PY' 2>/dev/null +import socket, sys +s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW) +s.bind((sys.argv[1], 0)) +s.send(bytes.fromhex(sys.argv[2])) +PY +sleep 0.5 +AFTER=$(macsec_inpktsok) +if [ "$AFTER" -gt "$BEFORE" ]; then + ok "kernel accepted a wolfIP-protected frame (InPktsOK $BEFORE -> $AFTER)" +else + bad "kernel did not accept the wolfIP frame (InPktsOK $BEFORE -> $AFTER)" + log "diagnostic: kernel RXSC stats:" + ip -n "$NS_K" -s macsec show 2>/dev/null | sed 's/^/ /' +fi + +echo +if [ "$FAIL" -eq 0 ]; then + echo "PASS: macsec static interop ($PASS checks)" + exit 0 +else + echo "FAIL: macsec static interop ($FAIL of $((PASS+FAIL)) checks failed)" + exit 1 +fi