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);