Merge pull request #482 from aidangarske/spdm-runners

Add hardware SPDM CI runner + fix SPDM auto-connect regression
pull/484/head
David Garske 2026-04-17 08:21:03 -07:00 committed by GitHub
commit 35629e96df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 285 additions and 1 deletions

View File

@ -0,0 +1,199 @@
name: Hardware SPDM Test
# Runs examples/spdm/spdm_test.sh on the self-hosted Pi with a real TPM.
on:
push:
branches: [master]
paths:
- 'src/spdm/**'
- 'wolftpm/spdm/**'
- 'examples/spdm/**'
- 'src/tpm2_wrap.c'
- 'src/tpm2_spdm.c'
- 'src/tpm2.c'
- 'hal/tpm_io_linux.c'
- 'configure.ac'
- '.github/workflows/hw-spdm-test.yml'
- 'scripts/hw-runner-health-check.sh'
- 'tests/unit_tests.c'
pull_request_target:
branches: [master]
types: [opened, synchronize, reopened]
paths:
- 'src/spdm/**'
- 'wolftpm/spdm/**'
- 'examples/spdm/**'
- 'src/tpm2_wrap.c'
- 'src/tpm2_spdm.c'
- 'src/tpm2.c'
- 'hal/tpm_io_linux.c'
- 'configure.ac'
- '.github/workflows/hw-spdm-test.yml'
- 'scripts/hw-runner-health-check.sh'
- 'tests/unit_tests.c'
permissions: read-all
# Serialize runs; hardware state is shared.
concurrency:
group: hw-spdm-runner
cancel-in-progress: false
jobs:
hw-spdm:
if: >
github.event_name != 'pull_request_target' ||
contains(fromJSON('["OWNER","MEMBER"]'),
github.event.pull_request.author_association)
runs-on: [self-hosted, Linux, ARM64, wolftpm-spdm]
timeout-minutes: 25
strategy:
fail-fast: false
matrix:
include:
- vendor: nuvoton
expected_vendor: "NPCT75x"
wolftpm_config: "--enable-spdm --enable-nuvoton --enable-debug"
spi_cs: "0"
modes: "nuvoton"
- vendor: nations
expected_vendor: "NS350"
wolftpm_config: "--enable-spdm --enable-nations --enable-debug"
spi_cs: "1"
modes: "nations nations-psk"
steps:
- name: Harden Runner
uses: step-security/harden-runner@6c3c2f2c1c457b00c10c4848d6f5491db3b629df # v2.18.0
with:
egress-policy: audit
- name: Checkout wolfTPM
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
- name: Hardware health check
id: health
continue-on-error: true
run: bash scripts/hw-runner-health-check.sh "${{ matrix.spi_cs }}"
- name: Warn if health check failed
if: steps.health.outcome == 'failure'
run: echo "::warning::Runner hardware check failed on ${{ runner.name }}. Skipping hardware tests."
- name: Clean workspace (preserve wolfssl cache dir)
if: steps.health.outcome == 'success'
run: git clean -xdf
- name: Resolve wolfSSL SHA and choose prefix
id: wolfssl
if: steps.health.outcome == 'success'
run: |
MAX_CACHE_AGE_SECS=86400
SHA=$(curl -sf https://api.github.com/repos/wolfssl/wolfssl/commits/master | jq -r .sha)
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
HIT=false
if [ "${{ github.event_name }}" = "push" ]; then
PREFIX="$HOME/wolfssl-install"
if [ -f "$PREFIX/.sha" ] && [ "$(cat "$PREFIX/.sha")" = "$SHA" ]; then
AGE=$(( $(date +%s) - $(stat -c %Y "$PREFIX/.sha") ))
[ "$AGE" -lt "$MAX_CACHE_AGE_SECS" ] && HIT=true
fi
else
PREFIX="$RUNNER_TEMP/wolfssl-install"
mkdir -p "$PREFIX"
fi
echo "prefix=$PREFIX" >> "$GITHUB_OUTPUT"
echo "hit=$HIT" >> "$GITHUB_OUTPUT"
- name: Checkout wolfSSL
if: steps.health.outcome == 'success' && steps.wolfssl.outputs.hit != 'true'
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
repository: wolfssl/wolfssl
path: wolfssl
ref: ${{ steps.wolfssl.outputs.sha }}
persist-credentials: false
- name: Build & install wolfSSL (user prefix, no sudo)
if: steps.health.outcome == 'success' && steps.wolfssl.outputs.hit != 'true'
working-directory: ./wolfssl
run: |
./autogen.sh
./configure --prefix=${{ steps.wolfssl.outputs.prefix }} \
--enable-wolftpm --enable-ecc --enable-sha384 \
--enable-aesgcm --enable-hkdf --enable-sp
make -j"$(nproc)"
make install
echo '${{ steps.wolfssl.outputs.sha }}' > '${{ steps.wolfssl.outputs.prefix }}/.sha'
- name: Build wolfTPM (${{ matrix.vendor }})
if: steps.health.outcome == 'success'
run: |
./autogen.sh
./configure ${{ matrix.wolftpm_config }} \
CPPFLAGS='-DTPM2_SPI_DEV_CS="${{ matrix.spi_cs }}"' \
CFLAGS="-I${{ steps.wolfssl.outputs.prefix }}/include" \
LDFLAGS="-L${{ steps.wolfssl.outputs.prefix }}/lib"
make -j"$(nproc)"
- name: Detect ${{ matrix.vendor }} TPM on SPI CS ${{ matrix.spi_cs }}
id: detect
if: steps.health.outcome == 'success'
env:
LD_LIBRARY_PATH: ${{ steps.wolfssl.outputs.prefix }}/lib
run: |
EXPECTED='${{ matrix.expected_vendor }}'
# caps may hang or error if no chip is at this CS; time-bound it.
OUT=$(timeout 15 ./examples/wrap/caps 2>&1 || true)
echo "$OUT"
if echo "$OUT" | grep -q "Vendor ${EXPECTED}"; then
echo "present=true" >> "$GITHUB_OUTPUT"
echo "[detect] ${EXPECTED} TPM found on CS ${{ matrix.spi_cs }}"
else
echo "present=false" >> "$GITHUB_OUTPUT"
fi
- name: Warn if ${{ matrix.vendor }} TPM not present
if: steps.health.outcome == 'success' && steps.detect.outputs.present == 'false'
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."
- name: Run SPDM hardware tests (${{ matrix.vendor }})
if: steps.health.outcome == 'success' && steps.detect.outputs.present == 'true'
env:
LD_LIBRARY_PATH: ${{ steps.wolfssl.outputs.prefix }}/lib
run: |
set -e
for mode in ${{ matrix.modes }}; do
echo "=== spdm_test.sh mode=$mode ==="
./examples/spdm/spdm_test.sh ./examples/spdm/spdm_ctrl "$mode" \
2>&1 | tee "spdm-${{ matrix.vendor }}-${mode}.log"
done
- name: Post-job cleanup
if: always() && steps.health.outcome == 'success' && steps.detect.outputs.present == 'true'
env:
LD_LIBRARY_PATH: ${{ steps.wolfssl.outputs.prefix }}/lib
run: |
pgrep -u "$(id -u)" -a | grep -vE '(Runner\.Listener|Runner\.Worker|actions-runner)' || true
pkill -u "$(id -u)" -f 'spdm_ctrl|unit\.test|caps$' 2>/dev/null || true
sleep 1
gpioset gpiochip0 4=0 2>/dev/null && sleep 0.1 && gpioset gpiochip0 4=1 2>/dev/null || true
sleep 2
./examples/spdm/spdm_ctrl --connect --unlock 2>/dev/null || true
./examples/management/flush 2>/dev/null || true
echo "[cleanup] done"
- name: Upload logs on failure
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: hw-spdm-logs-${{ matrix.vendor }}
path: |
*.log
config.log
retention-days: 14

View File

@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Pre-flight check for the self-hosted hardware SPDM runner.
# Fails fast (before the 10-minute build) if required device nodes aren't
# present or aren't accessible to the current user.
#
# Usage: hw-runner-health-check.sh <expected_spi_cs_number>
set -euo pipefail
CS="${1:?usage: $0 <spi_cs_number>}"
SPIDEV="/dev/spidev0.${CS}"
GPIOCHIP="/dev/gpiochip0"
echo "[health] runner user: $(id)"
if [ ! -c "$SPIDEV" ]; then
echo "[health] FAIL: $SPIDEV missing. Is SPI enabled in config.txt? Is the TPM wired to CS${CS}?"
exit 1
fi
if [ ! -r "$SPIDEV" ] || [ ! -w "$SPIDEV" ]; then
echo "[health] FAIL: $SPIDEV not rw for $(whoami). Add runner user to 'spi' group."
exit 1
fi
if [ ! -c "$GPIOCHIP" ]; then
echo "[health] FAIL: $GPIOCHIP missing."
exit 1
fi
if ! command -v gpioset >/dev/null; then
echo "[health] FAIL: gpioset not on PATH. Install the 'gpiod' package."
exit 1
fi
echo "[health] OK: $SPIDEV accessible, $GPIOCHIP present, gpioset on PATH"

View File

@ -252,10 +252,15 @@ int wolfTPM2_SPDM_SecuredExchange(
vdMsg, (word32)vdMsgSz, vdRsp, &vdRspSz);
}
/* Parse VENDOR_DEFINED_RESPONSE to extract TPM response */
/* Parse VENDOR_DEFINED_RESPONSE to extract TPM response.
* ParseVendorDefined returns payload dataLen (>= 0) on success,
* negative WOLFSPDM_E_* on failure. */
if (rc == 0) {
rc = wolfSPDM_ParseVendorDefined(vdRsp, vdRspSz,
rspVdCode, rspPlain, rspSz);
if (rc >= 0) {
rc = 0; /* success - convert dataLen to success indicator */
}
}
/* Verify response is for our TPM2_CMD request */

View File

@ -292,7 +292,16 @@ int wolfTPM2_Init(WOLFTPM2_DEV* dev, TPM2HalIoCb ioCb, void* userCtx)
return rc;
}
/* Vendor-specific connect handles SetTisIO, SetMode, and auto-generates
* a host ephemeral key pair for mutual authentication (MutAuth=1).
* Plain wolfTPM2_SpdmConnect() skips that setup and FINISH fails. */
#if defined(WOLFSPDM_NUVOTON)
rc = wolfTPM2_SpdmConnectNuvoton(dev, NULL, 0, NULL, 0);
#elif defined(WOLFSPDM_NATIONS)
rc = wolfTPM2_SpdmConnectNations(dev, NULL, 0, NULL, 0);
#else
rc = wolfTPM2_SpdmConnect(dev);
#endif
if (rc != 0) {
#ifdef DEBUG_WOLFTPM
printf("SPDM auto-connect failed: %d\n", rc);

View File

@ -91,6 +91,36 @@
#ifndef WOLFTPM2_NO_WRAPPER
#if !defined(WOLFTPM2_NO_WOLFCRYPT) && defined(HAVE_ECC) && \
!defined(WOLFTPM2_NO_ASN)
/* Query TPM_CAP_ALGS to see if a given algorithm is supported.
* Returns 1 if supported, 0 otherwise. Used to skip test iterations on TPMs
* that don't implement a given hash (e.g. Nuvoton NPCT75x lacks SHA512).
* Guarded by the same ifdef as its only caller (test_wolfTPM2_EccSignVerifyDig)
* so non-ECC builds don't trip -Werror=unused-function. */
static int test_tpm_alg_supported(TPM_ALG_ID alg)
{
GetCapability_In in;
GetCapability_Out out;
word32 i;
XMEMSET(&in, 0, sizeof(in));
XMEMSET(&out, 0, sizeof(out));
in.capability = TPM_CAP_ALGS;
in.property = alg;
in.propertyCount = 1;
if (TPM2_GetCapability(&in, &out) != TPM_RC_SUCCESS) {
return 1; /* On error, assume supported and let the real call fail */
}
for (i = 0; i < out.capabilityData.data.algorithms.count; i++) {
if (out.capabilityData.data.algorithms.algProperties[i].alg == alg) {
return 1;
}
}
return 0;
}
#endif /* !WOLFTPM2_NO_WOLFCRYPT && HAVE_ECC && !WOLFTPM2_NO_ASN */
static void test_wolfTPM2_Init(void)
{
int rc;
@ -1638,6 +1668,15 @@ static void test_wolfTPM2_EccSignVerifyDig(WOLFTPM2_DEV* dev,
}
#endif
/* Skip if this TPM doesn't implement the requested hash alg. Some TPMs
* (e.g. Nuvoton NPCT75x) only support a subset of hashes; the TPM rejects
* Create with TPM_RC_SIZE param 1, not TPM_RC_HASH, so the existing
* post-hoc skip-check can't catch it. Query capabilities up front. */
if (!test_tpm_alg_supported(hashAlg)) {
printf("Hash alg 0x%x not supported by TPM... Skipping\n", hashAlg);
return;
}
/* -- Use TPM key to sign and verify with wolfCrypt -- */
/* Create ECC key for signing */
rc = wolfTPM2_GetKeyTemplate_ECC_ex(&publicTemplate, hashAlg,