Skip to content

Commit 35629e9

Browse files
authored
Merge pull request #482 from aidangarske/spdm-runners
Add hardware SPDM CI runner + fix SPDM auto-connect regression
2 parents b8e63ea + 3cb1ddb commit 35629e9

5 files changed

Lines changed: 285 additions & 1 deletion

File tree

.github/workflows/hw-spdm-test.yml

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
name: Hardware SPDM Test
2+
3+
# Runs examples/spdm/spdm_test.sh on the self-hosted Pi with a real TPM.
4+
5+
on:
6+
push:
7+
branches: [master]
8+
paths:
9+
- 'src/spdm/**'
10+
- 'wolftpm/spdm/**'
11+
- 'examples/spdm/**'
12+
- 'src/tpm2_wrap.c'
13+
- 'src/tpm2_spdm.c'
14+
- 'src/tpm2.c'
15+
- 'hal/tpm_io_linux.c'
16+
- 'configure.ac'
17+
- '.github/workflows/hw-spdm-test.yml'
18+
- 'scripts/hw-runner-health-check.sh'
19+
- 'tests/unit_tests.c'
20+
pull_request_target:
21+
branches: [master]
22+
types: [opened, synchronize, reopened]
23+
paths:
24+
- 'src/spdm/**'
25+
- 'wolftpm/spdm/**'
26+
- 'examples/spdm/**'
27+
- 'src/tpm2_wrap.c'
28+
- 'src/tpm2_spdm.c'
29+
- 'src/tpm2.c'
30+
- 'hal/tpm_io_linux.c'
31+
- 'configure.ac'
32+
- '.github/workflows/hw-spdm-test.yml'
33+
- 'scripts/hw-runner-health-check.sh'
34+
- 'tests/unit_tests.c'
35+
36+
permissions: read-all
37+
38+
# Serialize runs; hardware state is shared.
39+
concurrency:
40+
group: hw-spdm-runner
41+
cancel-in-progress: false
42+
43+
jobs:
44+
hw-spdm:
45+
if: >
46+
github.event_name != 'pull_request_target' ||
47+
contains(fromJSON('["OWNER","MEMBER"]'),
48+
github.event.pull_request.author_association)
49+
runs-on: [self-hosted, Linux, ARM64, wolftpm-spdm]
50+
timeout-minutes: 25
51+
52+
strategy:
53+
fail-fast: false
54+
matrix:
55+
include:
56+
- vendor: nuvoton
57+
expected_vendor: "NPCT75x"
58+
wolftpm_config: "--enable-spdm --enable-nuvoton --enable-debug"
59+
spi_cs: "0"
60+
modes: "nuvoton"
61+
- vendor: nations
62+
expected_vendor: "NS350"
63+
wolftpm_config: "--enable-spdm --enable-nations --enable-debug"
64+
spi_cs: "1"
65+
modes: "nations nations-psk"
66+
67+
steps:
68+
- name: Harden Runner
69+
uses: step-security/harden-runner@6c3c2f2c1c457b00c10c4848d6f5491db3b629df # v2.18.0
70+
with:
71+
egress-policy: audit
72+
73+
- name: Checkout wolfTPM
74+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
75+
with:
76+
ref: ${{ github.event.pull_request.head.sha || github.sha }}
77+
persist-credentials: false
78+
79+
- name: Hardware health check
80+
id: health
81+
continue-on-error: true
82+
run: bash scripts/hw-runner-health-check.sh "${{ matrix.spi_cs }}"
83+
84+
- name: Warn if health check failed
85+
if: steps.health.outcome == 'failure'
86+
run: echo "::warning::Runner hardware check failed on ${{ runner.name }}. Skipping hardware tests."
87+
88+
- name: Clean workspace (preserve wolfssl cache dir)
89+
if: steps.health.outcome == 'success'
90+
run: git clean -xdf
91+
92+
- name: Resolve wolfSSL SHA and choose prefix
93+
id: wolfssl
94+
if: steps.health.outcome == 'success'
95+
run: |
96+
MAX_CACHE_AGE_SECS=86400
97+
SHA=$(curl -sf https://api.github.com/repos/wolfssl/wolfssl/commits/master | jq -r .sha)
98+
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
99+
HIT=false
100+
if [ "${{ github.event_name }}" = "push" ]; then
101+
PREFIX="$HOME/wolfssl-install"
102+
if [ -f "$PREFIX/.sha" ] && [ "$(cat "$PREFIX/.sha")" = "$SHA" ]; then
103+
AGE=$(( $(date +%s) - $(stat -c %Y "$PREFIX/.sha") ))
104+
[ "$AGE" -lt "$MAX_CACHE_AGE_SECS" ] && HIT=true
105+
fi
106+
else
107+
PREFIX="$RUNNER_TEMP/wolfssl-install"
108+
mkdir -p "$PREFIX"
109+
fi
110+
echo "prefix=$PREFIX" >> "$GITHUB_OUTPUT"
111+
echo "hit=$HIT" >> "$GITHUB_OUTPUT"
112+
113+
- name: Checkout wolfSSL
114+
if: steps.health.outcome == 'success' && steps.wolfssl.outputs.hit != 'true'
115+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
116+
with:
117+
repository: wolfssl/wolfssl
118+
path: wolfssl
119+
ref: ${{ steps.wolfssl.outputs.sha }}
120+
persist-credentials: false
121+
122+
- name: Build & install wolfSSL (user prefix, no sudo)
123+
if: steps.health.outcome == 'success' && steps.wolfssl.outputs.hit != 'true'
124+
working-directory: ./wolfssl
125+
run: |
126+
./autogen.sh
127+
./configure --prefix=${{ steps.wolfssl.outputs.prefix }} \
128+
--enable-wolftpm --enable-ecc --enable-sha384 \
129+
--enable-aesgcm --enable-hkdf --enable-sp
130+
make -j"$(nproc)"
131+
make install
132+
echo '${{ steps.wolfssl.outputs.sha }}' > '${{ steps.wolfssl.outputs.prefix }}/.sha'
133+
134+
- name: Build wolfTPM (${{ matrix.vendor }})
135+
if: steps.health.outcome == 'success'
136+
run: |
137+
./autogen.sh
138+
./configure ${{ matrix.wolftpm_config }} \
139+
CPPFLAGS='-DTPM2_SPI_DEV_CS="${{ matrix.spi_cs }}"' \
140+
CFLAGS="-I${{ steps.wolfssl.outputs.prefix }}/include" \
141+
LDFLAGS="-L${{ steps.wolfssl.outputs.prefix }}/lib"
142+
make -j"$(nproc)"
143+
144+
- name: Detect ${{ matrix.vendor }} TPM on SPI CS ${{ matrix.spi_cs }}
145+
id: detect
146+
if: steps.health.outcome == 'success'
147+
env:
148+
LD_LIBRARY_PATH: ${{ steps.wolfssl.outputs.prefix }}/lib
149+
run: |
150+
EXPECTED='${{ matrix.expected_vendor }}'
151+
# caps may hang or error if no chip is at this CS; time-bound it.
152+
OUT=$(timeout 15 ./examples/wrap/caps 2>&1 || true)
153+
echo "$OUT"
154+
if echo "$OUT" | grep -q "Vendor ${EXPECTED}"; then
155+
echo "present=true" >> "$GITHUB_OUTPUT"
156+
echo "[detect] ${EXPECTED} TPM found on CS ${{ matrix.spi_cs }}"
157+
else
158+
echo "present=false" >> "$GITHUB_OUTPUT"
159+
fi
160+
161+
- name: Warn if ${{ matrix.vendor }} TPM not present
162+
if: steps.health.outcome == 'success' && steps.detect.outputs.present == 'false'
163+
run: echo "::warning::${{ matrix.expected_vendor }} not detected on SPI CS ${{ matrix.spi_cs }}. Skipping ${{ matrix.vendor }} SPDM tests — wire the chip and re-run, or ignore if this vendor isn't installed on this runner."
164+
165+
- name: Run SPDM hardware tests (${{ matrix.vendor }})
166+
if: steps.health.outcome == 'success' && steps.detect.outputs.present == 'true'
167+
env:
168+
LD_LIBRARY_PATH: ${{ steps.wolfssl.outputs.prefix }}/lib
169+
run: |
170+
set -e
171+
for mode in ${{ matrix.modes }}; do
172+
echo "=== spdm_test.sh mode=$mode ==="
173+
./examples/spdm/spdm_test.sh ./examples/spdm/spdm_ctrl "$mode" \
174+
2>&1 | tee "spdm-${{ matrix.vendor }}-${mode}.log"
175+
done
176+
177+
- name: Post-job cleanup
178+
if: always() && steps.health.outcome == 'success' && steps.detect.outputs.present == 'true'
179+
env:
180+
LD_LIBRARY_PATH: ${{ steps.wolfssl.outputs.prefix }}/lib
181+
run: |
182+
pgrep -u "$(id -u)" -a | grep -vE '(Runner\.Listener|Runner\.Worker|actions-runner)' || true
183+
pkill -u "$(id -u)" -f 'spdm_ctrl|unit\.test|caps$' 2>/dev/null || true
184+
sleep 1
185+
gpioset gpiochip0 4=0 2>/dev/null && sleep 0.1 && gpioset gpiochip0 4=1 2>/dev/null || true
186+
sleep 2
187+
./examples/spdm/spdm_ctrl --connect --unlock 2>/dev/null || true
188+
./examples/management/flush 2>/dev/null || true
189+
echo "[cleanup] done"
190+
191+
- name: Upload logs on failure
192+
if: failure()
193+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
194+
with:
195+
name: hw-spdm-logs-${{ matrix.vendor }}
196+
path: |
197+
*.log
198+
config.log
199+
retention-days: 14

scripts/hw-runner-health-check.sh

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/usr/bin/env bash
2+
# Pre-flight check for the self-hosted hardware SPDM runner.
3+
# Fails fast (before the 10-minute build) if required device nodes aren't
4+
# present or aren't accessible to the current user.
5+
#
6+
# Usage: hw-runner-health-check.sh <expected_spi_cs_number>
7+
set -euo pipefail
8+
9+
CS="${1:?usage: $0 <spi_cs_number>}"
10+
SPIDEV="/dev/spidev0.${CS}"
11+
GPIOCHIP="/dev/gpiochip0"
12+
13+
echo "[health] runner user: $(id)"
14+
15+
if [ ! -c "$SPIDEV" ]; then
16+
echo "[health] FAIL: $SPIDEV missing. Is SPI enabled in config.txt? Is the TPM wired to CS${CS}?"
17+
exit 1
18+
fi
19+
if [ ! -r "$SPIDEV" ] || [ ! -w "$SPIDEV" ]; then
20+
echo "[health] FAIL: $SPIDEV not rw for $(whoami). Add runner user to 'spi' group."
21+
exit 1
22+
fi
23+
if [ ! -c "$GPIOCHIP" ]; then
24+
echo "[health] FAIL: $GPIOCHIP missing."
25+
exit 1
26+
fi
27+
if ! command -v gpioset >/dev/null; then
28+
echo "[health] FAIL: gpioset not on PATH. Install the 'gpiod' package."
29+
exit 1
30+
fi
31+
32+
echo "[health] OK: $SPIDEV accessible, $GPIOCHIP present, gpioset on PATH"

src/tpm2_spdm.c

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,10 +252,15 @@ int wolfTPM2_SPDM_SecuredExchange(
252252
vdMsg, (word32)vdMsgSz, vdRsp, &vdRspSz);
253253
}
254254

255-
/* Parse VENDOR_DEFINED_RESPONSE to extract TPM response */
255+
/* Parse VENDOR_DEFINED_RESPONSE to extract TPM response.
256+
* ParseVendorDefined returns payload dataLen (>= 0) on success,
257+
* negative WOLFSPDM_E_* on failure. */
256258
if (rc == 0) {
257259
rc = wolfSPDM_ParseVendorDefined(vdRsp, vdRspSz,
258260
rspVdCode, rspPlain, rspSz);
261+
if (rc >= 0) {
262+
rc = 0; /* success - convert dataLen to success indicator */
263+
}
259264
}
260265

261266
/* Verify response is for our TPM2_CMD request */

src/tpm2_wrap.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,16 @@ int wolfTPM2_Init(WOLFTPM2_DEV* dev, TPM2HalIoCb ioCb, void* userCtx)
292292
return rc;
293293
}
294294

295+
/* Vendor-specific connect handles SetTisIO, SetMode, and auto-generates
296+
* a host ephemeral key pair for mutual authentication (MutAuth=1).
297+
* Plain wolfTPM2_SpdmConnect() skips that setup and FINISH fails. */
298+
#if defined(WOLFSPDM_NUVOTON)
299+
rc = wolfTPM2_SpdmConnectNuvoton(dev, NULL, 0, NULL, 0);
300+
#elif defined(WOLFSPDM_NATIONS)
301+
rc = wolfTPM2_SpdmConnectNations(dev, NULL, 0, NULL, 0);
302+
#else
295303
rc = wolfTPM2_SpdmConnect(dev);
304+
#endif
296305
if (rc != 0) {
297306
#ifdef DEBUG_WOLFTPM
298307
printf("SPDM auto-connect failed: %d\n", rc);

tests/unit_tests.c

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,36 @@
9191

9292
#ifndef WOLFTPM2_NO_WRAPPER
9393

94+
#if !defined(WOLFTPM2_NO_WOLFCRYPT) && defined(HAVE_ECC) && \
95+
!defined(WOLFTPM2_NO_ASN)
96+
/* Query TPM_CAP_ALGS to see if a given algorithm is supported.
97+
* Returns 1 if supported, 0 otherwise. Used to skip test iterations on TPMs
98+
* that don't implement a given hash (e.g. Nuvoton NPCT75x lacks SHA512).
99+
* Guarded by the same ifdef as its only caller (test_wolfTPM2_EccSignVerifyDig)
100+
* so non-ECC builds don't trip -Werror=unused-function. */
101+
static int test_tpm_alg_supported(TPM_ALG_ID alg)
102+
{
103+
GetCapability_In in;
104+
GetCapability_Out out;
105+
word32 i;
106+
107+
XMEMSET(&in, 0, sizeof(in));
108+
XMEMSET(&out, 0, sizeof(out));
109+
in.capability = TPM_CAP_ALGS;
110+
in.property = alg;
111+
in.propertyCount = 1;
112+
if (TPM2_GetCapability(&in, &out) != TPM_RC_SUCCESS) {
113+
return 1; /* On error, assume supported and let the real call fail */
114+
}
115+
for (i = 0; i < out.capabilityData.data.algorithms.count; i++) {
116+
if (out.capabilityData.data.algorithms.algProperties[i].alg == alg) {
117+
return 1;
118+
}
119+
}
120+
return 0;
121+
}
122+
#endif /* !WOLFTPM2_NO_WOLFCRYPT && HAVE_ECC && !WOLFTPM2_NO_ASN */
123+
94124
static void test_wolfTPM2_Init(void)
95125
{
96126
int rc;
@@ -1638,6 +1668,15 @@ static void test_wolfTPM2_EccSignVerifyDig(WOLFTPM2_DEV* dev,
16381668
}
16391669
#endif
16401670

1671+
/* Skip if this TPM doesn't implement the requested hash alg. Some TPMs
1672+
* (e.g. Nuvoton NPCT75x) only support a subset of hashes; the TPM rejects
1673+
* Create with TPM_RC_SIZE param 1, not TPM_RC_HASH, so the existing
1674+
* post-hoc skip-check can't catch it. Query capabilities up front. */
1675+
if (!test_tpm_alg_supported(hashAlg)) {
1676+
printf("Hash alg 0x%x not supported by TPM... Skipping\n", hashAlg);
1677+
return;
1678+
}
1679+
16411680
/* -- Use TPM key to sign and verify with wolfCrypt -- */
16421681
/* Create ECC key for signing */
16431682
rc = wolfTPM2_GetKeyTemplate_ECC_ex(&publicTemplate, hashAlg,

0 commit comments

Comments
 (0)