diff --git a/.github/workflows/linux-kmod-build.yml b/.github/workflows/linux-kmod-build.yml
index ca1008b..d684b05 100644
--- a/.github/workflows/linux-kmod-build.yml
+++ b/.github/workflows/linux-kmod-build.yml
@@ -49,7 +49,7 @@ jobs:
steps:
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b #v4.1.5
- - name: Install Buildroot dependencies
+ - name: Install build and test dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
@@ -60,6 +60,7 @@ jobs:
git \
libncurses-dev \
python3 \
+ qemu-system-x86 \
rsync \
unzip \
wget
@@ -152,3 +153,38 @@ jobs:
find ${{ runner.temp }}/buildroot.build -name 'libnat20.a' | grep -q libnat20.a
echo "libnat20.a built successfully:"
find ${{ runner.temp }}/buildroot.build -name 'libnat20.a' -exec ls -la {} \;
+
+ - name: Build rootfs image
+ env:
+ NAT20LIB_OVERRIDE_SRCDIR: ${{ github.workspace }}
+ NAT20DEVICE_OVERRIDE_SRCDIR: ${{ github.workspace }}
+ NAT20CRYPTO_OVERRIDE_SRCDIR: ${{ github.workspace }}
+ NAT20SW_OVERRIDE_SRCDIR: ${{ github.workspace }}
+ LIBNAT20_OVERRIDE_SRCDIR: ${{ github.workspace }}
+ NAT20TEST_OVERRIDE_SRCDIR: ${{ github.workspace }}
+ run: make -C ${{ runner.temp }}/buildroot.build/buildroot -j $(( $(nproc) + 1 ))
+
+ - name: Run integration tests in QEMU
+ timeout-minutes: 5
+ run: |
+ BUILDROOT_DIR="${{ runner.temp }}/buildroot.build/buildroot"
+ KERNEL="${BUILDROOT_DIR}/output/images/bzImage"
+ ROOTFS="${BUILDROOT_DIR}/output/images/rootfs.ext2"
+
+ qemu-system-x86_64 \
+ -M pc \
+ -kernel "${KERNEL}" \
+ -drive file="${ROOTFS}",if=virtio,format=raw \
+ -append "rootwait root=/dev/vda console=ttyS0 init=/usr/bin/nat20test_qemu_init.sh" \
+ -nographic \
+ -no-reboot \
+ -net none \
+ 2>&1 | tee qemu_output.log
+
+ if grep -q "INTEGRATION_TESTS_PASSED" qemu_output.log; then
+ echo "Integration tests passed."
+ else
+ echo "Integration tests failed. QEMU output:"
+ cat qemu_output.log
+ exit 1
+ fi
diff --git a/.gitignore b/.gitignore
index e6fc5f4..1f3b638 100644
--- a/.gitignore
+++ b/.gitignore
@@ -49,4 +49,3 @@ build/
cmake_install.cmake
compile_commands.json
html/
-nat20test
diff --git a/examples/linux/br_external/Config.in b/examples/linux/br_external/Config.in
index 5cba4ea..2ba3073 100644
--- a/examples/linux/br_external/Config.in
+++ b/examples/linux/br_external/Config.in
@@ -38,3 +38,4 @@ source "$BR2_EXTERNAL_NAT20_PATH/package/nat20device/Config.in"
source "$BR2_EXTERNAL_NAT20_PATH/package/nat20sw/Config.in"
source "$BR2_EXTERNAL_NAT20_PATH/package/nat20lib/Config.in"
source "$BR2_EXTERNAL_NAT20_PATH/package/libnat20/Config.in"
+source "$BR2_EXTERNAL_NAT20_PATH/package/nat20test/Config.in"
diff --git a/examples/linux/br_external/bootstrap.sh b/examples/linux/br_external/bootstrap.sh
index 0eaf1d7..ef5a49e 100755
--- a/examples/linux/br_external/bootstrap.sh
+++ b/examples/linux/br_external/bootstrap.sh
@@ -99,6 +99,7 @@ pushd ${LIBNAT20_BR_BUILD_DIR}
echo "LIBNAT20_BR_BUILD_DIR=${LIBNAT20_BR_BUILD_DIR}" | tee .env
echo "LIBNAT20_ROOT=${LIBNAT20_ROOT}" | tee -a .env
+echo "LIBNAT20_PROJECT=${PROJECT}" | tee -a .env
cp ${LIBNAT20_ROOT}/examples/linux/br_external/utils/envsetup.sh ./
@@ -109,7 +110,6 @@ git clone --depth 1 --branch "2025.08.1" https://gitlab.com/buildroot.org/buildr
case "$PROJECT" in
qemu)
cp ${LIBNAT20_ROOT}/examples/linux/br_external/configs/qemu_br_defconfig buildroot/.config
- cp ${LIBNAT20_ROOT}/examples/linux/br_external/run-qemu.sh ./
;;
esac
diff --git a/examples/linux/br_external/configs/qemu_br_defconfig b/examples/linux/br_external/configs/qemu_br_defconfig
index a48abbd..b62ae8d 100644
--- a/examples/linux/br_external/configs/qemu_br_defconfig
+++ b/examples/linux/br_external/configs/qemu_br_defconfig
@@ -3981,3 +3981,4 @@ BR2_PACKAGE_NAT20DEVICE=y
BR2_PACKAGE_NAT20SW=y
BR2_PACKAGE_NAT20LIB=y
BR2_PACKAGE_LIBNAT20=y
+BR2_PACKAGE_NAT20TEST=y
diff --git a/examples/linux/br_external/package/nat20test/Config.in b/examples/linux/br_external/package/nat20test/Config.in
new file mode 100644
index 0000000..ee73801
--- /dev/null
+++ b/examples/linux/br_external/package/nat20test/Config.in
@@ -0,0 +1,42 @@
+# Copyright 2026 Aurora Operations, Inc.
+#
+# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0
+#
+# This work is dual licensed.
+# You may use it under Apache-2.0 or GPL-2.0 at your option.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# OR
+#
+# This program 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.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, see
+# .
+
+config BR2_PACKAGE_NAT20TEST
+ bool "nat20test"
+ depends on BR2_PACKAGE_LIBNAT20
+ depends on BR2_PACKAGE_OPENSSL
+ select BR2_PACKAGE_NAT20SW
+ help
+ Enable building the nat20test, an integration test for nat20device with nat20sw.
diff --git a/examples/linux/br_external/package/nat20test/nat20test.mk b/examples/linux/br_external/package/nat20test/nat20test.mk
new file mode 100644
index 0000000..38e1783
--- /dev/null
+++ b/examples/linux/br_external/package/nat20test/nat20test.mk
@@ -0,0 +1,51 @@
+# Copyright 2026 Aurora Operations, Inc.
+#
+# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0
+#
+# This work is dual licensed.
+# You may use it under Apache-2.0 or GPL-2.0 at your option.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# OR
+#
+# This program 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.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, see
+# .
+
+# In CI NAT20TEST_OVERRIDE_SRCDIR is set to the root of the repository,
+# so that the source under test is always the current branch.
+# Integrators who use this configuration should pin the version
+# to a specific commit or branch to avoid breakages when the main branch changes.
+NAT20TEST_VERSION = origin/main
+NAT20TEST_SITE = https://github.com/aurora-opensource/libnat20.git
+NAT20TEST_SITE_METHOD = git
+NAT20TEST_LICENSE = Apache-2.0 OR GPL-2.0
+NAT20TEST_LICENSE_FILES = LICENSE-Apache-2.0.txt LICENSE-GPL-2.0.txt
+
+NAT20TEST_SUBDIR = examples/linux/nat20test
+
+NAT20TEST_INSTALL_TARGET = YES
+NAT20TEST_DEPENDENCIES += libnat20 openssl
+
+$(eval $(cmake-package))
diff --git a/examples/linux/br_external/utils/envsetup.sh b/examples/linux/br_external/utils/envsetup.sh
index d0b3b80..0156e62 100644
--- a/examples/linux/br_external/utils/envsetup.sh
+++ b/examples/linux/br_external/utils/envsetup.sh
@@ -50,6 +50,7 @@ export NAT20CRYPTO_OVERRIDE_SRCDIR="$LIBNAT20_ROOT"
export NAT20SW_OVERRIDE_SRCDIR="$LIBNAT20_ROOT"
export NAT20DEVICE_OVERRIDE_SRCDIR="$LIBNAT20_ROOT"
export NAT20LIB_OVERRIDE_SRCDIR="$LIBNAT20_ROOT"
+export NAT20TEST_OVERRIDE_SRCDIR="$LIBNAT20_ROOT"
export LIBNAT20_OVERRIDE_SRCDIR="$LIBNAT20_ROOT"
function ensure_popd() {
@@ -77,16 +78,40 @@ function brrebuild() {
echo " nat20device - Rebuild the nat20device module"
echo " nat20sw - Rebuild the nat20sw module"
echo " nat20lib - Rebuild the nat20lib library"
+ echo " nat20test - Rebuild the nat20device integration test"
popd
return 1
fi
case "$1" in
all)
- ensure_popd make linux-rebuild nat20lib-rebuild nat20crypto-rebuild nat20device-rebuild nat20sw-rebuild libnat20-rebuild all
+ ensure_popd make linux-rebuild nat20lib-rebuild nat20crypto-rebuild nat20device-rebuild nat20sw-rebuild libnat20-rebuild nat20test-rebuild all
;;
*)
ensure_popd make $1-rebuild all
;;
esac
}
+
+function run-qemu() {
+ if [ $LIBNAT20_PROJECT != "qemu" ]; then
+ echo "Error: run-qemu is only supported for the qemu project."
+ return 1
+ fi
+
+ QEMU_BIN=qemu-system-x86_64
+
+ BUILDROOT_DIR="${LIBNAT20_BR_BUILD_DIR}/buildroot"
+ KERNEL_IMAGE="${BUILDROOT_DIR}/output/images/bzImage"
+ FS_IMAGE="${BUILDROOT_DIR}/output/images/rootfs.ext2"
+
+ if [ -n "$1" ]; then
+ "${QEMU_BIN}" -M pc -kernel "${KERNEL_IMAGE}" -nographic -drive file="${FS_IMAGE}",if=virtio,format=raw -append "rootwait root=/dev/vda console=ttyS0 init=$1" -serial mon:stdio -net nic,model=virtio -net user
+ else
+ "${QEMU_BIN}" -M pc -kernel "${KERNEL_IMAGE}" -nographic -drive file="${FS_IMAGE}",if=virtio,format=raw -append "rootwait root=/dev/vda console=ttyS0" -serial mon:stdio -net nic,model=virtio -net user
+ fi
+}
+
+function run-nat20test-test() {
+ run-qemu "/usr/bin/nat20test_qemu_init.sh"
+}
diff --git a/examples/linux/nat20test/CMakeLists.txt b/examples/linux/nat20test/CMakeLists.txt
new file mode 100644
index 0000000..d6da07e
--- /dev/null
+++ b/examples/linux/nat20test/CMakeLists.txt
@@ -0,0 +1,82 @@
+# Copyright 2026 Aurora Operations, Inc.
+#
+# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0
+#
+# This work is dual licensed.
+# You may use it under Apache-2.0 or GPL-2.0 at your option.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# OR
+#
+# This program 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.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, see
+# .
+
+cmake_minimum_required(VERSION 3.22)
+
+project(NAT20TEST VERSION 0.0.1 LANGUAGES C)
+
+# The C standard shall be C11.
+set(CMAKE_C_STANDARD 11)
+
+# CMake shall generate a compile_commands.json file for
+# the benefit of clangd based IDE support.
+set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
+
+
+###################################################################################################
+# Integration test binary — exercises the nat20 DICE service via /dev/nat200.
+add_executable(nat20_integration_test)
+
+find_package(LibNat20 REQUIRED)
+find_package(OpenSSL REQUIRED)
+
+target_sources(nat20_integration_test
+PRIVATE test/nat20_integration_test.c
+PRIVATE test/test_helpers.c
+)
+
+target_include_directories(nat20_integration_test
+ PRIVATE test
+)
+
+target_link_libraries(nat20_integration_test
+PRIVATE LibNat20::nat20
+PRIVATE LibNat20::nat20_service
+PRIVATE LibNat20::nat20_crypto_nat20
+PRIVATE OpenSSL::Crypto
+)
+
+target_compile_options(nat20_integration_test
+PRIVATE -pedantic
+PRIVATE -Wall
+PRIVATE -Wextra
+PRIVATE -Werror
+)
+
+install(TARGETS nat20_integration_test RUNTIME DESTINATION bin)
+install(PROGRAMS nat20test.sh DESTINATION bin)
+install(PROGRAMS nat20test_qemu_init.sh DESTINATION bin)
+
+###################################################################################################
diff --git a/examples/linux/br_external/run-qemu.sh b/examples/linux/nat20test/nat20test.sh
similarity index 72%
rename from examples/linux/br_external/run-qemu.sh
rename to examples/linux/nat20test/nat20test.sh
index a37bc9a..75caf86 100755
--- a/examples/linux/br_external/run-qemu.sh
+++ b/examples/linux/nat20test/nat20test.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/bin/sh
# Copyright 2026 Aurora Operations, Inc.
#
@@ -35,18 +35,12 @@
# along with this program; if not, see
# .
-QEMU_BIN=qemu-system-x86_64
+set -e
-if [ ! -f ".env" ]; then
- echo ".env file not found. Please run bootstrap.sh first."
- exit 1
-fi
+SCRIPT_DIR="$(dirname "$0")"
-source .env
+modprobe nat20sw
+mount -t securityfs none /sys/kernel/security
-BUILDROOT_DIR="${LIBNAT20_BR_BUILD_DIR}/buildroot"
-KERNEL_IMAGE="${BUILDROOT_DIR}/output/images/bzImage"
-FS_IMAGE="${BUILDROOT_DIR}/output/images/rootfs.ext2"
-
-
-"${QEMU_BIN}" -M pc -kernel "${KERNEL_IMAGE}" -nographic -drive file="${FS_IMAGE}",if=virtio,format=raw -append "rootwait root=/dev/vda console=ttyS0" -serial mon:stdio -net nic,model=virtio -net user
+echo "Running integration test suite..."
+"${SCRIPT_DIR}/nat20_integration_test"
diff --git a/examples/linux/nat20test/nat20test_qemu_init.sh b/examples/linux/nat20test/nat20test_qemu_init.sh
new file mode 100644
index 0000000..d60b9d5
--- /dev/null
+++ b/examples/linux/nat20test/nat20test_qemu_init.sh
@@ -0,0 +1,60 @@
+#!/bin/sh
+
+# Copyright 2026 Aurora Operations, Inc.
+#
+# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0
+#
+# This work is dual licensed.
+# You may use it under Apache-2.0 or GPL-2.0 at your option.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# OR
+#
+# This program 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.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, see
+# .
+
+# Init wrapper for running nat20test.sh in a QEMU VM.
+# This script is intended to be used as the init process (PID 1).
+# It mounts the necessary filesystems, runs the test suite, prints
+# a machine-parseable result marker, and powers off the VM.
+
+export PATH="/usr/bin:/bin:/sbin:/usr/sbin"
+
+mount -t proc none /proc
+mount -t sysfs none /sys
+mount -t tmpfs none /tmp
+
+cd /tmp
+
+nat20test.sh
+rc=$?
+
+if [ $rc -eq 0 ]; then
+ echo "INTEGRATION_TESTS_PASSED"
+else
+ echo "INTEGRATION_TESTS_FAILED (exit code: $rc)"
+fi
+
+poweroff -f
diff --git a/examples/linux/nat20test/test/nat20_integration_test.c b/examples/linux/nat20test/test/nat20_integration_test.c
new file mode 100644
index 0000000..25afebc
--- /dev/null
+++ b/examples/linux/nat20test/test/nat20_integration_test.c
@@ -0,0 +1,1231 @@
+/*
+ * Copyright 2026 Aurora Operations, Inc.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR GPL-2.0
+ *
+ * This work is dual licensed.
+ * You may use it under Apache-2.0 or GPL-2.0 at your option.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * OR
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see
+ * .
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "test_helpers.h"
+
+#define DEVICE_PATH "/dev/nat200"
+#define DICE_CHAIN_PATH "/sys/kernel/security/nat200/dice_chain"
+
+static int tests_run = 0;
+static int tests_passed = 0;
+static int tests_failed = 0;
+
+/* Start a test case. Call once at the beginning of each test function.
+ * Usage: TEST_BEGIN("descriptive test name"); */
+#define TEST_BEGIN(name) \
+ do { \
+ tests_run++; \
+ printf(" TEST: %s ... ", (name)); \
+ fflush(stdout); \
+ } while (0)
+
+/* Mark the current test as passed. Call once at the end of a successful test.
+ * Usage: TEST_PASS(); */
+#define TEST_PASS() \
+ do { \
+ tests_passed++; \
+ printf("PASS\n"); \
+ fflush(stdout); \
+ } while (0)
+
+/* Mark the current test as failed and print a diagnostic message.
+ * The first variadic argument is a printf format string; subsequent
+ * arguments are format parameters.
+ * Usage: TEST_FAIL("expected %d, got %d", expected, actual); */
+#define TEST_FAIL(...) \
+ do { \
+ tests_failed++; \
+ printf("FAIL\n"); \
+ fprintf(stderr, " " __VA_ARGS__); \
+ fprintf(stderr, "\n"); \
+ fflush(stderr); \
+ } while (0)
+
+/* Assert a condition. On failure, prints a diagnostic and returns from
+ * the enclosing function (marking the test as failed).
+ * The first argument is the condition; the remaining variadic arguments
+ * form a printf-style diagnostic message.
+ * Usage: ASSERT(ptr != NULL, "allocation failed for size %zu", size); */
+#define ASSERT(cond, ...) \
+ do { \
+ if (!(cond)) { \
+ TEST_FAIL(__VA_ARGS__); \
+ return; \
+ } \
+ } while (0)
+
+/* Assert equality. Convenience wrapper around ASSERT for comparing two values.
+ * Usage: ASSERT_EQ(err, n20_error_ok_e, "unexpected error: 0x%x", err); */
+#define ASSERT_EQ(a, b, ...) ASSERT((a) == (b), __VA_ARGS__)
+
+static ssize_t dispatch_request(uint8_t const* request,
+ size_t request_size,
+ uint8_t* response,
+ size_t response_size) {
+ int fd = open(DEVICE_PATH, O_RDWR);
+ if (fd < 0) {
+ perror("open " DEVICE_PATH);
+ return -1;
+ }
+
+ ssize_t written = write(fd, request, request_size);
+ if (written < 0) {
+ perror("write");
+ close(fd);
+ return -1;
+ }
+
+ ssize_t received = read(fd, response, response_size);
+ if (received < 0) {
+ perror("read");
+ close(fd);
+ return -1;
+ }
+
+ close(fd);
+ return received;
+}
+
+static n20_error_t send_request(n20_msg_request_t const* request,
+ uint8_t* response_buffer,
+ size_t response_buffer_size,
+ n20_slice_t* response_out) {
+ uint8_t msg_buffer[1024];
+ size_t msg_size = sizeof(msg_buffer);
+
+ n20_error_t err = n20_msg_request_write(request, msg_buffer, &msg_size);
+ if (err != n20_error_ok_e) {
+ return err;
+ }
+
+ ssize_t received = dispatch_request(msg_buffer + (sizeof(msg_buffer) - msg_size),
+ msg_size,
+ response_buffer,
+ response_buffer_size);
+ if (received < 0) {
+ return n20_error_crypto_implementation_specific_e;
+ }
+
+ response_out->buffer = response_buffer;
+ response_out->size = (size_t)received;
+ return n20_error_ok_e;
+}
+
+static void test_dice_chain_readable(void) {
+ TEST_BEGIN("dice_chain is readable from securityfs");
+
+ int fd = open(DICE_CHAIN_PATH, O_RDONLY);
+ ASSERT(fd >= 0, "Cannot open %s", DICE_CHAIN_PATH);
+
+ uint8_t buffer[4096];
+ ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
+ close(fd);
+
+ ASSERT(bytes_read > 0, "dice_chain is empty");
+ ASSERT_EQ(
+ buffer[0], 0x9f, "Expected CBOR indefinite array start (0x9f), got 0x%02x", buffer[0]);
+ ASSERT_EQ(buffer[bytes_read - 1],
+ 0xff,
+ "Expected CBOR break (0xff) at end, got 0x%02x",
+ buffer[bytes_read - 1]);
+
+ TEST_PASS();
+}
+
+static void test_cdi_cert_x509_p256(void) {
+ TEST_BEGIN("cdi-cert X.509 P-256");
+
+ n20_msg_request_t request = {0};
+ request.request_type = n20_msg_request_type_issue_cdi_cert_e;
+ request.payload.issue_cdi_cert.issuer_key_type = n20_crypto_key_type_secp256r1_e;
+ request.payload.issue_cdi_cert.subject_key_type = n20_crypto_key_type_secp256r1_e;
+ request.payload.issue_cdi_cert.certificate_format = n20_certificate_format_x509_e;
+
+ uint8_t response_buffer[2048];
+ n20_slice_t response;
+ n20_error_t err = send_request(&request, response_buffer, sizeof(response_buffer), &response);
+ ASSERT_EQ(err, n20_error_ok_e, "send_request failed: 0x%x", err);
+
+ n20_msg_issue_cert_response_t cert_response;
+ err = n20_msg_issue_cert_response_read(&cert_response, response);
+ ASSERT_EQ(err, n20_error_ok_e, "Failed to parse cert response: 0x%x", err);
+ ASSERT_EQ(cert_response.error_code,
+ n20_error_ok_e,
+ "Service returned error: 0x%x",
+ cert_response.error_code);
+ ASSERT(cert_response.certificate.size > 0, "Certificate is empty");
+ ASSERT_EQ(cert_response.certificate.buffer[0],
+ 0x30,
+ "Expected DER SEQUENCE tag (0x30), got 0x%02x",
+ cert_response.certificate.buffer[0]);
+
+ TEST_PASS();
+}
+
+static void test_cdi_cert_cose_p256(void) {
+ TEST_BEGIN("cdi-cert COSE P-256");
+
+ n20_msg_request_t request = {0};
+ request.request_type = n20_msg_request_type_issue_cdi_cert_e;
+ request.payload.issue_cdi_cert.issuer_key_type = n20_crypto_key_type_secp256r1_e;
+ request.payload.issue_cdi_cert.subject_key_type = n20_crypto_key_type_secp256r1_e;
+ request.payload.issue_cdi_cert.certificate_format = n20_certificate_format_cose_e;
+
+ uint8_t response_buffer[2048];
+ n20_slice_t response;
+ n20_error_t err = send_request(&request, response_buffer, sizeof(response_buffer), &response);
+ ASSERT_EQ(err, n20_error_ok_e, "send_request failed: 0x%x", err);
+
+ n20_msg_issue_cert_response_t cert_response;
+ err = n20_msg_issue_cert_response_read(&cert_response, response);
+ ASSERT_EQ(err, n20_error_ok_e, "Failed to parse cert response: 0x%x", err);
+ ASSERT_EQ(cert_response.error_code,
+ n20_error_ok_e,
+ "Service returned error: 0x%x",
+ cert_response.error_code);
+ ASSERT(cert_response.certificate.size > 0, "Certificate is empty");
+
+ n20_istream_t istream;
+ n20_istream_init(&istream, cert_response.certificate.buffer, cert_response.certificate.size);
+ n20_cbor_type_t type;
+ uint64_t value;
+ bool ok = n20_cbor_read_header(&istream, &type, &value);
+ ASSERT(ok, "Failed to parse COSE_Sign1 CBOR header");
+ ASSERT_EQ(type, n20_cbor_type_array_e, "Expected CBOR array, got type %d", (int)type);
+ ASSERT_EQ(value, 4u, "COSE_Sign1 must have 4 elements, got %llu", (unsigned long long)value);
+
+ TEST_PASS();
+}
+
+static void test_eca_cert_x509_p256(void) {
+ TEST_BEGIN("eca-cert X.509 P-256");
+
+ n20_msg_request_t request = {0};
+ request.request_type = n20_msg_request_type_issue_eca_cert_e;
+ request.payload.issue_eca_cert.issuer_key_type = n20_crypto_key_type_secp256r1_e;
+ request.payload.issue_eca_cert.subject_key_type = n20_crypto_key_type_secp256r1_e;
+ request.payload.issue_eca_cert.certificate_format = n20_certificate_format_x509_e;
+
+ uint8_t response_buffer[2048];
+ n20_slice_t response;
+ n20_error_t err = send_request(&request, response_buffer, sizeof(response_buffer), &response);
+ ASSERT_EQ(err, n20_error_ok_e, "send_request failed: 0x%x", err);
+
+ n20_msg_issue_cert_response_t cert_response;
+ err = n20_msg_issue_cert_response_read(&cert_response, response);
+ ASSERT_EQ(err, n20_error_ok_e, "Failed to parse cert response: 0x%x", err);
+ ASSERT_EQ(cert_response.error_code,
+ n20_error_ok_e,
+ "Service returned error: 0x%x",
+ cert_response.error_code);
+ ASSERT(cert_response.certificate.size > 0, "ECA certificate is empty");
+ ASSERT_EQ(cert_response.certificate.buffer[0],
+ 0x30,
+ "Expected DER SEQUENCE tag (0x30), got 0x%02x",
+ cert_response.certificate.buffer[0]);
+
+ TEST_PASS();
+}
+
+static void test_eca_ee_cert_x509_p256(void) {
+ TEST_BEGIN("eca-ee-cert X.509 P-256");
+
+ uint8_t key_usage[] = {0x01};
+ n20_msg_request_t request = {0};
+ request.request_type = n20_msg_request_type_issue_eca_ee_cert_e;
+ request.payload.issue_eca_ee_cert.issuer_key_type = n20_crypto_key_type_secp256r1_e;
+ request.payload.issue_eca_ee_cert.subject_key_type = n20_crypto_key_type_secp256r1_e;
+ request.payload.issue_eca_ee_cert.certificate_format = n20_certificate_format_x509_e;
+ request.payload.issue_eca_ee_cert.name = (n20_string_slice_t){.size = 4, .buffer = "test"};
+ request.payload.issue_eca_ee_cert.key_usage =
+ (n20_slice_t){.size = sizeof(key_usage), .buffer = key_usage};
+
+ uint8_t response_buffer[2048];
+ n20_slice_t response;
+ n20_error_t err = send_request(&request, response_buffer, sizeof(response_buffer), &response);
+ ASSERT_EQ(err, n20_error_ok_e, "send_request failed: 0x%x", err);
+
+ n20_msg_issue_cert_response_t cert_response;
+ err = n20_msg_issue_cert_response_read(&cert_response, response);
+ ASSERT_EQ(err, n20_error_ok_e, "Failed to parse cert response: 0x%x", err);
+ ASSERT_EQ(cert_response.error_code,
+ n20_error_ok_e,
+ "Service returned error: 0x%x",
+ cert_response.error_code);
+ ASSERT(cert_response.certificate.size > 0, "ECA EE certificate is empty");
+ ASSERT_EQ(cert_response.certificate.buffer[0],
+ 0x30,
+ "Expected DER SEQUENCE tag (0x30), got 0x%02x",
+ cert_response.certificate.buffer[0]);
+
+ TEST_PASS();
+}
+
+static void test_eca_ee_sign_p256(void) {
+ TEST_BEGIN("eca-ee-sign P-256");
+
+ uint8_t key_usage[] = {0x01};
+ uint8_t message[] = "test message to sign";
+ n20_msg_request_t request = {0};
+ request.request_type = n20_msg_request_type_eca_ee_sign_e;
+ request.payload.eca_ee_sign.subject_key_type = n20_crypto_key_type_secp256r1_e;
+ request.payload.eca_ee_sign.name = (n20_string_slice_t){.size = 4, .buffer = "test"};
+ request.payload.eca_ee_sign.key_usage =
+ (n20_slice_t){.size = sizeof(key_usage), .buffer = key_usage};
+ request.payload.eca_ee_sign.message =
+ (n20_slice_t){.size = sizeof(message) - 1, .buffer = message};
+
+ uint8_t response_buffer[1024];
+ n20_slice_t response;
+ n20_error_t err = send_request(&request, response_buffer, sizeof(response_buffer), &response);
+ ASSERT_EQ(err, n20_error_ok_e, "send_request failed: 0x%x", err);
+
+ n20_msg_eca_ee_sign_response_t sign_response;
+ err = n20_msg_eca_ee_sign_response_read(&sign_response, response);
+ ASSERT_EQ(err, n20_error_ok_e, "Failed to parse sign response: 0x%x", err);
+ ASSERT_EQ(sign_response.error_code,
+ n20_error_ok_e,
+ "Service returned error: 0x%x",
+ sign_response.error_code);
+ ASSERT_EQ(sign_response.signature.size,
+ 64u,
+ "P-256 signature should be 64 bytes, got %zu",
+ sign_response.signature.size);
+
+ TEST_PASS();
+}
+
+static uint8_t const TEST_CODE_HASH[32] = {
+ 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
+ 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
+};
+static uint8_t const TEST_CONFIG_HASH[32] = {
+ 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d,
+ 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d,
+};
+static uint8_t const TEST_AUTHORITY_HASH[32] = {
+ 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a,
+ 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a,
+};
+static uint8_t const TEST_HIDDEN[32] = {
+ 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
+ 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
+};
+
+/*
+ * Full chain generation and verification test.
+ *
+ * The test exercises the full DICE certificate chain across all supported
+ * key type and format permutations. Since promote is irreversible, all
+ * certificates at a given level must be generated before promoting.
+ *
+ * Structure:
+ * - At each level, generate CDI/ECA/ECA_EE/sign for all key_type × format combos
+ * - Also generate ECA/ECA_EE/sign via parent_path from earlier levels
+ * - After all promotes, verify chains and check parent_path equivalence
+ */
+
+typedef struct {
+ uint8_t data[2048];
+ size_t size;
+} cert_buffer_t;
+
+typedef struct {
+ uint8_t data[128];
+ size_t size;
+} sig_buffer_t;
+
+static bool issue_cdi_cert(n20_crypto_key_type_t issuer_key_type,
+ n20_crypto_key_type_t subject_key_type,
+ n20_certificate_format_t format,
+ n20_parent_path_t parent_path,
+ cert_buffer_t* out) {
+ n20_msg_request_t request = {0};
+ request.request_type = n20_msg_request_type_issue_cdi_cert_e;
+ request.payload.issue_cdi_cert.issuer_key_type = issuer_key_type;
+ request.payload.issue_cdi_cert.subject_key_type = subject_key_type;
+ request.payload.issue_cdi_cert.certificate_format = format;
+ request.payload.issue_cdi_cert.parent_path = parent_path;
+ request.payload.issue_cdi_cert.next_context.code_hash =
+ (n20_slice_t){.size = sizeof(TEST_CODE_HASH), .buffer = TEST_CODE_HASH};
+ request.payload.issue_cdi_cert.next_context.configuration_hash =
+ (n20_slice_t){.size = sizeof(TEST_CONFIG_HASH), .buffer = TEST_CONFIG_HASH};
+ request.payload.issue_cdi_cert.next_context.authority_hash =
+ (n20_slice_t){.size = sizeof(TEST_AUTHORITY_HASH), .buffer = TEST_AUTHORITY_HASH};
+ request.payload.issue_cdi_cert.next_context.mode = n20_open_dice_mode_normal_e;
+ request.payload.issue_cdi_cert.next_context.hidden =
+ (n20_slice_t){.size = sizeof(TEST_HIDDEN), .buffer = TEST_HIDDEN};
+
+ uint8_t response_buffer[2048];
+ n20_slice_t response;
+ if (send_request(&request, response_buffer, sizeof(response_buffer), &response) !=
+ n20_error_ok_e) {
+ return false;
+ }
+
+ n20_msg_issue_cert_response_t cert_response;
+ if (n20_msg_issue_cert_response_read(&cert_response, response) != n20_error_ok_e) {
+ return false;
+ }
+ if (cert_response.error_code != n20_error_ok_e) {
+ fprintf(stderr, " cdi-cert error: 0x%x\n", cert_response.error_code);
+ return false;
+ }
+ if (cert_response.certificate.size > sizeof(out->data)) {
+ return false;
+ }
+
+ memcpy(out->data, cert_response.certificate.buffer, cert_response.certificate.size);
+ out->size = cert_response.certificate.size;
+ return true;
+}
+
+static bool issue_eca_cert(n20_crypto_key_type_t issuer_key_type,
+ n20_crypto_key_type_t subject_key_type,
+ n20_certificate_format_t format,
+ n20_parent_path_t parent_path,
+ cert_buffer_t* out) {
+ n20_msg_request_t request = {0};
+ request.request_type = n20_msg_request_type_issue_eca_cert_e;
+ request.payload.issue_eca_cert.issuer_key_type = issuer_key_type;
+ request.payload.issue_eca_cert.subject_key_type = subject_key_type;
+ request.payload.issue_eca_cert.certificate_format = format;
+ request.payload.issue_eca_cert.parent_path = parent_path;
+
+ uint8_t response_buffer[2048];
+ n20_slice_t response;
+ if (send_request(&request, response_buffer, sizeof(response_buffer), &response) !=
+ n20_error_ok_e) {
+ return false;
+ }
+
+ n20_msg_issue_cert_response_t cert_response;
+ if (n20_msg_issue_cert_response_read(&cert_response, response) != n20_error_ok_e) {
+ return false;
+ }
+ if (cert_response.error_code != n20_error_ok_e) {
+ fprintf(stderr, " eca-cert error: 0x%x\n", cert_response.error_code);
+ return false;
+ }
+ if (cert_response.certificate.size > sizeof(out->data)) {
+ return false;
+ }
+
+ memcpy(out->data, cert_response.certificate.buffer, cert_response.certificate.size);
+ out->size = cert_response.certificate.size;
+ return true;
+}
+
+static bool issue_eca_ee_cert(n20_crypto_key_type_t issuer_key_type,
+ n20_crypto_key_type_t subject_key_type,
+ n20_certificate_format_t format,
+ n20_parent_path_t parent_path,
+ cert_buffer_t* out) {
+ uint8_t key_usage[] = {0x01};
+ n20_msg_request_t request = {0};
+ request.request_type = n20_msg_request_type_issue_eca_ee_cert_e;
+ request.payload.issue_eca_ee_cert.issuer_key_type = issuer_key_type;
+ request.payload.issue_eca_ee_cert.subject_key_type = subject_key_type;
+ request.payload.issue_eca_ee_cert.certificate_format = format;
+ request.payload.issue_eca_ee_cert.parent_path = parent_path;
+ request.payload.issue_eca_ee_cert.name = (n20_string_slice_t){.size = 7, .buffer = "testkey"};
+ request.payload.issue_eca_ee_cert.key_usage =
+ (n20_slice_t){.size = sizeof(key_usage), .buffer = key_usage};
+
+ uint8_t response_buffer[2048];
+ n20_slice_t response;
+ if (send_request(&request, response_buffer, sizeof(response_buffer), &response) !=
+ n20_error_ok_e) {
+ return false;
+ }
+
+ n20_msg_issue_cert_response_t cert_response;
+ if (n20_msg_issue_cert_response_read(&cert_response, response) != n20_error_ok_e) {
+ return false;
+ }
+ if (cert_response.error_code != n20_error_ok_e) {
+ fprintf(stderr, " eca-ee-cert error: 0x%x\n", cert_response.error_code);
+ return false;
+ }
+ if (cert_response.certificate.size > sizeof(out->data)) {
+ return false;
+ }
+
+ memcpy(out->data, cert_response.certificate.buffer, cert_response.certificate.size);
+ out->size = cert_response.certificate.size;
+ return true;
+}
+
+static bool eca_ee_sign(n20_crypto_key_type_t key_type,
+ n20_parent_path_t parent_path,
+ uint8_t const* message,
+ size_t message_size,
+ sig_buffer_t* out) {
+ uint8_t key_usage[] = {0x01};
+ n20_msg_request_t request = {0};
+ request.request_type = n20_msg_request_type_eca_ee_sign_e;
+ request.payload.eca_ee_sign.subject_key_type = key_type;
+ request.payload.eca_ee_sign.parent_path = parent_path;
+ request.payload.eca_ee_sign.name = (n20_string_slice_t){.size = 7, .buffer = "testkey"};
+ request.payload.eca_ee_sign.key_usage =
+ (n20_slice_t){.size = sizeof(key_usage), .buffer = key_usage};
+ request.payload.eca_ee_sign.message = (n20_slice_t){.size = message_size, .buffer = message};
+
+ uint8_t response_buffer[1024];
+ n20_slice_t response;
+ if (send_request(&request, response_buffer, sizeof(response_buffer), &response) !=
+ n20_error_ok_e) {
+ return false;
+ }
+
+ n20_msg_eca_ee_sign_response_t sign_response;
+ if (n20_msg_eca_ee_sign_response_read(&sign_response, response) != n20_error_ok_e) {
+ return false;
+ }
+ if (sign_response.error_code != n20_error_ok_e) {
+ fprintf(stderr, " eca-ee-sign error: 0x%x\n", sign_response.error_code);
+ return false;
+ }
+ if (sign_response.signature.size > sizeof(out->data)) {
+ return false;
+ }
+
+ memcpy(out->data, sign_response.signature.buffer, sign_response.signature.size);
+ out->size = sign_response.signature.size;
+ return true;
+}
+
+static bool do_promote(uint8_t const* compressed_input, size_t compressed_input_size) {
+ n20_msg_request_t request = {0};
+ request.request_type = n20_msg_request_type_promote_e;
+ request.payload.promote.compressed_context =
+ (n20_slice_t){.size = compressed_input_size, .buffer = compressed_input};
+
+ uint8_t response_buffer[1024];
+ n20_slice_t response;
+ if (send_request(&request, response_buffer, sizeof(response_buffer), &response) !=
+ n20_error_ok_e) {
+ return false;
+ }
+
+ n20_msg_error_response_t promote_resp;
+ if (n20_msg_error_response_read(&promote_resp, response) != n20_error_ok_e) {
+ return false;
+ }
+ if (promote_resp.error_code != n20_error_ok_e) {
+ fprintf(stderr, " promote error: 0x%x\n", promote_resp.error_code);
+ return false;
+ }
+ return true;
+}
+
+static bool read_uds_cert(cert_buffer_t* out) {
+ int fd = open(DICE_CHAIN_PATH, O_RDONLY);
+ if (fd < 0) {
+ return false;
+ }
+
+ uint8_t dice_chain_buf[4096];
+ ssize_t dc_size = read(fd, dice_chain_buf, sizeof(dice_chain_buf));
+ close(fd);
+ if (dc_size <= 10) {
+ return false;
+ }
+
+ n20_istream_t dc_stream;
+ n20_istream_init(&dc_stream, dice_chain_buf, (size_t)dc_size);
+ n20_cbor_type_t cbor_type;
+ uint64_t cbor_value;
+ n20_cbor_read_header(&dc_stream, &cbor_type, &cbor_value);
+ n20_cbor_read_header(&dc_stream, &cbor_type, &cbor_value);
+ n20_cbor_read_header(&dc_stream, &cbor_type, &cbor_value);
+ n20_slice_t uds_cert_slice;
+ if (!n20_istream_get_slice(&dc_stream, &uds_cert_slice, cbor_value)) {
+ return false;
+ }
+ if (uds_cert_slice.size > sizeof(out->data)) {
+ return false;
+ }
+ memcpy(out->data, uds_cert_slice.buffer, uds_cert_slice.size);
+ out->size = uds_cert_slice.size;
+ return true;
+}
+
+/* Key types to test. */
+static n20_crypto_key_type_t const KEY_TYPES[] = {
+ n20_crypto_key_type_secp256r1_e,
+ n20_crypto_key_type_secp384r1_e,
+};
+#define NUM_KEY_TYPES (sizeof(KEY_TYPES) / sizeof(KEY_TYPES[0]))
+
+/* Certificate format variants for CDI certs. ECA/ECA_EE are X.509 only. */
+static n20_certificate_format_t const CDI_FORMATS[] = {
+ n20_certificate_format_x509_e,
+#if N20_WITH_COSE == 1
+ n20_certificate_format_cose_e,
+#endif
+};
+#define NUM_CDI_FORMATS (sizeof(CDI_FORMATS) / sizeof(CDI_FORMATS[0]))
+
+/*
+ * Data structure to hold all artifacts generated at level 1 (before any promote).
+ *
+ * CDI1: issued at level 0 with no parent path.
+ * Dimensions: subject_key_type[NUM_KEY_TYPES] × format[NUM_CDI_FORMATS]
+ * Issuer key type is always P-256 (the UDS key type).
+ *
+ * CDI2: issued at level 0 with parent_path depth 1.
+ * Dimensions: issuer_key_type[NUM_KEY_TYPES] × subject_key_type[NUM_KEY_TYPES]
+ * × format[NUM_CDI_FORMATS]
+ * The issuer_key_type selects the signing key derivation path from the
+ * parent CDI.
+ *
+ * ECA: issued at level 0 with parent_path depth 2.
+ * Dimensions: issuer_key_type[NUM_KEY_TYPES] × subject_key_type[NUM_KEY_TYPES]
+ * Format is always X.509.
+ *
+ * ECA_EE: issued at level 0 with parent_path depth 2.
+ * The issuer key for ECA_EE is the ECA's subject key.
+ * Dimensions: eca_key_type[NUM_KEY_TYPES] × ee_subject_key_type[NUM_KEY_TYPES]
+ * Format is always X.509.
+ *
+ * Signature: issued at level 0 with parent_path depth 2.
+ * The signing key type is the ECA_EE's subject key type.
+ * Dimensions: ee_subject_key_type[NUM_KEY_TYPES]
+ */
+
+typedef struct {
+ /* CDI1 certs: indexed by [subject_key_type_idx][format_idx] */
+ cert_buffer_t cdi1[NUM_KEY_TYPES][NUM_CDI_FORMATS];
+ bool cdi1_valid[NUM_KEY_TYPES][NUM_CDI_FORMATS];
+
+ /* CDI2 certs (via parent path depth 1): indexed by
+ * [issuer_key_type_idx][subject_key_type_idx][format_idx] */
+ cert_buffer_t cdi2[NUM_KEY_TYPES][NUM_KEY_TYPES][NUM_CDI_FORMATS];
+ bool cdi2_valid[NUM_KEY_TYPES][NUM_KEY_TYPES][NUM_CDI_FORMATS];
+
+ /* ECA certs (via parent path depth 2): indexed by [issuer_key_type_idx][subject_key_type_idx]
+ */
+ cert_buffer_t eca[NUM_KEY_TYPES][NUM_KEY_TYPES];
+ bool eca_valid[NUM_KEY_TYPES][NUM_KEY_TYPES];
+
+ /* ECA_EE certs (via parent path depth 2):
+ * indexed by [eca_subject_key_type_idx][ee_subject_key_type_idx]
+ * The ECA_EE issuer_key_type = ECA's subject_key_type. */
+ cert_buffer_t eca_ee[NUM_KEY_TYPES][NUM_KEY_TYPES];
+ bool eca_ee_valid[NUM_KEY_TYPES][NUM_KEY_TYPES];
+
+ /* Signatures (via parent path depth 2):
+ * indexed by [ee_subject_key_type_idx] */
+ sig_buffer_t signature[NUM_KEY_TYPES];
+ bool signature_valid[NUM_KEY_TYPES];
+} level_artifacts_t;
+
+static level_artifacts_t level1_artifacts;
+static cert_buffer_t uds_cert;
+static uint8_t compressed_input[N20_FUNC_COMPRESSED_INPUT_SIZE];
+static uint8_t const test_message[] = "DICE chain integration test message";
+
+static void test_level1(void) {
+ TEST_BEGIN("Level 1: generate all certs at UDS level");
+
+ n20_parent_path_t no_path = N20_MSG_PARENT_PATH_EMPTY;
+ n20_slice_t path_elements[2] = {
+ {.size = sizeof(compressed_input), .buffer = compressed_input},
+ {.size = sizeof(compressed_input), .buffer = compressed_input},
+ };
+ n20_parent_path_t path_depth1 = {
+ .length = 1, .is_encoded = false, .decoded = &path_elements[0]};
+ n20_parent_path_t path_depth2 = {
+ .length = 2, .is_encoded = false, .decoded = &path_elements[0]};
+
+ n20_error_t err = test_compress_cdi_input(TEST_CODE_HASH,
+ sizeof(TEST_CODE_HASH),
+ TEST_CONFIG_HASH,
+ sizeof(TEST_CONFIG_HASH),
+ TEST_AUTHORITY_HASH,
+ sizeof(TEST_AUTHORITY_HASH),
+ (uint8_t)n20_open_dice_mode_normal_e,
+ TEST_HIDDEN,
+ sizeof(TEST_HIDDEN),
+ compressed_input,
+ sizeof(compressed_input));
+ ASSERT_EQ(err, n20_error_ok_e, "compress_cdi_input failed: 0x%x", err);
+
+ ASSERT(read_uds_cert(&uds_cert), "Failed to read UDS cert");
+
+ /* CDI1: subject_key_type × format, issuer = P-256, no parent path */
+ for (size_t si = 0; si < NUM_KEY_TYPES; si++) {
+ for (size_t fi = 0; fi < NUM_CDI_FORMATS; fi++) {
+ ASSERT((level1_artifacts.cdi1_valid[si][fi] =
+ issue_cdi_cert(n20_crypto_key_type_secp256r1_e,
+ KEY_TYPES[si],
+ CDI_FORMATS[fi],
+ no_path,
+ &level1_artifacts.cdi1[si][fi])),
+ "Failed to issue CDI1 cert (subject key type %d, format %d)",
+ KEY_TYPES[si],
+ CDI_FORMATS[fi]);
+ }
+ }
+
+ /* CDI2: issuer_key_type × subject_key_type × format, parent_path depth 1 */
+ for (size_t ii = 0; ii < NUM_KEY_TYPES; ii++) {
+ for (size_t si = 0; si < NUM_KEY_TYPES; si++) {
+ for (size_t fi = 0; fi < NUM_CDI_FORMATS; fi++) {
+ ASSERT((level1_artifacts.cdi2_valid[ii][si][fi] =
+ issue_cdi_cert(KEY_TYPES[ii],
+ KEY_TYPES[si],
+ CDI_FORMATS[fi],
+ path_depth1,
+ &level1_artifacts.cdi2[ii][si][fi])),
+ "Failed to issue CDI2 cert (issuer key type %d, subject key type %d, format "
+ "%d)",
+ KEY_TYPES[ii],
+ KEY_TYPES[si],
+ CDI_FORMATS[fi]);
+ }
+ }
+ }
+
+ /* ECA: issuer_key_type × subject_key_type, parent_path depth 2 */
+ for (size_t ii = 0; ii < NUM_KEY_TYPES; ii++) {
+ for (size_t si = 0; si < NUM_KEY_TYPES; si++) {
+ ASSERT((level1_artifacts.eca_valid[ii][si] =
+ issue_eca_cert(KEY_TYPES[ii],
+ KEY_TYPES[si],
+ n20_certificate_format_x509_e,
+ path_depth2,
+ &level1_artifacts.eca[ii][si])),
+ "Failed to issue ECA cert (issuer key type %d, subject key type %d)",
+ KEY_TYPES[ii],
+ KEY_TYPES[si]);
+ }
+ }
+
+ /* ECA_EE: eca_subject_key_type × ee_subject_key_type, parent_path depth 2.
+ * The ECA_EE issuer key type = ECA subject key type. */
+ for (size_t ei = 0; ei < NUM_KEY_TYPES; ei++) {
+ for (size_t si = 0; si < NUM_KEY_TYPES; si++) {
+ ASSERT((level1_artifacts.eca_ee_valid[ei][si] =
+ issue_eca_ee_cert(KEY_TYPES[ei],
+ KEY_TYPES[si],
+ n20_certificate_format_x509_e,
+ path_depth2,
+ &level1_artifacts.eca_ee[ei][si])),
+ "Failed to issue ECA_EE cert (eca subject key type %d, ee subject key type %d)",
+ KEY_TYPES[ei],
+ KEY_TYPES[si]);
+ }
+ }
+
+ /* Signature: ee_subject_key_type, parent_path depth 2 */
+ for (size_t si = 0; si < NUM_KEY_TYPES; si++) {
+ ASSERT(
+ (level1_artifacts.signature_valid[si] = eca_ee_sign(KEY_TYPES[si],
+ path_depth2,
+ test_message,
+ sizeof(test_message) - 1,
+ &level1_artifacts.signature[si])),
+ "Failed to issue signature (ee subject key type %d)",
+ KEY_TYPES[si]);
+ }
+
+ /* Verification: check X.509 chains where applicable */
+ uint8_t uds_pubkey[97];
+ size_t uds_pubkey_size = sizeof(uds_pubkey);
+ ASSERT(test_extract_x509_pubkey(uds_cert.data, uds_cert.size, uds_pubkey, &uds_pubkey_size),
+ "Failed to extract UDS public key");
+ ASSERT(test_verify_x509_signature(uds_cert.data,
+ uds_cert.size,
+ uds_pubkey,
+ uds_pubkey_size,
+ n20_crypto_key_type_secp256r1_e),
+ "UDS self-signed verification failed");
+
+ /* Verify CDI1 X.509 certs against UDS key */
+ for (size_t si = 0; si < NUM_KEY_TYPES; si++) {
+ if (!level1_artifacts.cdi1_valid[si][0]) {
+ continue; /* X.509 is index 0 */
+ }
+ ASSERT(test_verify_x509_signature(level1_artifacts.cdi1[si][0].data,
+ level1_artifacts.cdi1[si][0].size,
+ uds_pubkey,
+ uds_pubkey_size,
+ n20_crypto_key_type_secp256r1_e),
+ "CDI1 X.509 (sub=%d) verification against UDS failed",
+ KEY_TYPES[si]);
+ if (NUM_CDI_FORMATS > 1 && level1_artifacts.cdi1_valid[si][1]) {
+ /* If COSE format also generated, verify signature using same UDS key */
+ test_cose_sign1_t cose_sign1 = {0};
+ ASSERT(test_parse_cose_sign1(level1_artifacts.cdi1[si][1].data,
+ level1_artifacts.cdi1[si][1].size,
+ &cose_sign1),
+ "Failed to parse CDI1 COSE_Sign1 cert (sub=%d)",
+ KEY_TYPES[si]);
+ ASSERT(test_verify_cose_sign1(&cose_sign1,
+ uds_pubkey + 1,
+ uds_pubkey_size - 1,
+ n20_crypto_key_type_secp256r1_e),
+ "CDI1 COSE (sub=%d) verification against UDS failed",
+ KEY_TYPES[si]);
+ }
+ }
+
+ /* Verify CDI2 X.509 against CDI1 subject key (P-256 CDI1 -> CDI2) */
+ for (size_t issfi = 0; issfi < NUM_CDI_FORMATS; issfi++) {
+ for (size_t ii = 0; ii < NUM_KEY_TYPES; ii++) {
+ /* CDI 1 subject key is the issuer for the CDI2 certs.
+ * So use issuer index ii as subject index of the CDI1 matrix. */
+ if (level1_artifacts.cdi1_valid[ii][issfi]) {
+ uint8_t cdi1_pubkey[97];
+ size_t cdi1_pubkey_size = sizeof(cdi1_pubkey);
+ if (issfi == 0) {
+ /* X.509 format: extract pubkey from cert */
+ ASSERT(test_extract_x509_pubkey(level1_artifacts.cdi1[ii][issfi].data,
+ level1_artifacts.cdi1[ii][issfi].size,
+ cdi1_pubkey,
+ &cdi1_pubkey_size),
+ "Failed to extract CDI1 public key");
+ } else {
+ /* COSE format: pubkey is the COSE_Sign1 payload */
+ n20_crypto_key_type_t got_key_type;
+ ASSERT(test_extract_cose_pubkey(level1_artifacts.cdi1[ii][issfi].data,
+ level1_artifacts.cdi1[ii][issfi].size,
+ cdi1_pubkey,
+ &cdi1_pubkey_size,
+ &got_key_type),
+ "Failed to extract CDI1 COSE_Sign1 cert (sub=%d)",
+ KEY_TYPES[ii]);
+ ASSERT_EQ(got_key_type,
+ KEY_TYPES[ii],
+ "Unexpected key type extracted from CDI1 COSE_Sign1 cert (sub=%d)",
+ KEY_TYPES[ii]);
+ }
+ for (size_t si = 0; si < NUM_KEY_TYPES; si++) {
+ if (!level1_artifacts.cdi2_valid[ii][si][0]) {
+ continue;
+ }
+ ASSERT(test_verify_x509_signature(level1_artifacts.cdi2[ii][si][0].data,
+ level1_artifacts.cdi2[ii][si][0].size,
+ cdi1_pubkey,
+ cdi1_pubkey_size,
+ KEY_TYPES[ii]),
+ "CDI2 X.509 (issf=%zu, iss=%d, sub=%d) verification against CDI1 failed",
+ issfi,
+ KEY_TYPES[ii],
+ KEY_TYPES[si]);
+ if (NUM_CDI_FORMATS > 1 && level1_artifacts.cdi2_valid[ii][si][1]) {
+ /* If COSE format also generated, verify signature using CDI1 key */
+ test_cose_sign1_t cose_sign1 = {0};
+ ASSERT(test_parse_cose_sign1(level1_artifacts.cdi2[ii][si][1].data,
+ level1_artifacts.cdi2[ii][si][1].size,
+ &cose_sign1),
+ "Failed to parse CDI2 COSE_Sign1 cert (sub=%d)",
+ KEY_TYPES[si]);
+ ASSERT(
+ test_verify_cose_sign1(
+ &cose_sign1, cdi1_pubkey + 1, cdi1_pubkey_size - 1, KEY_TYPES[ii]),
+ "CDI2 COSE (issf=%zu, iss=%d, sub=%d) verification against CDI1 failed",
+ issfi,
+ KEY_TYPES[ii],
+ KEY_TYPES[si]);
+ }
+ }
+ }
+ }
+ }
+
+ /* Verify ECA certificates against CDI2 keys */
+ for (size_t issfi = 0; issfi < NUM_CDI_FORMATS; issfi++) {
+ for (size_t ii2 = 0; ii2 < NUM_KEY_TYPES; ii2++) {
+ for (size_t ii = 0; ii < NUM_KEY_TYPES; ii++) {
+ /* Get CDI2 public key for this issuer key type.
+ * Use the issuer index ii as the subject index of the CDI2 matrix.
+ * The issuer index ii2 corresponds to the CDI2 issuer key type. */
+ if (!level1_artifacts.cdi2_valid[ii2][ii][issfi]) {
+ continue;
+ }
+ uint8_t cdi2_pubkey[97];
+ size_t cdi2_pubkey_size = sizeof(cdi2_pubkey);
+ if (issfi == 0) {
+ /* X.509 format: extract pubkey from cert */
+ ASSERT(test_extract_x509_pubkey(level1_artifacts.cdi2[ii2][ii][issfi].data,
+ level1_artifacts.cdi2[ii2][ii][issfi].size,
+ cdi2_pubkey,
+ &cdi2_pubkey_size),
+ "Failed to extract public key from CDI2 X.509 cert (iss=%d, sub=%d)",
+ KEY_TYPES[ii2],
+ KEY_TYPES[ii]);
+ } else {
+ /* COSE format: pubkey is the COSE_Sign1 payload */
+ n20_crypto_key_type_t got_key_type;
+ ASSERT(
+ test_extract_cose_pubkey(level1_artifacts.cdi2[ii2][ii][issfi].data,
+ level1_artifacts.cdi2[ii2][ii][issfi].size,
+ cdi2_pubkey,
+ &cdi2_pubkey_size,
+ &got_key_type),
+ "Failed to extract public key from CDI2 COSE_Sign1 cert (iss=%d, sub=%d)",
+ KEY_TYPES[ii2],
+ KEY_TYPES[ii]);
+ ASSERT_EQ(
+ got_key_type,
+ KEY_TYPES[ii],
+ "Unexpected key type extracted from CDI2 COSE_Sign1 cert (iss=%d, sub=%d)",
+ KEY_TYPES[ii2],
+ KEY_TYPES[ii]);
+ }
+
+ for (size_t si = 0; si < NUM_KEY_TYPES; si++) {
+ if (!level1_artifacts.eca_valid[ii][si]) {
+ continue;
+ }
+
+ /* ECA signed by CDI2's subject key (type = KEY_TYPES[ii]) */
+ ASSERT(test_verify_x509_signature(level1_artifacts.eca[ii][si].data,
+ level1_artifacts.eca[ii][si].size,
+ cdi2_pubkey,
+ cdi2_pubkey_size,
+ KEY_TYPES[ii]),
+ "ECA (cdi2.iss = %d, eca.iss=%d, eca.sub=%d) verification failed",
+ KEY_TYPES[ii2],
+ KEY_TYPES[ii],
+ KEY_TYPES[si]);
+ }
+ }
+ }
+ }
+ /* Verify ECA_EE certificates against ECA keys */
+ for (size_t ii2 = 0; ii2 < NUM_KEY_TYPES; ii2++) {
+ for (size_t ii = 0; ii < NUM_KEY_TYPES; ii++) {
+ /* Get ECA public key for this issuer key type.
+ * Use the issuer index ii as the subject index of the CDI2 matrix. */
+ if (!level1_artifacts.eca_valid[ii2][ii]) {
+ continue;
+ }
+ uint8_t eca_pubkey[97];
+ size_t eca_pubkey_size = sizeof(eca_pubkey);
+ ASSERT(test_extract_x509_pubkey(level1_artifacts.eca[ii2][ii].data,
+ level1_artifacts.eca[ii2][ii].size,
+ eca_pubkey,
+ &eca_pubkey_size),
+ "Failed to extract public key from ECA cert (iss=%d, sub=%d)",
+ KEY_TYPES[ii2],
+ KEY_TYPES[ii]);
+
+ for (size_t si = 0; si < NUM_KEY_TYPES; si++) {
+ if (!level1_artifacts.eca_ee_valid[ii][si]) {
+ continue;
+ }
+
+ /* ECA_EE signed by ECA's subject key (type = KEY_TYPES[ii]) */
+ ASSERT(test_verify_x509_signature(level1_artifacts.eca_ee[ii][si].data,
+ level1_artifacts.eca_ee[ii][si].size,
+ eca_pubkey,
+ eca_pubkey_size,
+ KEY_TYPES[ii]),
+ "ECA_EE (eca.iss = %d, eca.sub=%d, ee.sub=%d) verification failed",
+ KEY_TYPES[ii2],
+ KEY_TYPES[ii],
+ KEY_TYPES[si]);
+ }
+ }
+ }
+
+ /* Verify ECA_EE Signatures against ECA_EE keys */
+ for (size_t ii = 0; ii < NUM_KEY_TYPES; ii++) {
+ for (size_t si = 0; si < NUM_KEY_TYPES; si++) {
+ if (!level1_artifacts.eca_ee_valid[ii][si]) {
+ continue;
+ }
+ uint8_t eca_ee_pubkey[97];
+ size_t eca_ee_pubkey_size = sizeof(eca_ee_pubkey);
+ ASSERT(test_extract_x509_pubkey(level1_artifacts.eca_ee[ii][si].data,
+ level1_artifacts.eca_ee[ii][si].size,
+ eca_ee_pubkey,
+ &eca_ee_pubkey_size),
+ "Failed to extract public key from ECA_EE cert (iss=%d, sub=%d)",
+ KEY_TYPES[ii],
+ KEY_TYPES[si]);
+
+ if (!level1_artifacts.signature_valid[si]) {
+ continue;
+ }
+ /* Verify signature against ECA_EE key */
+ ASSERT(test_verify_raw_signature(eca_ee_pubkey + 1,
+ eca_ee_pubkey_size - 1,
+ test_message,
+ sizeof(test_message) - 1,
+ level1_artifacts.signature[si].data,
+ level1_artifacts.signature[si].size,
+ KEY_TYPES[si]),
+ "Signature verification failed (ee.iss=%d, ee.sub=%d)",
+ KEY_TYPES[ii],
+ KEY_TYPES[si]);
+ }
+ }
+
+ TEST_PASS();
+}
+
+/*
+ * This test is run after test_level1 and one promote step.
+ * At this point we are at CDI1 level. Generate:
+ * - CDI2: issuer_key_type × subject_key_type × format, parent_path = empty (depth 0)
+ * - ECA: issuer_key_type × subject_key_type, parent_path = depth 1
+ * - ECA_EE: eca_subject_key_type × ee_subject_key_type, parent_path = depth 1
+ * - Signature: ee_subject_key_type, parent_path = depth 1
+ *
+ * Compare all results to level1_artifacts. They must be identical.
+ */
+static void test_level2(void) {
+ TEST_BEGIN("Level 2: after promote, compare with level 1 artifacts");
+
+ n20_parent_path_t no_path = N20_MSG_PARENT_PATH_EMPTY;
+ n20_slice_t path_elements[1] = {
+ {.size = sizeof(compressed_input), .buffer = compressed_input},
+ };
+ n20_parent_path_t path_depth1 = {
+ .length = 1, .is_encoded = false, .decoded = &path_elements[0]};
+
+ /* CDI2: no parent path (we are now at CDI1 level) */
+ for (size_t ii = 0; ii < NUM_KEY_TYPES; ii++) {
+ for (size_t si = 0; si < NUM_KEY_TYPES; si++) {
+ for (size_t fi = 0; fi < NUM_CDI_FORMATS; fi++) {
+ cert_buffer_t cert;
+ bool ok =
+ issue_cdi_cert(KEY_TYPES[ii], KEY_TYPES[si], CDI_FORMATS[fi], no_path, &cert);
+ ASSERT(ok,
+ "Level 2 CDI2 (iss=%d, sub=%d, fmt=%d) failed",
+ KEY_TYPES[ii],
+ KEY_TYPES[si],
+ CDI_FORMATS[fi]);
+ ASSERT(level1_artifacts.cdi2_valid[ii][si][fi],
+ "Level 1 CDI2 (iss=%d, sub=%d, fmt=%d) was not valid",
+ KEY_TYPES[ii],
+ KEY_TYPES[si],
+ CDI_FORMATS[fi]);
+ ASSERT(
+ cert.size == level1_artifacts.cdi2[ii][si][fi].size &&
+ memcmp(cert.data, level1_artifacts.cdi2[ii][si][fi].data, cert.size) == 0,
+ "CDI2 mismatch (iss=%d, sub=%d, fmt=%d): level2 != level1",
+ KEY_TYPES[ii],
+ KEY_TYPES[si],
+ CDI_FORMATS[fi]);
+ }
+ }
+ }
+
+ /* ECA: parent_path depth 1 */
+ for (size_t ii = 0; ii < NUM_KEY_TYPES; ii++) {
+ for (size_t si = 0; si < NUM_KEY_TYPES; si++) {
+ cert_buffer_t cert;
+ bool ok = issue_eca_cert(
+ KEY_TYPES[ii], KEY_TYPES[si], n20_certificate_format_x509_e, path_depth1, &cert);
+ ASSERT(ok, "Level 2 ECA (iss=%d, sub=%d) failed", KEY_TYPES[ii], KEY_TYPES[si]);
+ ASSERT(level1_artifacts.eca_valid[ii][si],
+ "Level 1 ECA (iss=%d, sub=%d) was not valid",
+ KEY_TYPES[ii],
+ KEY_TYPES[si]);
+ ASSERT(cert.size == level1_artifacts.eca[ii][si].size &&
+ memcmp(cert.data, level1_artifacts.eca[ii][si].data, cert.size) == 0,
+ "ECA mismatch (iss=%d, sub=%d): level2 != level1",
+ KEY_TYPES[ii],
+ KEY_TYPES[si]);
+ }
+ }
+
+ /* ECA_EE: parent_path depth 1 */
+ for (size_t ei = 0; ei < NUM_KEY_TYPES; ei++) {
+ for (size_t si = 0; si < NUM_KEY_TYPES; si++) {
+ cert_buffer_t cert;
+ bool ok = issue_eca_ee_cert(
+ KEY_TYPES[ei], KEY_TYPES[si], n20_certificate_format_x509_e, path_depth1, &cert);
+ ASSERT(ok, "Level 2 ECA_EE (eca=%d, ee=%d) failed", KEY_TYPES[ei], KEY_TYPES[si]);
+ ASSERT(level1_artifacts.eca_ee_valid[ei][si],
+ "Level 1 ECA_EE (eca=%d, ee=%d) was not valid",
+ KEY_TYPES[ei],
+ KEY_TYPES[si]);
+ ASSERT(cert.size == level1_artifacts.eca_ee[ei][si].size &&
+ memcmp(cert.data, level1_artifacts.eca_ee[ei][si].data, cert.size) == 0,
+ "ECA_EE mismatch (eca=%d, ee=%d): level2 != level1",
+ KEY_TYPES[ei],
+ KEY_TYPES[si]);
+ }
+ }
+
+ /* Signature: parent_path depth 1 */
+ for (size_t si = 0; si < NUM_KEY_TYPES; si++) {
+ sig_buffer_t sig;
+ bool ok =
+ eca_ee_sign(KEY_TYPES[si], path_depth1, test_message, sizeof(test_message) - 1, &sig);
+ ASSERT(ok, "Level 2 signature (ee=%d) failed", KEY_TYPES[si]);
+ ASSERT(level1_artifacts.signature_valid[si],
+ "Level 1 signature (ee=%d) was not valid",
+ KEY_TYPES[si]);
+ ASSERT(sig.size == level1_artifacts.signature[si].size &&
+ memcmp(sig.data, level1_artifacts.signature[si].data, sig.size) == 0,
+ "Signature mismatch (ee=%d): level2 != level1",
+ KEY_TYPES[si]);
+ }
+
+ TEST_PASS();
+}
+
+/*
+ * This test is run after test_level2 and one promote step.
+ * At this point we are at CDI2 level. Generate:
+ * - ECA: issuer_key_type × subject_key_type, parent_path = empty (depth 0)
+ * - ECA_EE: eca_subject_key_type × ee_subject_key_type, parent_path = empty
+ * - Signature: ee_subject_key_type, parent_path = empty
+ *
+ * Compare all results to level1_artifacts. They must be identical.
+ */
+static void test_level3(void) {
+ TEST_BEGIN("Level 3: after second promote, compare with level 1 artifacts");
+
+ n20_parent_path_t no_path = N20_MSG_PARENT_PATH_EMPTY;
+
+ /* ECA: no parent path */
+ for (size_t ii = 0; ii < NUM_KEY_TYPES; ii++) {
+ for (size_t si = 0; si < NUM_KEY_TYPES; si++) {
+ cert_buffer_t cert;
+ bool ok = issue_eca_cert(
+ KEY_TYPES[ii], KEY_TYPES[si], n20_certificate_format_x509_e, no_path, &cert);
+ ASSERT(ok, "Level 3 ECA (iss=%d, sub=%d) failed", KEY_TYPES[ii], KEY_TYPES[si]);
+ ASSERT(level1_artifacts.eca_valid[ii][si],
+ "Level 1 ECA (iss=%d, sub=%d) was not valid",
+ KEY_TYPES[ii],
+ KEY_TYPES[si]);
+ ASSERT(cert.size == level1_artifacts.eca[ii][si].size &&
+ memcmp(cert.data, level1_artifacts.eca[ii][si].data, cert.size) == 0,
+ "ECA mismatch (iss=%d, sub=%d): level3 != level1",
+ KEY_TYPES[ii],
+ KEY_TYPES[si]);
+ }
+ }
+
+ /* ECA_EE: no parent path */
+ for (size_t ei = 0; ei < NUM_KEY_TYPES; ei++) {
+ for (size_t si = 0; si < NUM_KEY_TYPES; si++) {
+ cert_buffer_t cert;
+ bool ok = issue_eca_ee_cert(
+ KEY_TYPES[ei], KEY_TYPES[si], n20_certificate_format_x509_e, no_path, &cert);
+ ASSERT(ok, "Level 3 ECA_EE (eca=%d, ee=%d) failed", KEY_TYPES[ei], KEY_TYPES[si]);
+ ASSERT(level1_artifacts.eca_ee_valid[ei][si],
+ "Level 1 ECA_EE (eca=%d, ee=%d) was not valid",
+ KEY_TYPES[ei],
+ KEY_TYPES[si]);
+ ASSERT(cert.size == level1_artifacts.eca_ee[ei][si].size &&
+ memcmp(cert.data, level1_artifacts.eca_ee[ei][si].data, cert.size) == 0,
+ "ECA_EE mismatch (eca=%d, ee=%d): level3 != level1",
+ KEY_TYPES[ei],
+ KEY_TYPES[si]);
+ }
+ }
+
+ /* Signature: no parent path */
+ for (size_t si = 0; si < NUM_KEY_TYPES; si++) {
+ sig_buffer_t sig;
+ bool ok = eca_ee_sign(KEY_TYPES[si], no_path, test_message, sizeof(test_message) - 1, &sig);
+ ASSERT(ok, "Level 3 signature (ee=%d) failed", KEY_TYPES[si]);
+ ASSERT(level1_artifacts.signature_valid[si],
+ "Level 1 signature (ee=%d) was not valid",
+ KEY_TYPES[si]);
+ ASSERT(sig.size == level1_artifacts.signature[si].size &&
+ memcmp(sig.data, level1_artifacts.signature[si].data, sig.size) == 0,
+ "Signature mismatch (ee=%d): level3 != level1",
+ KEY_TYPES[si]);
+ }
+
+ TEST_PASS();
+}
+
+static void test_promote_1_to_2(void) {
+ TEST_BEGIN("Promote from level 1 to level 2");
+ ASSERT(do_promote(compressed_input, sizeof(compressed_input)), "Promote failed");
+ TEST_PASS();
+}
+
+static void test_promote_2_to_3(void) {
+ TEST_BEGIN("Promote from level 2 to level 3");
+ ASSERT(do_promote(compressed_input, sizeof(compressed_input)), "Promote failed");
+ TEST_PASS();
+}
+
+int main(void) {
+ printf("nat20 integration test suite\n");
+ printf("============================\n\n");
+
+ test_dice_chain_readable();
+ test_cdi_cert_x509_p256();
+#if N20_WITH_COSE == 1
+ test_cdi_cert_cose_p256();
+#endif
+ test_eca_cert_x509_p256();
+ test_eca_ee_cert_x509_p256();
+ test_eca_ee_sign_p256();
+
+ /* Full parameterized chain test (promote is irreversible — runs once) */
+ test_level1();
+ test_promote_1_to_2();
+ test_level2();
+ test_promote_2_to_3();
+ test_level3();
+
+ printf("\n============================\n");
+ printf("Results: %d passed, %d failed, %d total\n", tests_passed, tests_failed, tests_run);
+
+ return tests_failed > 0 ? EXIT_FAILURE : EXIT_SUCCESS;
+}
diff --git a/examples/linux/nat20test/test/test_helpers.c b/examples/linux/nat20test/test/test_helpers.c
new file mode 100644
index 0000000..dfb5d3e
--- /dev/null
+++ b/examples/linux/nat20test/test/test_helpers.c
@@ -0,0 +1,535 @@
+/*
+ * Copyright 2026 Aurora Operations, Inc.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR GPL-2.0
+ *
+ * This work is dual licensed.
+ * You may use it under Apache-2.0 or GPL-2.0 at your option.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * OR
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see
+ * .
+ */
+
+#include "test_helpers.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+n20_error_t test_compress_cdi_input(uint8_t const* code_hash,
+ size_t code_hash_size,
+ uint8_t const* config_hash,
+ size_t config_hash_size,
+ uint8_t const* authority_hash,
+ size_t authority_hash_size,
+ uint8_t mode,
+ uint8_t const* hidden,
+ size_t hidden_size,
+ uint8_t* compressed_out,
+ size_t compressed_out_size) {
+ n20_crypto_digest_context_t* digest_ctx = NULL;
+ n20_error_t err = n20_crypto_nat20_open(&digest_ctx);
+ if (err != n20_error_ok_e) {
+ return err;
+ }
+
+ n20_open_dice_cert_info_t cert_info = {0};
+ cert_info.cert_type = n20_cert_type_cdi_e;
+ cert_info.open_dice_input.code_hash =
+ (n20_slice_t){.size = code_hash_size, .buffer = code_hash};
+ cert_info.open_dice_input.configuration_hash =
+ (n20_slice_t){.size = config_hash_size, .buffer = config_hash};
+ cert_info.open_dice_input.authority_hash =
+ (n20_slice_t){.size = authority_hash_size, .buffer = authority_hash};
+ cert_info.open_dice_input.mode = (n20_open_dice_modes_t)mode;
+ cert_info.open_dice_input.hidden = (n20_slice_t){.size = hidden_size, .buffer = hidden};
+
+ if (compressed_out_size < N20_FUNC_COMPRESSED_INPUT_SIZE) {
+ n20_crypto_nat20_close(digest_ctx);
+ return n20_error_insufficient_buffer_size_e;
+ }
+
+ err = n20_compress_input(digest_ctx, &cert_info, compressed_out);
+ n20_crypto_nat20_close(digest_ctx);
+ return err;
+}
+
+static EVP_PKEY* evp_pkey_from_ec_pubkey(uint8_t const* pubkey,
+ size_t pubkey_size,
+ n20_crypto_key_type_t key_type) {
+ char const* group_name =
+ (key_type == n20_crypto_key_type_secp256r1_e) ? SN_X9_62_prime256v1 : SN_secp384r1;
+
+ OSSL_PARAM_BLD* bld = OSSL_PARAM_BLD_new();
+ if (bld == NULL) {
+ return NULL;
+ }
+
+ OSSL_PARAM_BLD_push_utf8_string(bld, OSSL_PKEY_PARAM_GROUP_NAME, group_name, 0);
+ OSSL_PARAM_BLD_push_octet_string(bld, OSSL_PKEY_PARAM_PUB_KEY, pubkey, pubkey_size);
+
+ OSSL_PARAM* params = OSSL_PARAM_BLD_to_param(bld);
+ OSSL_PARAM_BLD_free(bld);
+ if (params == NULL) {
+ return NULL;
+ }
+
+ EVP_PKEY_CTX* pctx = EVP_PKEY_CTX_new_from_name(NULL, "EC", NULL);
+ if (pctx == NULL) {
+ OSSL_PARAM_free(params);
+ return NULL;
+ }
+
+ EVP_PKEY* pkey = NULL;
+ if (EVP_PKEY_fromdata_init(pctx) != 1 ||
+ EVP_PKEY_fromdata(pctx, &pkey, EVP_PKEY_PUBLIC_KEY, params) != 1) {
+ pkey = NULL;
+ }
+
+ EVP_PKEY_CTX_free(pctx);
+ OSSL_PARAM_free(params);
+ return pkey;
+}
+
+static EVP_PKEY* evp_pkey_from_pubkey(uint8_t const* pubkey,
+ size_t pubkey_size,
+ n20_crypto_key_type_t key_type) {
+ switch (key_type) {
+ case n20_crypto_key_type_ed25519_e:
+ return EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL, pubkey, pubkey_size);
+ case n20_crypto_key_type_secp256r1_e:
+ case n20_crypto_key_type_secp384r1_e:
+ return evp_pkey_from_ec_pubkey(pubkey, pubkey_size, key_type);
+ default:
+ return NULL;
+ }
+}
+
+bool test_verify_x509_signature(uint8_t const* cert_der,
+ size_t cert_der_size,
+ uint8_t const* issuer_pubkey,
+ size_t issuer_pubkey_size,
+ n20_crypto_key_type_t key_type) {
+ uint8_t const* p = cert_der;
+ X509* cert = d2i_X509(NULL, &p, (long)cert_der_size);
+ if (cert == NULL) {
+ fprintf(stderr, " d2i_X509 failed\n");
+ return false;
+ }
+
+ EVP_PKEY* pkey = evp_pkey_from_pubkey(issuer_pubkey, issuer_pubkey_size, key_type);
+ if (pkey == NULL) {
+ fprintf(stderr, " Failed to construct EVP_PKEY\n");
+ X509_free(cert);
+ return false;
+ }
+
+ bool result = X509_verify(cert, pkey) == 1;
+ if (!result) {
+ fprintf(stderr, " X509_verify failed\n");
+ }
+
+ EVP_PKEY_free(pkey);
+ X509_free(cert);
+ return result;
+}
+
+bool test_extract_x509_pubkey(uint8_t const* cert_der,
+ size_t cert_der_size,
+ uint8_t* pubkey_out,
+ size_t* pubkey_size_in_out) {
+ uint8_t const* p = cert_der;
+ X509* cert = d2i_X509(NULL, &p, (long)cert_der_size);
+ if (cert == NULL) {
+ fprintf(stderr, " d2i_X509 failed\n");
+ return false;
+ }
+
+ EVP_PKEY* pkey = X509_get_pubkey(cert);
+ if (pkey == NULL) {
+ fprintf(stderr, " X509_get_pubkey failed\n");
+ X509_free(cert);
+ return false;
+ }
+
+ bool result = false;
+ int key_id = EVP_PKEY_id(pkey);
+
+ if (key_id == EVP_PKEY_ED25519) {
+ size_t len = *pubkey_size_in_out;
+ if (EVP_PKEY_get_raw_public_key(pkey, pubkey_out, &len) == 1) {
+ *pubkey_size_in_out = len;
+ result = true;
+ }
+ } else if (key_id == EVP_PKEY_EC) {
+ size_t len = 0;
+ if (EVP_PKEY_get_octet_string_param(pkey, OSSL_PKEY_PARAM_PUB_KEY, NULL, 0, &len) == 1 &&
+ len <= *pubkey_size_in_out) {
+ if (EVP_PKEY_get_octet_string_param(
+ pkey, OSSL_PKEY_PARAM_PUB_KEY, pubkey_out, *pubkey_size_in_out, &len) == 1) {
+ *pubkey_size_in_out = len;
+ result = true;
+ }
+ }
+ }
+
+ EVP_PKEY_free(pkey);
+ X509_free(cert);
+ return result;
+}
+
+bool test_parse_cose_sign1(uint8_t const* data, size_t size, test_cose_sign1_t* out) {
+ n20_istream_t stream;
+ n20_istream_init(&stream, data, size);
+
+ n20_cbor_type_t type;
+ uint64_t value;
+
+ if (!n20_cbor_read_header(&stream, &type, &value)) return false;
+ if (type != n20_cbor_type_array_e || value != 4) return false;
+
+ /* Element 1: protected header (bstr) */
+ if (!n20_cbor_read_header(&stream, &type, &value)) return false;
+ if (type != n20_cbor_type_bytes_e) return false;
+ if (!n20_istream_get_slice(&stream, &out->protected_header, value)) return false;
+
+ /* Element 2: unprotected header (map) — skip */
+ if (!n20_cbor_read_skip_item(&stream)) return false;
+
+ /* Element 3: payload (bstr or nil) */
+ if (!n20_cbor_read_header(&stream, &type, &value)) return false;
+ if (type == n20_cbor_type_bytes_e) {
+ if (!n20_istream_get_slice(&stream, &out->payload, value)) return false;
+ } else if (type == n20_cbor_type_simple_float_e && value == 22) {
+ /* nil payload */
+ out->payload = (n20_slice_t){.size = 0, .buffer = NULL};
+ } else {
+ return false;
+ }
+
+ /* Element 4: signature (bstr) */
+ if (!n20_cbor_read_header(&stream, &type, &value)) return false;
+ if (type != n20_cbor_type_bytes_e) return false;
+ if (!n20_istream_get_slice(&stream, &out->signature, value)) return false;
+
+ return true;
+}
+
+bool test_verify_raw_signature(uint8_t const* pubkey,
+ size_t pubkey_size,
+ uint8_t const* message,
+ size_t message_size,
+ uint8_t const* sig,
+ size_t sig_size,
+ n20_crypto_key_type_t key_type) {
+ EVP_PKEY* pkey = NULL;
+
+ switch (key_type) {
+ case n20_crypto_key_type_ed25519_e:
+ pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL, pubkey, pubkey_size);
+ break;
+ case n20_crypto_key_type_secp256r1_e:
+ case n20_crypto_key_type_secp384r1_e: {
+ /* The pubkey is raw x||y — wrap with 0x04 uncompressed prefix */
+ uint8_t uncompressed[1 + 96];
+ uncompressed[0] = 0x04;
+ if (pubkey_size > sizeof(uncompressed) - 1) {
+ fprintf(stderr, " Public key size too large for uncompressed format\n");
+ return false;
+ }
+ memcpy(uncompressed + 1, pubkey, pubkey_size);
+ pkey = evp_pkey_from_ec_pubkey(uncompressed, 1 + pubkey_size, key_type);
+ break;
+ }
+ default:
+ return false;
+ }
+
+ if (pkey == NULL) return false;
+
+ bool result = false;
+
+ if (key_type == n20_crypto_key_type_ed25519_e) {
+ EVP_MD_CTX* md_ctx = EVP_MD_CTX_new();
+ if (md_ctx != NULL) {
+ if (EVP_DigestVerifyInit(md_ctx, NULL, NULL, NULL, pkey) == 1 &&
+ EVP_DigestVerify(md_ctx, sig, sig_size, message, message_size) == 1) {
+ result = true;
+ }
+ EVP_MD_CTX_free(md_ctx);
+ }
+ } else {
+ /* ECDSA: convert raw r||s to DER ECDSA_SIG for OpenSSL */
+ size_t coord_size = sig_size / 2;
+ BIGNUM* r_bn = BN_bin2bn(sig, (int)coord_size, NULL);
+ BIGNUM* s_bn = BN_bin2bn(sig + coord_size, (int)coord_size, NULL);
+ ECDSA_SIG* ecdsa_sig = ECDSA_SIG_new();
+ if (r_bn && s_bn && ecdsa_sig && ECDSA_SIG_set0(ecdsa_sig, r_bn, s_bn)) {
+ /* DER-encode the signature */
+ uint8_t* der_sig = NULL;
+ int der_sig_len = i2d_ECDSA_SIG(ecdsa_sig, &der_sig);
+ if (der_sig_len > 0 && der_sig != NULL) {
+ EVP_MD const* md =
+ (key_type == n20_crypto_key_type_secp256r1_e) ? EVP_sha256() : EVP_sha384();
+ EVP_MD_CTX* md_ctx = EVP_MD_CTX_new();
+ if (md_ctx != NULL) {
+ if (EVP_DigestVerifyInit(md_ctx, NULL, md, NULL, pkey) == 1 &&
+ EVP_DigestVerifyUpdate(md_ctx, message, message_size) == 1 &&
+ EVP_DigestVerifyFinal(md_ctx, der_sig, (size_t)der_sig_len) == 1) {
+ result = true;
+ }
+ EVP_MD_CTX_free(md_ctx);
+ }
+ OPENSSL_free(der_sig);
+ }
+ /* r_bn and s_bn are owned by ecdsa_sig after ECDSA_SIG_set0 */
+ } else {
+ BN_free(r_bn);
+ BN_free(s_bn);
+ }
+ ECDSA_SIG_free(ecdsa_sig);
+ }
+
+ EVP_PKEY_free(pkey);
+ return result;
+}
+
+bool test_verify_cose_sign1(test_cose_sign1_t const* sign1,
+ uint8_t const* issuer_pubkey,
+ size_t issuer_pubkey_size,
+ n20_crypto_key_type_t key_type) {
+ /* Reconstruct the Sig_structure1:
+ * ["Signature1", protected, external_aad, payload]
+ * Encoded as: 84 6a "Signature1" 40
+ *
+ * The to-be-signed data is the concatenation of:
+ * [0] = array(4) header + "Signature1" text string
+ * [1] = protected header as bstr (with its CBOR bstr wrapper)
+ * [2] = empty bstr (0x40)
+ * [3] = payload as bstr (with its CBOR bstr wrapper)
+ */
+ uint8_t sig_struct_buf[2048];
+ n20_stream_t s;
+ n20_stream_init(&s, sig_struct_buf, sizeof(sig_struct_buf));
+
+ /* Write in reverse (right-to-left stream) */
+ /* [3] payload as bstr */
+ n20_cbor_write_byte_string(&s, sign1->payload);
+ /* [2] empty external_aad */
+ n20_cbor_write_byte_string(&s, (n20_slice_t){.size = 0, .buffer = NULL});
+ /* [1] protected header as bstr */
+ n20_cbor_write_byte_string(&s, sign1->protected_header);
+ /* [0] context string "Signature1" */
+ n20_stream_prepend(&s, (uint8_t const*)"\x6aSignature1", 11);
+ /* Array header for 4 elements */
+ n20_cbor_write_array_header(&s, 4);
+
+ if (n20_stream_has_buffer_overflow(&s)) {
+ fprintf(stderr, " Sig_structure1 buffer overflow\n");
+ return false;
+ }
+
+ size_t tbs_size = n20_stream_byte_count(&s);
+ uint8_t const* tbs_data = sig_struct_buf + (sizeof(sig_struct_buf) - tbs_size);
+
+ return test_verify_raw_signature(issuer_pubkey,
+ issuer_pubkey_size,
+ tbs_data,
+ tbs_size,
+ sign1->signature.buffer,
+ sign1->signature.size,
+ key_type);
+}
+
+/* COSE_Key label constants (matching cose.c) */
+#define COSE_KEY_LABEL_KEY_TYPE (1)
+#define COSE_KEY_LABEL_ALGORITHM_ID (3)
+#define COSE_KEY_LABEL_CURVE (-1)
+#define COSE_KEY_LABEL_X_COORDINATE (-2)
+#define COSE_KEY_LABEL_Y_COORDINATE (-3)
+
+#define COSE_KEY_TYPE_OKP (1)
+#define COSE_KEY_TYPE_EC2 (2)
+
+#define COSE_CURVE_P256 (1)
+#define COSE_CURVE_P384 (2)
+#define COSE_CURVE_ED25519 (6)
+
+/* CWT claim label for the subject public key */
+#define CWT_LABEL_SUBJECT_PUBLIC_KEY (-4670552)
+
+static int64_t cbor_read_int(n20_istream_t* s) {
+ n20_cbor_type_t type;
+ uint64_t value;
+ if (!n20_cbor_read_header(s, &type, &value)) return 0;
+ if (type == n20_cbor_type_uint_e) return (int64_t)value;
+ if (type == n20_cbor_type_nint_e) return -1 - (int64_t)value;
+ return 0;
+}
+
+bool test_extract_cose_pubkey(uint8_t const* cose_sign1,
+ size_t cose_sign1_size,
+ uint8_t* pubkey_out,
+ size_t* pubkey_size_in_out,
+ n20_crypto_key_type_t* key_type_out) {
+ /* Parse the COSE_Sign1 to get the payload */
+ test_cose_sign1_t sign1;
+ if (!test_parse_cose_sign1(cose_sign1, cose_sign1_size, &sign1)) {
+ fprintf(stderr, " Failed to parse COSE_Sign1\n");
+ return false;
+ }
+
+ if (sign1.payload.buffer == NULL || sign1.payload.size == 0) {
+ fprintf(stderr, " COSE_Sign1 has no payload\n");
+ return false;
+ }
+
+ /* The payload is a CWT (CBOR map). Find the subject public key claim. */
+ n20_istream_t cwt;
+ n20_istream_init(&cwt, sign1.payload.buffer, sign1.payload.size);
+
+ n20_cbor_type_t type;
+ uint64_t value;
+ if (!n20_cbor_read_header(&cwt, &type, &value) || type != n20_cbor_type_map_e) {
+ fprintf(stderr, " CWT payload is not a map\n");
+ return false;
+ }
+ uint64_t map_count = value;
+
+ /* Iterate through the CWT map to find the subject public key */
+ bool found_pubkey = false;
+ for (uint64_t i = 0; i < map_count; i++) {
+ int64_t label = cbor_read_int(&cwt);
+ if (label == CWT_LABEL_SUBJECT_PUBLIC_KEY) {
+ found_pubkey = true;
+ break;
+ } else {
+ if (!n20_cbor_read_skip_item(&cwt)) {
+ fprintf(stderr, " Failed to skip CWT claim value\n");
+ return false;
+ }
+ }
+ }
+
+ if (!found_pubkey) {
+ fprintf(stderr, " Subject public key claim not found in CWT\n");
+ return false;
+ }
+
+ if (!n20_cbor_read_header(&cwt, &type, &value) || type != n20_cbor_type_bytes_e) {
+ fprintf(stderr, " COSE_Key is not a byte string\n");
+ return false;
+ }
+
+ if (!n20_cbor_read_header(&cwt, &type, &value) || type != n20_cbor_type_map_e) {
+ fprintf(stderr, " COSE_Key is not a map\n");
+ return false;
+ }
+ uint64_t key_pairs = value;
+
+ n20_slice_t x = {0};
+ n20_slice_t y = {0};
+ int64_t key_type_val = 0;
+ int64_t crv = 0;
+
+ for (uint64_t i = 0; i < key_pairs; i++) {
+ int64_t key_label = cbor_read_int(&cwt);
+ switch (key_label) {
+ case COSE_KEY_LABEL_KEY_TYPE:
+ key_type_val = cbor_read_int(&cwt);
+ break;
+ case COSE_KEY_LABEL_CURVE:
+ crv = cbor_read_int(&cwt);
+ break;
+ case COSE_KEY_LABEL_X_COORDINATE:
+ if (!n20_cbor_read_header(&cwt, &type, &value) || type != n20_cbor_type_bytes_e) {
+ return false;
+ }
+ if (!n20_istream_get_slice(&cwt, &x, value)) return false;
+ break;
+ case COSE_KEY_LABEL_Y_COORDINATE:
+ if (!n20_cbor_read_header(&cwt, &type, &value) || type != n20_cbor_type_bytes_e) {
+ return false;
+ }
+ if (!n20_istream_get_slice(&cwt, &y, value)) return false;
+ break;
+ default:
+ if (!n20_cbor_read_skip_item(&cwt)) return false;
+ break;
+ }
+ }
+
+ /* Reconstruct the raw public key based on key type */
+ if (key_type_val == COSE_KEY_TYPE_EC2) {
+ /* EC2: output is x || y */
+ if (x.size == 0 || y.size == 0) {
+ fprintf(stderr, " EC2 key missing x or y coordinate\n");
+ return false;
+ }
+ size_t total = x.size + y.size + 1;
+ if (total > *pubkey_size_in_out) return false;
+ pubkey_out[0] = 0x04; /* Uncompressed point prefix */
+ memcpy(pubkey_out + 1, x.buffer, x.size);
+ memcpy(pubkey_out + 1 + x.size, y.buffer, y.size);
+ *pubkey_size_in_out = total;
+
+ if (crv == COSE_CURVE_P256) {
+ *key_type_out = n20_crypto_key_type_secp256r1_e;
+ } else if (crv == COSE_CURVE_P384) {
+ *key_type_out = n20_crypto_key_type_secp384r1_e;
+ } else {
+ fprintf(stderr, " Unknown EC2 curve: %lld\n", (long long)crv);
+ return false;
+ }
+ } else if (key_type_val == COSE_KEY_TYPE_OKP) {
+ /* OKP (Ed25519): output is x only */
+ if (x.size == 0) {
+ fprintf(stderr, " OKP key missing x coordinate\n");
+ return false;
+ }
+ if (x.size > *pubkey_size_in_out) return false;
+ memcpy(pubkey_out, x.buffer, x.size);
+ *pubkey_size_in_out = x.size;
+ *key_type_out = n20_crypto_key_type_ed25519_e;
+ } else {
+ fprintf(stderr, " Unknown COSE key type: %lld\n", (long long)key_type_val);
+ return false;
+ }
+
+ return true;
+}
diff --git a/examples/linux/nat20test/test/test_helpers.h b/examples/linux/nat20test/test/test_helpers.h
new file mode 100644
index 0000000..f36f60c
--- /dev/null
+++ b/examples/linux/nat20test/test/test_helpers.h
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2026 Aurora Operations, Inc.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR GPL-2.0
+ *
+ * This work is dual licensed.
+ * You may use it under Apache-2.0 or GPL-2.0 at your option.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * OR
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see
+ * .
+ */
+
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+/**
+ * Compute the compressed input for a CDI level given the OpenDICE parameters.
+ * Uses the libnat20 software digest implementation.
+ */
+n20_error_t test_compress_cdi_input(uint8_t const* code_hash,
+ size_t code_hash_size,
+ uint8_t const* config_hash,
+ size_t config_hash_size,
+ uint8_t const* authority_hash,
+ size_t authority_hash_size,
+ uint8_t mode,
+ uint8_t const* hidden,
+ size_t hidden_size,
+ uint8_t* compressed_out,
+ size_t compressed_out_size);
+
+/**
+ * Verify an X.509 DER certificate's signature against an issuer public key.
+ * For self-signed certs, pass the cert's own public key.
+ *
+ * @param cert_der DER-encoded certificate
+ * @param cert_der_size Size of the certificate
+ * @param issuer_pubkey Raw public key (0x04||x||y for EC, compressed for Ed25519)
+ * @param issuer_pubkey_size Size of the public key
+ * @param key_type Key type of the issuer
+ * @return true if signature is valid
+ */
+bool test_verify_x509_signature(uint8_t const* cert_der,
+ size_t cert_der_size,
+ uint8_t const* issuer_pubkey,
+ size_t issuer_pubkey_size,
+ n20_crypto_key_type_t key_type);
+
+/**
+ * Extract the subject public key from a DER-encoded X.509 certificate.
+ * For EC keys, the output is the uncompressed point (0x04 || x || y).
+ * For Ed25519, it's the 32-byte compressed point.
+ *
+ * @param cert_der DER-encoded certificate
+ * @param cert_der_size Size of the certificate
+ * @param pubkey_out Output buffer for the public key
+ * @param pubkey_size_in_out In: buffer size, Out: bytes written
+ * @return true on success
+ */
+bool test_extract_x509_pubkey(uint8_t const* cert_der,
+ size_t cert_der_size,
+ uint8_t* pubkey_out,
+ size_t* pubkey_size_in_out);
+
+/**
+ * Parsed COSE_Sign1 structure.
+ */
+typedef struct {
+ n20_slice_t protected_header;
+ n20_slice_t payload;
+ n20_slice_t signature;
+} test_cose_sign1_t;
+
+/**
+ * Parse a COSE_Sign1 structure from CBOR-encoded bytes.
+ * The returned slices point into the input buffer.
+ */
+bool test_parse_cose_sign1(uint8_t const* data, size_t size, test_cose_sign1_t* out);
+
+/**
+ * Verify a COSE_Sign1 signature.
+ * Reconstructs the Sig_structure1 and verifies against the given public key.
+ *
+ * @param sign1 Parsed COSE_Sign1 (from test_parse_cose_sign1)
+ * @param issuer_pubkey Raw public key bytes (without 0x04 prefix for EC)
+ * @param issuer_pubkey_size Size of the public key
+ * @param key_type Key type of the issuer
+ * @return true if signature is valid
+ */
+bool test_verify_cose_sign1(test_cose_sign1_t const* sign1,
+ uint8_t const* issuer_pubkey,
+ size_t issuer_pubkey_size,
+ n20_crypto_key_type_t key_type);
+
+/**
+ * Verify a raw ECDSA/EdDSA signature over a message.
+ *
+ * @param pubkey Raw public key (x||y for EC, 32 bytes for Ed25519)
+ * @param pubkey_size Size of the public key
+ * @param message Message that was signed
+ * @param message_size Size of the message
+ * @param sig Signature (r||s for ECDSA, 64 bytes for Ed25519)
+ * @param sig_size Size of the signature
+ * @param key_type Key type
+ * @return true if signature is valid
+ */
+bool test_verify_raw_signature(uint8_t const* pubkey,
+ size_t pubkey_size,
+ uint8_t const* message,
+ size_t message_size,
+ uint8_t const* sig,
+ size_t sig_size,
+ n20_crypto_key_type_t key_type);
+
+/**
+ * Extract the subject public key from a COSE_Sign1 certificate.
+ *
+ * Assumes the COSE_Sign1 payload is a CWT containing a claim with label
+ * -4670552 (N20_OPEN_DICE_CWT_LABEL_SUBJECT_PUBLIC_KEY) whose value is
+ * a COSE_Key map. Extracts the x (and y for EC2) coordinates and writes
+ * them as raw 0x04||x||y (for EC) or raw 32-byte key (for OKP/Ed25519).
+ *
+ * @param cose_sign1 COSE_Sign1 encoded certificate bytes
+ * @param cose_sign1_size Size of the COSE_Sign1 data
+ * @param pubkey_out Output buffer for the raw public key
+ * @param pubkey_size_in_out In: buffer size, Out: bytes written
+ * @param key_type_out Output: detected key type
+ * @return true on success
+ */
+bool test_extract_cose_pubkey(uint8_t const* cose_sign1,
+ size_t cose_sign1_size,
+ uint8_t* pubkey_out,
+ size_t* pubkey_size_in_out,
+ n20_crypto_key_type_t* key_type_out);