Skip to content

Commit d720af1

Browse files
committed
macsec: clean-room 802.1AE software SecY with wolfDen wolfMKA control plane
1 parent 3f25c79 commit d720af1

23 files changed

Lines changed: 3653 additions & 5 deletions

.github/workflows/supplicant.yml

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ on:
99
- 'src/supplicant/**'
1010
- 'tools/hostapd/**'
1111
- 'tools/wolfsta/**'
12+
- 'src/macsec/**'
13+
- 'tools/macsec/**'
1214
- 'Makefile'
1315
- '.github/workflows/supplicant.yml'
1416
pull_request:
@@ -17,6 +19,8 @@ on:
1719
- 'src/supplicant/**'
1820
- 'tools/hostapd/**'
1921
- 'tools/wolfsta/**'
22+
- 'src/macsec/**'
23+
- 'tools/macsec/**'
2024
- 'Makefile'
2125
- '.github/workflows/supplicant.yml'
2226

@@ -30,6 +34,9 @@ env:
3034
# never cache-hit). Bump to refresh.
3135
WOLFSSL_REF: v5.9.1-stable
3236
WOLFSSL_PREFIX: /home/runner/wolfssl-install
37+
# Build the whole suite with MACsec/MKA on so every object is compiled once
38+
# with a consistent flag set (CMAC/GCM/AES-key-wrap come from --enable-all).
39+
WOLFIP_ENABLE_MACSEC: 1
3340

3441
jobs:
3542
supplicant:
@@ -45,7 +52,7 @@ jobs:
4552
run: |
4653
sudo apt-get update
4754
sudo apt-get install -y build-essential autoconf automake libtool pkg-config \
48-
hostapd dnsmasq socat iw rfkill \
55+
hostapd wpasupplicant dnsmasq socat iw rfkill tcpdump \
4956
libnl-3-dev libnl-genl-3-dev
5057
# mac80211_hwsim lives in linux-modules-extra; best-effort.
5158
sudo apt-get install -y "linux-modules-extra-$(uname -r)" || true
@@ -80,8 +87,10 @@ jobs:
8087
# against the cached wolfSSL (no sudo install needed; rpath is absolute
8188
# so the binaries resolve the lib even under sudo).
8289

83-
# Hard gate: in-process unit tests (no root / no radio).
84-
- name: Supplicant unit tests
90+
# Hard gate: in-process unit tests (no root / no radio). With
91+
# WOLFIP_ENABLE_MACSEC=1 in env this also builds and runs the MACsec/MKA
92+
# crypto, SecY, SA and protocol tests.
93+
- name: Supplicant + MACsec unit tests
8594
run: make supplicant-tests
8695

8796
# PEAP/MSCHAPv2 is off by default (pulls in deprecated MD4 + DES); the
@@ -148,5 +157,52 @@ jobs:
148157
sudo ./tools/hostapd/run_hwsim_wolfsta_dhcp_test.sh
149158
sudo ./tools/hostapd/run_hwsim_wolfsta_dhcp_psk_test.sh
150159
160+
# Best-effort: MACsec interop needs the kernel `macsec` module (in-tree
161+
# in mainline, but not loadable on every hosted runner). Probe and skip
162+
# cleanly. Unlike hwsim this needs no radio, only veth.
163+
- name: Probe kernel macsec
164+
id: macsec
165+
run: |
166+
if sudo modprobe macsec 2>/dev/null; then
167+
echo "available=yes" >> "$GITHUB_OUTPUT"
168+
else
169+
echo "available=no" >> "$GITHUB_OUTPUT"
170+
echo "::warning::kernel macsec module unavailable; skipping macsec interop"
171+
fi
172+
173+
- name: Build MACsec SecY interop tool
174+
if: steps.macsec.outputs.available == 'yes'
175+
run: make build/macsec-probe
176+
177+
# SecY framing vs the kernel MACsec module, both directions (static SAK).
178+
- name: MACsec SecY kernel interop
179+
if: steps.macsec.outputs.available == 'yes'
180+
run: sudo bash tools/macsec/run_macsec_static_test.sh
181+
182+
# NOTE: MKA control-plane interop (run_macsec_mka_test.sh /
183+
# run_macsec_mka_dataplane_test.sh) needs the wolfDen wolfMKA library
184+
# (WOLFMKA_DIR) with the L2-ICV interop fix (wolfSSL/wolfDen PR #2); it
185+
# returns to CI once that library is available upstream.
186+
187+
# Hard gate: the in-process unit tests rebuilt with AddressSanitizer +
188+
# UBSan, to catch memory / undefined-behaviour bugs in the parsers.
189+
# Runs from a clean tree (sanitizer objects must not be reused); placed
190+
# last so it does not disturb the normal-build interop steps above.
191+
- name: Supplicant + MACsec unit tests (ASan + UBSan)
192+
run: |
193+
sudo sysctl vm.mmap_rnd_bits=28 || true
194+
make clean
195+
make supplicant-tests-sanitize
196+
197+
# Hard gate: short libFuzzer smoke over the SecY receive path. Needs
198+
# clang for -fsanitize=fuzzer.
199+
- name: MACsec SecY fuzz smoke
200+
run: |
201+
if ! command -v clang >/dev/null; then
202+
echo "::warning::clang unavailable; skipping fuzz smoke"; exit 0
203+
fi
204+
make macsec-fuzz
205+
./build/fuzz-macsec-secy -max_total_time=30 -rss_limit_mb=4096
206+
151207
# NOTE: P-521 hunt-and-peck (run_hwsim_sae_softmac_g21_test.sh) has an
152208
# open hostapd interop gap - kept as a make target, not yet in CI.

Makefile

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ WOLFIP_ENABLE_PEAP_MSCHAPV2 ?= 0
284284
WOLFIP_ENABLE_SAE ?= 1
285285
WOLFIP_ENABLE_SAE_H2E ?= 1
286286
WOLFIP_ENABLE_SAE_HNP ?= 1
287+
WOLFIP_ENABLE_MACSEC ?= 0
287288

288289
ifneq ($(WOLFSSL_PREFIX),)
289290
WOLFSSL_CFLAGS := -I$(WOLFSSL_PREFIX)/include
@@ -335,7 +336,40 @@ $(error WOLFIP_ENABLE_SAE_H2E=1 requires WOLFIP_ENABLE_SAE=1)
335336
endif
336337
endif
337338

338-
SUPPLICANT_OBJ := $(patsubst src/%.c,build/%.o,$(SUPPLICANT_SRC))
339+
# MACsec / MKA (IEEE 802.1AE + 802.1X-2010). Standalone module under
340+
# src/macsec/ that reuses the supplicant crypto layer (AES key wrap, secure
341+
# zero). Requires wolfSSL built with --enable-cmac (WOLFSSL_CMAC).
342+
# The software SecY (macsec_crypto/macsec_secy/macsec_sa) is always built with
343+
# MACsec. The MKA control plane is provided by the wolfDen wolfMKA library
344+
# (https://github.com/wolfSSL/wolfDen/tree/main/mka) from a local clone (not a
345+
# submodule): set WOLFMKA_DIR to build it. Without it, only the SecY data plane
346+
# (usable with a static SAK) is built.
347+
WOLFMKA_DIR ?=
348+
WOLFMKA_OBJ :=
349+
WOLFIP_HAVE_WOLFMKA :=
350+
351+
ifeq ($(WOLFIP_ENABLE_MACSEC),1)
352+
SUPPLICANT_SRC += src/macsec/macsec_crypto.c \
353+
src/macsec/macsec_secy.c \
354+
src/macsec/macsec_sa.c
355+
CFLAGS += -DWOLFIP_ENABLE_MACSEC=1 -Isrc/macsec
356+
ifneq ($(WOLFMKA_DIR),)
357+
ifeq ($(wildcard $(WOLFMKA_DIR)/include/wolfmka/mka_kay.h),)
358+
$(error WOLFMKA_DIR is set but does not point at a wolfMKA clone (got '$(WOLFMKA_DIR)'))
359+
endif
360+
WOLFIP_HAVE_WOLFMKA := 1
361+
SUPPLICANT_SRC += src/macsec/mka_wolfmka.c
362+
# WOLFMKA_ICV_L2_ADDR makes wolfMKA include the L2 addresses (DA||SA||EtherType)
363+
# in the MKA ICV, matching wpa_supplicant / the Linux kernel (802.1X-2010
364+
# 11.11.3); required to interoperate with them (wolfSSL/wolfDen PR #2).
365+
CFLAGS += -DWOLFIP_MKA_WOLFMKA=1 -DWOLFMKA_ICV_L2_ADDR -I$(WOLFMKA_DIR)/include
366+
# wolfMKA library objects live outside src/, compiled to build/wolfmka/.
367+
WOLFMKA_OBJ := build/wolfmka/mka_crypto.o build/wolfmka/mka_kay.o \
368+
build/wolfmka/mka_pdu.o build/wolfmka/mka_status.o
369+
endif
370+
endif
371+
372+
SUPPLICANT_OBJ := $(patsubst src/%.c,build/%.o,$(SUPPLICANT_SRC)) $(WOLFMKA_OBJ)
339373

340374
# Header-dependency tracking for the supplicant + its host glue. Without
341375
# this, editing a supplicant header (e.g. a struct in supplicant.h) would
@@ -351,6 +385,17 @@ build/supplicant/%.o: src/supplicant/%.c
351385
@echo "[CC] $<"
352386
@$(CC) $(CFLAGS) $(WOLFSSL_CFLAGS) $(NL80211_CFLAGS) -MMD -MP -Isrc/supplicant -c $< -o $@
353387

388+
build/macsec/%.o: src/macsec/%.c
389+
@mkdir -p `dirname $@` || true
390+
@echo "[CC] $<"
391+
@$(CC) $(CFLAGS) $(WOLFSSL_CFLAGS) -MMD -MP -Isrc/supplicant -Isrc/macsec -c $< -o $@
392+
393+
# wolfMKA library sources from the local clone ($(WOLFMKA_DIR)/src).
394+
build/wolfmka/%.o: $(WOLFMKA_DIR)/src/%.c
395+
@mkdir -p build/wolfmka || true
396+
@echo "[CC] $<"
397+
@$(CC) $(CFLAGS) $(WOLFSSL_CFLAGS) -I$(WOLFMKA_DIR)/include -c $< -o $@
398+
354399
# WOLFSSL_LIBS / WOLFSSL_CFLAGS may already be set above when
355400
# WOLFSSL_PREFIX is provided. Otherwise default to pkg-config detection
356401
# and a plain -lwolfssl fallback.
@@ -368,6 +413,59 @@ build/test-wpa-crypto: $(SUPPLICANT_OBJ) build/supplicant/test_wpa_crypto.o
368413
@echo "[LD] $@"
369414
@$(CC) $(CFLAGS) -o $@ $(BEGIN_GROUP) $(^) $(LDFLAGS) $(WOLFSSL_LIBS) $(END_GROUP)
370415

416+
build/test-macsec-crypto: $(SUPPLICANT_OBJ) build/macsec/test_macsec_crypto.o
417+
@echo "[LD] $@"
418+
@$(CC) $(CFLAGS) -o $@ $(BEGIN_GROUP) $(^) $(LDFLAGS) $(WOLFSSL_LIBS) $(END_GROUP)
419+
420+
build/test-macsec-secy: $(SUPPLICANT_OBJ) build/macsec/test_macsec_secy.o
421+
@echo "[LD] $@"
422+
@$(CC) $(CFLAGS) -o $@ $(BEGIN_GROUP) $(^) $(LDFLAGS) $(WOLFSSL_LIBS) $(END_GROUP)
423+
424+
build/test-macsec-sa: $(SUPPLICANT_OBJ) build/macsec/test_macsec_sa.o
425+
@echo "[LD] $@"
426+
@$(CC) $(CFLAGS) -o $@ $(BEGIN_GROUP) $(^) $(LDFLAGS) $(WOLFSSL_LIBS) $(END_GROUP)
427+
428+
# macsec_probe: byte-level SecY cross-check tool used by the kernel-macsec
429+
# interop scripts (tools/macsec/run_macsec_static_test.sh).
430+
build/tools/macsec_probe.o: tools/macsec/macsec_probe.c
431+
@mkdir -p build/tools || true
432+
@echo "[CC] $<"
433+
@$(CC) $(CFLAGS) $(WOLFSSL_CFLAGS) -Isrc/supplicant -Isrc/macsec -c $< -o $@
434+
435+
build/macsec-probe: $(SUPPLICANT_OBJ) build/tools/macsec_probe.o
436+
@echo "[LD] $@"
437+
@$(CC) $(CFLAGS) -o $@ $(BEGIN_GROUP) $(^) $(LDFLAGS) $(WOLFSSL_LIBS) $(END_GROUP)
438+
439+
# macsec_sta: AF_PACKET host harness that runs the wolfMKA participant for
440+
# interop against wpa_supplicant (run_macsec_mka_test.sh). Linux only; requires
441+
# WOLFMKA_DIR (the wolfMKA library provides the MKA control plane).
442+
build/tools/macsec_sta.o: tools/macsec/macsec_sta.c
443+
@test -n "$(WOLFMKA_DIR)" || { echo "macsec-sta requires WOLFMKA_DIR=<wolfMKA clone>"; exit 1; }
444+
@mkdir -p build/tools || true
445+
@echo "[CC] $<"
446+
@$(CC) $(CFLAGS) $(WOLFSSL_CFLAGS) -Isrc/supplicant -Isrc/macsec -I$(WOLFMKA_DIR)/include -c $< -o $@
447+
448+
build/macsec-sta: $(SUPPLICANT_OBJ) build/tools/macsec_sta.o
449+
@echo "[LD] $@"
450+
@$(CC) $(CFLAGS) -o $@ $(BEGIN_GROUP) $(^) $(LDFLAGS) $(WOLFSSL_LIBS) $(END_GROUP)
451+
452+
# libFuzzer harness for the network-facing SecY receive path. Needs clang
453+
# (-fsanitize=fuzzer); the macsec sources are compiled straight into the
454+
# harness so the instrumentation is consistent. Not built by default. Requires
455+
# a CMAC-enabled wolfSSL (WOLFSSL_CFLAGS/LIBS).
456+
FUZZ_CC ?= clang
457+
FUZZ_FLAGS ?= -fsanitize=fuzzer,address,undefined -fno-sanitize-recover=all -g -O1
458+
FUZZ_MACSEC_SRC := src/macsec/macsec_crypto.c src/macsec/macsec_secy.c \
459+
src/macsec/macsec_sa.c src/supplicant/wpa_crypto.c
460+
461+
build/fuzz-macsec-secy: src/macsec/fuzz_macsec_secy.c $(FUZZ_MACSEC_SRC)
462+
@mkdir -p build || true
463+
@echo "[FUZZ] $@"
464+
@$(FUZZ_CC) $(FUZZ_FLAGS) $(WOLFSSL_CFLAGS) -Isrc/supplicant -Isrc/macsec \
465+
$(^) -o $@ $(WOLFSSL_LIBS)
466+
467+
macsec-fuzz: build/fuzz-macsec-secy
468+
371469
build/test-supplicant-4way: $(SUPPLICANT_OBJ) build/supplicant/test_supplicant_4way.o
372470
@echo "[LD] $@"
373471
@$(CC) $(CFLAGS) -o $@ $(BEGIN_GROUP) $(^) $(LDFLAGS) $(WOLFSSL_LIBS) $(END_GROUP)
@@ -535,10 +633,23 @@ endif
535633
ifeq ($(WOLFIP_ENABLE_SAE),1)
536634
SUPPLICANT_TEST_BINS += build/test-sae-crypto build/test-supplicant-sae
537635
endif
636+
ifeq ($(WOLFIP_ENABLE_MACSEC),1)
637+
SUPPLICANT_TEST_BINS += build/test-macsec-crypto build/test-macsec-secy \
638+
build/test-macsec-sa
639+
endif
538640

539641
supplicant-tests: $(SUPPLICANT_TEST_BINS)
540642
@for t in $(SUPPLICANT_TEST_BINS); do echo "==> $$t"; $$t || exit 1; done
541643

644+
# The same in-process unit tests built with AddressSanitizer + UBSan, to catch
645+
# memory / undefined-behaviour bugs in the parsers. Target-specific flags flow
646+
# to the object + link rules of the prerequisite bins. Run from a clean tree
647+
# (sanitizer objects must not be reused by a normal build).
648+
supplicant-tests-sanitize: CFLAGS += -fsanitize=address,undefined \
649+
-fno-sanitize-recover=all -g
650+
supplicant-tests-sanitize: LDFLAGS += -fsanitize=address,undefined
651+
supplicant-tests-sanitize: supplicant-tests
652+
542653
# Real-authenticator interop tests. Both require hostapd installed and
543654
# root (veth pair + AF_PACKET raw socket). Not part of supplicant-tests
544655
# because of those constraints.

src/macsec/fuzz_macsec_secy.c

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/* fuzz_macsec_secy.c
2+
*
3+
* Copyright (C) 2006-2025 wolfSSL Inc.
4+
*
5+
* This file is part of wolfIP.
6+
*
7+
* wolfIP is free software; you can redistribute it and/or modify
8+
* it under the terms of the GNU General Public License as published by
9+
* the Free Software Foundation; either version 2 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* wolfIP is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU General Public License along
18+
* with this program; if not, write to the Free Software Foundation, Inc.,
19+
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
20+
*/
21+
22+
/* libFuzzer harness for the 802.1AE SecY receive path. Feeds arbitrary bytes
23+
* to macsec_sectag_parse and macsec_validate: GCM authentication rejects
24+
* random frames, but the SecTAG parse and the length / header / offset bounds
25+
* math that run before decryption are fully exercised. Each confidentiality
26+
* offset (0/30/50) is tried. Build: make WOLFIP_ENABLE_MACSEC=1 build/fuzz-macsec-secy */
27+
28+
#include <stdint.h>
29+
#include <string.h>
30+
31+
#include "macsec_secy.h"
32+
33+
static const uint8_t FZ_SAK[16] = {
34+
0xad,0x7a,0x2b,0xd0,0x3e,0xac,0x83,0x5a,
35+
0x6f,0x62,0x0f,0xdc,0xb5,0x06,0xb3,0x45
36+
};
37+
static const uint8_t FZ_SCI[8] = { 0x02,0,0,0,0,0x22,0x00,0x01 };
38+
39+
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
40+
{
41+
struct macsec_sectag tag;
42+
struct macsec_validate_params vp;
43+
uint8_t out[2048];
44+
size_t out_len;
45+
size_t i;
46+
static const size_t offsets[3] = { 0U, 30U, 50U };
47+
48+
if (size > 1600) {
49+
return 0;
50+
}
51+
52+
/* SecTAG parser on the raw bytes (starting at the MACsec EtherType). */
53+
(void)macsec_sectag_parse(data, size, &tag);
54+
55+
/* Full validate path for each configured confidentiality offset. */
56+
for (i = 0; i < 3; i++) {
57+
memset(&vp, 0, sizeof(vp));
58+
vp.sak = FZ_SAK;
59+
vp.sak_len = sizeof(FZ_SAK);
60+
vp.sci = FZ_SCI;
61+
vp.conf_offset = offsets[i];
62+
out_len = 0;
63+
(void)macsec_validate(&vp, data, size, out, sizeof(out), &out_len,
64+
&tag);
65+
}
66+
return 0;
67+
}

0 commit comments

Comments
 (0)