diff --git a/.github/license-check/license-config.json b/.github/license-check/license-config.json index f05119d..fbb042e 100644 --- a/.github/license-check/license-config.json +++ b/.github/license-check/license-config.json @@ -10,6 +10,7 @@ "**/Kbuild", "examples/linux/br_external/external.desc", "examples/linux/**/Makefile", + "examples/linux/nat20cli/openssl_dice.cnf", ".clang-format", ".gitignore" ], diff --git a/.github/workflows/linux-kmod-build.yml b/.github/workflows/linux-kmod-build.yml index dda417f..92286c0 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 @@ -138,3 +139,93 @@ jobs: find ${{ runner.temp }}/buildroot.build -name 'nat20sw.ko' | grep -q nat20sw.ko echo "nat20sw.ko built successfully:" find ${{ runner.temp }}/buildroot.build -name 'nat20sw.ko' -exec ls -la {} \; + + - name: Build libnat20 userspace library + env: + LIBNAT20_OVERRIDE_SRCDIR: ${{ github.workspace }} + run: | + cd ${{ runner.temp }}/buildroot.build/buildroot + make libnat20-dirclean + make libnat20 -j $(( $(nproc) + 1 )) + + - name: Verify libnat20 was produced + run: | + 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 nat20cli userspace cli tool + env: + LIBNAT20_OVERRIDE_SRCDIR: ${{ github.workspace }} + NAT20CLI_OVERRIDE_SRCDIR: ${{ github.workspace }} + run: | + cd ${{ runner.temp }}/buildroot.build/buildroot + make nat20cli-dirclean + make nat20cli -j $(( $(nproc) + 1 )) + + - name: Verify nat20cli was produced + run: | + find ${{ runner.temp }}/buildroot.build -name 'nat20cli' | grep -q nat20cli + echo "nat20cli built successfully:" + find ${{ runner.temp }}/buildroot.build -name 'nat20cli' -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 }} + NAT20CLI_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 + + - name: Run nat20cli 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/nat20cli_qemu_init.sh" \ + -nographic \ + -no-reboot \ + -net none \ + 2>&1 | tee qemu_output.log + + if grep -q "NAT20CLI_TESTS_PASSED" qemu_output.log; then + echo "NAT20CLI tests passed." + else + echo "NAT20CLI 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/README.md b/README.md index e2c152e..dd95cca 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,15 @@ make nat20_docs Open the documentation by pointing your browser to `html/index.html` in your build directory. +## Examples + +### Linux DICE kernel modules + +The `examples/linux/` directory contains a complete Linux kernel module +implementation of a software DICE node, including a userspace CLI tool and a +Buildroot-based build environment targeting QEMU. See +[examples/linux/README.md](examples/linux/README.md) for details. + ## API Reference The API references is generated from the main branch using doxygen and deployed diff --git a/examples/linux/README.md b/examples/linux/README.md new file mode 100644 index 0000000..4036377 --- /dev/null +++ b/examples/linux/README.md @@ -0,0 +1,132 @@ +# Linux DICE Example + +This directory contains a set of Linux kernel modules and a userspace CLI tool +that together implement a software DICE node using libnat20. + +## Architecture + +The example is structured as four kernel modules with the following dependency +chain: + +``` +nat20sw ──► nat20crypto ──► nat20lib + nat20device ──► +``` + +### nat20lib + +A kernel module wrapper around the core libnat20 C library. It compiles all +core library sources (CBOR, COSE, CWT, X.509, HKDF, service dispatch) into a +single `.ko` and re-exports their symbols via `EXPORT_SYMBOL()` so that other +kernel modules can use them. + +### nat20crypto + +Implements the libnat20 crypto interface (`n20_crypto_context_t`) using the +Linux kernel's crypto API. Provides ECDSA signing (P-256, P-384), HKDF key +derivation, and SHA-2 hashing — all in kernel space. + +### nat20device + +A generic character device framework for DICE services. On load it allocates +the `nat20` device class. Backend modules (like `nat20sw`) register via +`nat20device_register_driver()`, which creates: + +- `/dev/nat20` — a character device for DICE service requests (write a CBOR + request, read the CBOR response). +- `/sys/kernel/security/nat20/dice_chain` — a read-only securityfs file + exposing the boot-time DICE certificate chain. + +### nat20sw + +A concrete software DICE node that wires `nat20lib`, `nat20crypto`, and +`nat20device` together. On load it derives a UDS from a hardcoded passphrase +(for demonstration purposes only), issues a self-signed X.509 certificate, and +registers itself with the `nat20device` framework. + +### nat20cli + +A userspace command-line tool that communicates with the DICE service via +`/dev/nat200`. It can issue CDI certificates, ECA certificates, and advance +the DICE layer via the `promote` command. + +## The `dice_chain` securityfs interface + +Each registered nat20device driver instance exposes a `dice_chain` file at +`/sys/kernel/security/nat20/dice_chain`. This file contains the DICE +certificate chain constructed during bootup, encoded as a CBOR +indefinite-length array. + +### Encoding + +The file contains a single CBOR indefinite-length array (`0x9f ... 0xff`). +Each element in the array is one of the following CBOR-tagged values: + +| CBOR tag | Content | Description | +|----------|---------|-------------| +| `#6.80150(bstr)` | DER-encoded X.509 certificate | An X.509 certificate, typically with the Open DICE Input extension (OID `1.3.6.1.4.1.11129.2.1.24`). | +| `#6.18(COSE_Sign1)` | COSE_Sign1 with CWT payload | A COSE certificate as specified by the Open DICE profile or a similar custom DICE profile. | +| `#8.80152(COSE_Key)` | COSE_Key | A public key. | + +### Ordering and semantics + +- The first element represents the root device identity key. It may be a + self-signed certificate, a CA-signed certificate, or a bare public key + (`#8.80152`). +- A benign implementation orders the remaining certificates from root to leaf. +- The chain only contains certificates generated during bootup. Userspace is + responsible for managing additional CDI certificates issued via the + `/dev/nat20` character device. + +## Building with Buildroot + +The `br_external/` directory provides a Buildroot external tree for building +all components targeting a QEMU x86_64 virtual machine. + +```sh +# Bootstrap the Buildroot environment (build directory must be outside the repo) +BUILDROOT_DIR=/path/to/buildroot.build +examples/linux/br_external/bootstrap.sh qemu $BUILDROOT_DIR + +# Build everything +cd $BUILDROOT_DIR/buildroot +make +``` + +### Local development +The bootstrap script installs a file `envsetup.sh` in `$BUILDROOT_DIR` +which sets the `*_OVERRIDE_SRCDIR`s for all of the packages nat20* etc., +and defines helper functions for rebuilding and running the result in +qemu. + +``` +# Source the environment +cd $BUILDROOT_DIR +. envsetup.sh + +# Rebuild a package and the rootfs, this is equivalent to +# pushd $BUILDROOT_DIR/buildroot +# make nat20sw-rebuild all +# popd +brrebuild nat20sw + +# Rebuild all packages and the rootfs +brrebuild all + +# Run the resulting rootfs and kernel in qemu +run-qemu + +# Run the nat20cli test in qemu and shut down the emulator +run-nat20cli-test + +# Run the nat20test integration test in qemu and sht down the emulator +run-nat20test-test +``` + +### Caveat - `bootstrap.sh` +The bootstrap script is usefull for setting up the development environment +once, but it cannot keep the build root environment in sync with the upstream +repository. When pulling the upstream repository and changes where made to +`envsetup.sh`, `bootstrap.sh`, or the buildroot config, it may be necessary +to bootstrap a new environment. A savvy user may track the changes and manually +update the environment to save 30 minutes rebuilding the build root environment. \ No newline at end of file diff --git a/examples/linux/br_external/Config.in b/examples/linux/br_external/Config.in index 81e3ea5..2e41cc0 100644 --- a/examples/linux/br_external/Config.in +++ b/examples/linux/br_external/Config.in @@ -33,7 +33,10 @@ # along with this program; if not, see # . +source "$BR2_EXTERNAL_NAT20_PATH/package/nat20cli/Config.in" source "$BR2_EXTERNAL_NAT20_PATH/package/nat20crypto/Config.in" 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 567ba96..1e70e7d 100644 --- a/examples/linux/br_external/configs/qemu_br_defconfig +++ b/examples/linux/br_external/configs/qemu_br_defconfig @@ -3976,7 +3976,10 @@ BR2_TARGET_UBOOT_CUSTOM_PATCH_DIR="" # # Provides NAT20 related packages. # +BR2_PACKAGE_NAT20CLI=y BR2_PACKAGE_NAT20CRYPTO=y 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/libnat20/Config.in b/examples/linux/br_external/package/libnat20/Config.in new file mode 100644 index 0000000..6c99698 --- /dev/null +++ b/examples/linux/br_external/package/libnat20/Config.in @@ -0,0 +1,39 @@ +# 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_LIBNAT20 + bool "libnat20" + help + Add the libnat20 DICE library diff --git a/examples/linux/br_external/package/libnat20/libnat20.mk b/examples/linux/br_external/package/libnat20/libnat20.mk new file mode 100644 index 0000000..04a1570 --- /dev/null +++ b/examples/linux/br_external/package/libnat20/libnat20.mk @@ -0,0 +1,49 @@ +# 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 LIBNAT20_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. +LIBNAT20_VERSION = origin/main +LIBNAT20_SITE = https://github.com/aurora-opensource/libnat20.git +LIBNAT20_SITE_METHOD = git +LIBNAT20_LICENSE = Apache-2.0 OR GPL-2.0 +LIBNAT20_LICENSE_FILES = LICENSE-Apache-2.0.txt LICENSE-GPL-2.0.txt + +LIBNAT20_INSTALL_STAGING = YES +LIBNAT20_INSTALL_TARGET = NO + +$(eval $(cmake-package)) diff --git a/examples/linux/br_external/package/nat20cli/Config.in b/examples/linux/br_external/package/nat20cli/Config.in new file mode 100644 index 0000000..0eb7b0c --- /dev/null +++ b/examples/linux/br_external/package/nat20cli/Config.in @@ -0,0 +1,40 @@ +# 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_NAT20CLI + bool "nat20cli" + depends on BR2_PACKAGE_LIBNAT20 + help + Enable building the nat20cli tool. diff --git a/examples/linux/br_external/package/nat20cli/nat20cli.mk b/examples/linux/br_external/package/nat20cli/nat20cli.mk new file mode 100644 index 0000000..3527db4 --- /dev/null +++ b/examples/linux/br_external/package/nat20cli/nat20cli.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 NAT20CLI_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. +NAT20CLI_VERSION = origin/main +NAT20CLI_SITE = https://github.com/aurora-opensource/libnat20.git +NAT20CLI_SITE_METHOD = git +NAT20CLI_LICENSE = Apache-2.0 OR GPL-2.0 +NAT20CLI_LICENSE_FILES = LICENSE-Apache-2.0.txt LICENSE-GPL-2.0.txt + +NAT20CLI_SUBDIR = examples/linux/nat20cli + +NAT20CLI_INSTALL_TARGET = YES +NAT20CLI_DEPENDENCIES += libnat20 + +$(eval $(cmake-package)) 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 a7001b6..f9c13b3 100644 --- a/examples/linux/br_external/utils/envsetup.sh +++ b/examples/linux/br_external/utils/envsetup.sh @@ -46,10 +46,13 @@ fi source .env +export NAT20CLI_OVERRIDE_SRCDIR="$LIBNAT20_ROOT" 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() { "$@" @@ -71,20 +74,50 @@ function brrebuild() { echo "Available targets:" echo " all - Rebuild all components" echo " linux - Rebuild the linux kernel" + echo " nat20cli - Rebuild the Dice CLI" echo " nat20crypto - Rebuild the nat20crypto module" + echo " libnat20 - Rebuild the libnat20 library" 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 all + ensure_popd make linux-rebuild nat20lib-rebuild nat20crypto-rebuild nat20device-rebuild nat20sw-rebuild libnat20-rebuild nat20cli-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" +} + +function run-nat20cli-test() { + run-qemu "/usr/bin/nat20cli_qemu_init.sh" +} diff --git a/examples/linux/nat20cli/CMakeLists.txt b/examples/linux/nat20cli/CMakeLists.txt new file mode 100644 index 0000000..d7df32f --- /dev/null +++ b/examples/linux/nat20cli/CMakeLists.txt @@ -0,0 +1,83 @@ +# 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(NAT20CLI 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 benfit of clangd based IDE support. +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +################################################################################################### +# The following section defines all the groups of source files. +# All files must be specified explicitly; no globbing or other generation is allowed. + +set(NAT20CLI_SOURCES + # Add the core library source files here. + src/main.c +) + +################################################################################################### + +################################################################################################### +# The nat20_service library is also part of the product of this project. +# It will always be compiled. +add_executable(nat20cli) + +find_package(LibNat20 REQUIRED) + +target_sources(nat20cli + PRIVATE ${NAT20CLI_SOURCES} +) + +target_link_libraries(nat20cli PRIVATE LibNat20::nat20 LibNat20::nat20_service LibNat20::nat20_crypto_nat20) + +target_compile_options(nat20cli + PRIVATE -pedantic + PRIVATE -Wall + PRIVATE -Wextra + PRIVATE -Werror +) + +install(TARGETS nat20cli RUNTIME DESTINATION bin) +install(PROGRAMS nat20cli_test.sh DESTINATION bin) +install(PROGRAMS nat20cli_qemu_init.sh DESTINATION bin) +install(FILES openssl_dice.cnf DESTINATION bin) + +################################################################################################### diff --git a/examples/linux/nat20cli/nat20cli_qemu_init.sh b/examples/linux/nat20cli/nat20cli_qemu_init.sh new file mode 100644 index 0000000..75a0af8 --- /dev/null +++ b/examples/linux/nat20cli/nat20cli_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 + +nat20cli_test.sh +rc=$? + +if [ $rc -eq 0 ]; then + echo "NAT20CLI_TESTS_PASSED" +else + echo "NAT20CLI_TESTS_FAILED (exit code: $rc)" +fi + +poweroff -f diff --git a/examples/linux/nat20cli/nat20cli_test.sh b/examples/linux/nat20cli/nat20cli_test.sh new file mode 100755 index 0000000..6b42ea9 --- /dev/null +++ b/examples/linux/nat20cli/nat20cli_test.sh @@ -0,0 +1,113 @@ +#!/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 +# . + +set -e + +SCRIPT_DIR="$(dirname "$0")" +export OPENSSL_CONF="${SCRIPT_DIR}/openssl_dice.cnf" + +modprobe nat20sw +mount -t securityfs none /sys/kernel/security + +echo -n "Test message for ECA EE cert" > message.txt + +nat20cli cdi-cert --key-type p256 --parent-key-type p256 --output cdi_0.der --certificate-format x509 --code-desc 795375622d322e332e343a33386334353963666164666132623839353333363939353465313266373534386433613161633937336338383830303563336236646232333436636263386631 --code 228d8f76c811276e991012cf5f46090377fc72c95a6ef9e1ccd4eebec8997be5b57f0fb2c7f4804af212711e7b49533f8bc00ddee9480f76155b3da1101604b9 --conf-desc 45787472616f7264696e617279206e6f726d616c20636f6e66696775726174696f6e --conf 671e957aff5565a55961dcaef7634f1a665d8f286e7bd99593532741417f22981b57bdc39241c9685f7377e3622067c261c3ce974e6db5f18d121adad2d76185 --auth-desc 41206365727469666963617465 --auth 50808e4ab921ecf31ca5f662b6d8b85b98ec4d3f64175c8b5d70c1f0e2fef048f87b3178907e1f2d652bd8588fa84f4c374347cc34b97dae13a5b981790b38cb --mode normal --hidden 2f299d2cc916e5219a6bcbc14c7135fa25e9a71018c2bafe8c0658d4041de6c87aa444aedcc68e7d7674b81b5838be1b74bf19d4d6fb05fb0db9ee7e297afc09 +nat20cli cdi-cert --key-type p256 --parent-key-type p256 --output cdi_0.cose --certificate-format cose --code-desc 795375622d322e332e343a33386334353963666164666132623839353333363939353465313266373534386433613161633937336338383830303563336236646232333436636263386631 --code 228d8f76c811276e991012cf5f46090377fc72c95a6ef9e1ccd4eebec8997be5b57f0fb2c7f4804af212711e7b49533f8bc00ddee9480f76155b3da1101604b9 --conf-desc 45787472616f7264696e617279206e6f726d616c20636f6e66696775726174696f6e --conf 671e957aff5565a55961dcaef7634f1a665d8f286e7bd99593532741417f22981b57bdc39241c9685f7377e3622067c261c3ce974e6db5f18d121adad2d76185 --auth-desc 41206365727469666963617465 --auth 50808e4ab921ecf31ca5f662b6d8b85b98ec4d3f64175c8b5d70c1f0e2fef048f87b3178907e1f2d652bd8588fa84f4c374347cc34b97dae13a5b981790b38cb --mode normal --hidden 2f299d2cc916e5219a6bcbc14c7135fa25e9a71018c2bafe8c0658d4041de6c87aa444aedcc68e7d7674b81b5838be1b74bf19d4d6fb05fb0db9ee7e297afc09 +nat20cli promote -i 790fd72ee1352017d822773bc8f5c1ac6e4bf310dfac72fbff622368c01372bc78324f0c06cbc37964e32b18588560a386357e4517ffe93052c67fe6213c38bc +nat20cli cdi-cert --key-type p256 --parent-key-type p256 --output cdi_1.der --certificate-format x509 --code-desc 795375622d322e332e343a33386334353963666164666132623839353333363939353465313266373534386433613161633937336338383830303563336236646232333436636263386631 --code 228d8f76c811276e991012cf5f46090377fc72c95a6ef9e1ccd4eebec8997be5b57f0fb2c7f4804af212711e7b49533f8bc00ddee9480f76155b3da1101604b9 --conf-desc 45787472616f7264696e617279206e6f726d616c20636f6e66696775726174696f6e --conf 671e957aff5565a55961dcaef7634f1a665d8f286e7bd99593532741417f22981b57bdc39241c9685f7377e3622067c261c3ce974e6db5f18d121adad2d76185 --auth-desc 41206365727469666963617465 --auth 50808e4ab921ecf31ca5f662b6d8b85b98ec4d3f64175c8b5d70c1f0e2fef048f87b3178907e1f2d652bd8588fa84f4c374347cc34b97dae13a5b981790b38cb --mode normal --hidden 2f299d2cc916e5219a6bcbc14c7135fa25e9a71018c2bafe8c0658d4041de6c87aa444aedcc68e7d7674b81b5838be1b74bf19d4d6fb05fb0db9ee7e297afc09 +nat20cli cdi-cert --key-type p256 --parent-key-type p256 --output cdi_1.cose --certificate-format cose --code-desc 795375622d322e332e343a33386334353963666164666132623839353333363939353465313266373534386433613161633937336338383830303563336236646232333436636263386631 --code 228d8f76c811276e991012cf5f46090377fc72c95a6ef9e1ccd4eebec8997be5b57f0fb2c7f4804af212711e7b49533f8bc00ddee9480f76155b3da1101604b9 --conf-desc 45787472616f7264696e617279206e6f726d616c20636f6e66696775726174696f6e --conf 671e957aff5565a55961dcaef7634f1a665d8f286e7bd99593532741417f22981b57bdc39241c9685f7377e3622067c261c3ce974e6db5f18d121adad2d76185 --auth-desc 41206365727469666963617465 --auth 50808e4ab921ecf31ca5f662b6d8b85b98ec4d3f64175c8b5d70c1f0e2fef048f87b3178907e1f2d652bd8588fa84f4c374347cc34b97dae13a5b981790b38cb --mode normal --hidden 2f299d2cc916e5219a6bcbc14c7135fa25e9a71018c2bafe8c0658d4041de6c87aa444aedcc68e7d7674b81b5838be1b74bf19d4d6fb05fb0db9ee7e297afc09 +nat20cli promote -i 790fd72ee1352017d822773bc8f5c1ac6e4bf310dfac72fbff622368c01372bc78324f0c06cbc37964e32b18588560a386357e4517ffe93052c67fe6213c38bc +nat20cli eca-cert --key-type p256 --parent-key-type p256 --output eca.der --certificate-format x509 --challenge aabbcc +nat20cli eca-ee-cert --key-type p256 --parent-key-type p256 --output eca_ee.der --certificate-format x509 --challenge aabbcc --name "Test ECA EE Cert" --key-usage sign +nat20cli eca-ee-sign --key-type p256 --parent-key-type p256 --challenge aabbcc --name "Test ECA EE Cert" --key-usage sign --message "$(xxd -p message.txt)" --output eca_ee.sig + +openssl x509 -inform der -outform pem -in cdi_0.der -out cdi_0.pem +openssl x509 -inform der -outform pem -in cdi_1.der -out cdi_1.pem +openssl x509 -inform der -outform pem -in eca.der -out eca.pem +openssl x509 -inform der -outform pem -in eca_ee.der -out eca_ee.pem + +# The dice chain is formatted as variable length CBOR array +# with each element being a tagged certificate. +# Here, it is assumed the the chain contains only the semi hardcoded UDS certificate +# from the nat20sw example, which is the only certificate in the chain. +# arr (#6.80150 (bytes(DER encoded cert))) +# tail -c+10 strips off the first 9 bytes: +# The variable lenght array header (1 byte 0x9f) +# The certificate tag (5 bytes) +# The bytes header (3 bytes) +# The head -c-1 strips off the last byte, which is the CBOR "break" byte (0xff) for the variable length array. +# The resulting uds_cert.der file is the DER encoded UDS certificate, which can be parsed with standard tools. +tail -c+10 /sys/kernel/security/nat200/dice_chain | head -c-1 > uds_cert.der + +openssl x509 -inform der -in uds_cert.der -outform pem -out uds_cert_p256.pem + +cat uds_cert_p256.pem cdi_0.pem cdi_1.pem eca.pem > chain.pem + +openssl x509 -inform pem -in uds_cert_p256.pem -noout -text +openssl x509 -inform pem -in cdi_0.pem -noout -text +openssl x509 -inform pem -in cdi_1.pem -noout -text +openssl x509 -inform pem -in eca.pem -noout -text +openssl x509 -inform pem -in eca_ee.pem -noout -text + + +# Make an asn1 spec for generaring a signature that can be verified with OpenSSL. +# The signature is generated by the nat20cli eca-ee-sign command, which produces +# a raw signature (R||S). The ASN.1 spec wraps the raw signature in a structure +# that OpenSSL can parse and verify. +cat << EOF > sig.asn1 +asn1 = SEQUENCE:sig_seq + +[sig_seq] +r = INTEGER:0x$(head -c 32 eca_ee.sig | xxd -p | tr -d '\n') +s = INTEGER:0x$(tail -c 32 eca_ee.sig | xxd -p | tr -d '\n') +EOF + +openssl asn1parse -genconf sig.asn1 -out sig.der + +# Verify the certificate chain. The UDS certificate is self-signed, so it is the trust anchor for the chain. +# The -ignore_critical flag is needed to ignore the critical extension in the UDS certificate, +# which is not understood by OpenSSL but is required by the DICE specification. This check +# only verifies the signatures and the certificate format, not the critical extension semantics. +openssl verify -ignore_critical -CAfile chain.pem eca_ee.pem + +echo "OpenSSL chain verification passed." + +# Extract public key from the ECA EE cert and verify the signature on the message. +openssl x509 -inform pem -in eca_ee.pem -pubkey -noout > eca_ee_pubkey.pem +openssl dgst -sha256 -verify eca_ee_pubkey.pem -signature sig.der message.txt + +echo "OpenSSL signature verification passed." \ No newline at end of file diff --git a/examples/linux/nat20cli/openssl_dice.cnf b/examples/linux/nat20cli/openssl_dice.cnf new file mode 100644 index 0000000..5e27382 --- /dev/null +++ b/examples/linux/nat20cli/openssl_dice.cnf @@ -0,0 +1,61 @@ +# 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 +# . + +# OpenSSL configuration for DICE certificate extensions. +# +# DICE certificates contain critical X.509v3 extensions with OIDs that +# vanilla OpenSSL does not recognize. This config registers their names +# for human-readable display in `openssl x509 -text` output. +# +# Because OpenSSL has no config-level mechanism to register handlers for +# custom critical extensions, `openssl verify` must also be invoked with +# -ignore_critical when verifying DICE certificate chains. +# +# Usage: +# export OPENSSL_CONF=/path/to/openssl_dice.cnf +# openssl x509 -in cert.pem -noout -text +# openssl verify -ignore_critical -CAfile chain.pem leaf.pem + +openssl_conf = openssl_init + +[openssl_init] +oid_section = dice_oids + +[dice_oids] +openDiceInput = Open DICE Input, 1.3.6.1.4.1.11129.2.1.24 +tcgDiceTcbInfo = TCG DICE TCB Info, 2.23.133.5.4.1 +tcgDiceMultiTcbInfo = TCG DICE Multi-TCB Info, 2.23.133.5.4.5 +tcgDiceUeid = TCG DICE UEID, 2.23.133.5.4.4 +tcgDiceTcbFreshness = TCG DICE TCB Freshness, 2.23.133.5.4.11 diff --git a/examples/linux/nat20cli/src/main.c b/examples/linux/nat20cli/src/main.c new file mode 100644 index 0000000..9349440 --- /dev/null +++ b/examples/linux/nat20cli/src/main.c @@ -0,0 +1,1055 @@ +/* + * 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 +#include +#include +#include + +// CLI tool specific error codes +typedef enum { + cli_error_ok = 0, + cli_error_invalid_argument, + cli_error_io, + cli_error_memory, + cli_error_libnat20, + cli_error_server, +} cli_error_t; + +char const *usage_format_str = + "Usage: %s \n" + "Commands:\n" + " promote Instruct the service to promote the caller to the next " + "level.\n" + " cdi-cert Instruct the service to issue a CDI certificate.\n" + " eca-cert Instruct the service to issue an ECA certificate.\n" + " eca-ee-cert Instruct the service to issue an ECA End-Entity " + "certificate.\n" + " eca-ee-sign Instruct the service to sign a message with an ECA EE " + "key.\n" + "Options promote:\n" + " --compressed-input -i :\n" + " A hex string. " + "H(||||)\n" + "\n" + "Options common (all commands except promote):\n" + " --key-type -k \n" + " --parent-path-element -n \n" + " A parent path element. May be given multiple times. Each " + "element\n" + " is a compressed input. The inputs are used to derive the " + "effective\n" + " parent CDI and thus the key material for the operation.\n" + " --output -o \n" + " The output file to write the resulting certificate or " + "signature to.\n" + "\n" + "Options (*-cert commands):\n" + " --parent-key-type -p \n" + " The key type of the parent key. This is used to identify " + "the\n" + " issuer key algorithm.\n" + " --certificate-format -f \n" + " The format of the certificate to be issued.\n" + "\n" + "Options (cdi-cert):" + " --code -c \n" + " The code hash as hex string.\n" + " --code-desc -C \n" + " The code description as hex string.\n" + " --conf -g \n" + " The configuration hash as hex string.\n" + " --conf-desc -G \n" + " The configuration description as hex string.\n" + " --auth -a \n" + " The authorization hash as hex string.\n" + " --auth-desc -A \n" + " The authorization description as hex string.\n" + " --mode -m \n" + " The mode.\n" + " --hidden -H \n" + " The hidden context as hex string. Hidden is part of the " + "CDI derivation " + "context.\n" + " But does not appear in the CDI certificate.\n" + " --profile-name -P \n" + " The profile name. The DICE profile name is used to " + "identify the\n" + " specific DICE profile being used.\n" + "\n" + "Options (eca-ee-cert and eca-ee-sign)\n" + " --name -N \n" + " The application specific name of the end-entity key. It " + "is not\n" + " included in the issued end-entity certificate, but it is " + "part of\n" + " the key derivation context. Thus keys with different " + "names are\n" + " never identical.\n" + " --key-usage -u \n" + " The key usage.\n" + "\n" + "Options (eca-cert and eca-ee-cert)\n" + " --challenge -l \n" + " The challenge. Will be included in the certificate. " + "Using the\n" + " TCG DICE Freshness extension.\n" + "\n" + "Options (eca-ee-sign)\n" + " --message -M \n" + " The message.\n"; + +void print_usage(char const *prog) { fprintf(stderr, usage_format_str, prog); } + +int parse_key_type(char const *str) { + if (strcmp(str, "ed25519") == 0) return n20_crypto_key_type_ed25519_e; + if (strcmp(str, "p256") == 0) return n20_crypto_key_type_secp256r1_e; + if (strcmp(str, "p384") == 0) return n20_crypto_key_type_secp384r1_e; + return n20_crypto_key_type_none_e; +} + +int parse_request_type(char const *str) { + if (strcmp(str, "promote") == 0) return n20_msg_request_type_promote_e; + if (strcmp(str, "cdi-cert") == 0) return n20_msg_request_type_issue_cdi_cert_e; + if (strcmp(str, "eca-cert") == 0) return n20_msg_request_type_issue_eca_cert_e; + if (strcmp(str, "eca-ee-cert") == 0) return n20_msg_request_type_issue_eca_ee_cert_e; + if (strcmp(str, "eca-ee-sign") == 0) return n20_msg_request_type_eca_ee_sign_e; + return n20_msg_request_type_none_e; +} + +int parse_mode(char const *str) { + if (strcmp(str, "not-configured") == 0) return n20_open_dice_mode_not_configured_e; + if (strcmp(str, "normal") == 0) return n20_open_dice_mode_normal_e; + if (strcmp(str, "debug") == 0) return n20_open_dice_mode_debug_e; + if (strcmp(str, "recovery") == 0) return n20_open_dice_mode_recovery_e; + return n20_open_dice_mode_not_configured_e; +} + +int parse_output_format(char const *str) { + if (strcmp(str, "x509") == 0) return n20_certificate_format_x509_e; +#ifdef N20_WITH_COSE + if (strcmp(str, "cose") == 0) return n20_certificate_format_cose_e; +#endif + return n20_certificate_format_none_e; +} + +void parse_key_usage(char const *str, uint8_t key_usage[2]) { + if (strcmp(str, "sign") == 0) { + N20_OPEN_DICE_KEY_USAGE_SET_DIGITAL_SIGNATURE(key_usage); + } else if (strcmp(str, "cert-sign") == 0) { + N20_OPEN_DICE_KEY_USAGE_SET_KEY_CERT_SIGN(key_usage); + } +} + +// Intermediate structure to hold parsed command-line options +typedef struct { + // Common fields + int request_type; + char const *output_file; + + // Key-related fields + int subject_key_type; // -k + int issuer_key_type; // -p + + // Parent path (used by most commands except promote) + struct { + char const **elements; // Array of hex strings + size_t count; + size_t capacity; + } parent_path; + + // Certificate-related + int certificate_format; // -f + char const *challenge; // -l + + // CDI-specific fields + struct { + char const *code_hash; // -c + char const *code_desc; // -C + char const *conf_hash; // -g + char const *conf_desc; // -G + char const *auth_hash; // -a + char const *auth_desc; // -A + char const *hidden; // -H + int mode; // -m + char const *profile_name; // -P + } cdi_fields; + + // ECA EE-specific fields + struct { + char const *name; // -N + char const *key_usage_str; // -u + } ee_fields; + + // Command-specific fields + char const *compressed_input; // -i (promote) + char const *message; // -M (eca-ee-sign) +} parsed_options_t; + +// Convert a hex nibble character to its 4-bit value +static int8_t nibble2bits(uint8_t nibble) { + nibble -= 0x30; // Convert ASCII to numeric value + if (nibble <= 9) return nibble; + nibble &= 0xDF; // Convert to uppercase + nibble -= 7; // Adjust for A-F + if (nibble < 0x10) return nibble; + return -1; +} + +static int hex_string_to_bytes_in_place(char *hex) { + size_t len = strlen(hex); + uint8_t *out_pos = (uint8_t *)hex; + size_t pos = 0; + if ((len & 1) != 0) { + // Odd length, assume leading zero + *out_pos++ = nibble2bits(hex[0]); + pos++; + } + + while (pos < len) { + int8_t high = nibble2bits(hex[pos++]); + int8_t low = nibble2bits(hex[pos++]); + if (high < 0 || low < 0) { + return -1; // Invalid hex character + } + *out_pos++ = (high << 4) | low; + } + + return out_pos - (uint8_t *)hex; // Return number of bytes written +} + +// Helper function to parse hex string into a slice +static cli_error_t parse_hex_to_slice(n20_slice_t *slice, + char const *hex_str, + char const *field_name) { + if (hex_str == NULL) { + slice->buffer = NULL; + slice->size = 0; + return cli_error_ok; + } + + slice->buffer = (uint8_t *)hex_str; + int bytes_written = hex_string_to_bytes_in_place((char *)slice->buffer); + if (bytes_written < 0) { + fprintf(stderr, "Invalid hex string for %s\n", field_name); + return cli_error_invalid_argument; + } + slice->size = bytes_written; + return cli_error_ok; +} + +// Helper function to add parent path element to options +static bool add_parent_path_element(parsed_options_t *opts, char const *element) { + if (opts->parent_path.count >= opts->parent_path.capacity) { + size_t new_capacity = opts->parent_path.capacity == 0 ? 4 : opts->parent_path.capacity * 2; + char const **new_elements = + reallocarray((void *)opts->parent_path.elements, new_capacity, sizeof(char const *)); + if (new_elements == NULL) { + return false; + } + opts->parent_path.elements = new_elements; + opts->parent_path.capacity = new_capacity; + } + opts->parent_path.elements[opts->parent_path.count++] = element; + return true; +} + +// Helper function to clean up parsed options +static void cleanup_parsed_options(parsed_options_t *opts) { + if (opts->parent_path.elements != NULL) { + free((void *)opts->parent_path.elements); + opts->parent_path.elements = NULL; + } +} + +static bool add_parent_path_decoded(n20_parent_path_t *path, char const *hex_str) { + if (path->is_encoded) { + fprintf(stderr, "Cannot add parent path element to already encoded path\n"); + return false; + } + n20_slice_t *new_slices = + reallocarray((void *)path->decoded, path->length + 1, sizeof(n20_slice_t)); + if (new_slices == NULL) { + free((void *)path->decoded); + path->decoded = NULL; + return false; + } + path->decoded = new_slices; + new_slices[path->length].buffer = (uint8_t *)hex_str; + new_slices[path->length].size = strlen(hex_str) / 2; // Assuming hex string represents bytes + path->length++; + return true; +} + +static void clean_up_request(n20_msg_request_t *request) { + n20_parent_path_t *path = NULL; + switch (request->request_type) { + case n20_msg_request_type_issue_cdi_cert_e: + path = &request->payload.issue_cdi_cert.parent_path; + break; + case n20_msg_request_type_issue_eca_cert_e: + path = &request->payload.issue_eca_cert.parent_path; + break; + case n20_msg_request_type_issue_eca_ee_cert_e: + path = &request->payload.issue_eca_ee_cert.parent_path; + break; + case n20_msg_request_type_eca_ee_sign_e: + path = &request->payload.eca_ee_sign.parent_path; + break; + default: + return; // No parent path to clean up + } + if (!path->is_encoded && path->decoded != NULL) { + free((void *)path->decoded); + path->decoded = NULL; + } +} +// Unified option parsing function +static int parse_command_options(int argc, char *argv[], parsed_options_t *opts) { + // Define all possible long options + static struct option long_options[] = {// Common options + {"key-type", required_argument, 0, 'k'}, + {"parent-path-element", required_argument, 0, 'n'}, + {"output", required_argument, 0, 'o'}, + {"parent-key-type", required_argument, 0, 'p'}, + {"certificate-format", required_argument, 0, 'f'}, + {"challenge", required_argument, 0, 'l'}, + {"help", no_argument, 0, '?'}, + + // Promote options + {"compressed-input", required_argument, 0, 'i'}, + + // CDI cert options + {"code", required_argument, 0, 'c'}, + {"code-desc", required_argument, 0, 'C'}, + {"conf", required_argument, 0, 'g'}, + {"conf-desc", required_argument, 0, 'G'}, + {"auth", required_argument, 0, 'a'}, + {"auth-desc", required_argument, 0, 'A'}, + {"mode", required_argument, 0, 'm'}, + {"hidden", required_argument, 0, 'H'}, + {"profile-name", required_argument, 0, 'P'}, + + // ECA EE options + {"name", required_argument, 0, 'N'}, + {"key-usage", required_argument, 0, 'u'}, + + // ECA EE sign options + {"message", required_argument, 0, 'M'}, + + {0, 0, 0, 0}}; + + int opt; + while ((opt = getopt_long( + argc, argv, "i:k:n:o:p:f:c:C:g:G:a:A:m:H:P:l:N:u:M:?", long_options, NULL)) != -1) { + switch (opt) { + // Common options + case 'k': + opts->subject_key_type = parse_key_type(optarg); + break; + case 'p': + opts->issuer_key_type = parse_key_type(optarg); + break; + case 'n': + if (!add_parent_path_element(opts, optarg)) { + fprintf(stderr, "Failed to add parent path element\n"); + return -1; + } + break; + case 'o': + opts->output_file = optarg; + break; + case 'f': + opts->certificate_format = parse_output_format(optarg); + break; + case 'l': + opts->challenge = optarg; + break; + + // Promote options + case 'i': + opts->compressed_input = optarg; + break; + + // CDI cert options + case 'c': + opts->cdi_fields.code_hash = optarg; + break; + case 'C': + opts->cdi_fields.code_desc = optarg; + break; + case 'g': + opts->cdi_fields.conf_hash = optarg; + break; + case 'G': + opts->cdi_fields.conf_desc = optarg; + break; + case 'a': + opts->cdi_fields.auth_hash = optarg; + break; + case 'A': + opts->cdi_fields.auth_desc = optarg; + break; + case 'm': + opts->cdi_fields.mode = parse_mode(optarg); + break; + case 'H': + opts->cdi_fields.hidden = optarg; + break; + case 'P': + opts->cdi_fields.profile_name = optarg; + break; + + // ECA EE options + case 'N': + opts->ee_fields.name = optarg; + break; + case 'u': + opts->ee_fields.key_usage_str = optarg; + break; + + // ECA EE sign options + case 'M': + opts->message = optarg; + break; + + case '?': + // Help requested + return -1; + default: + // Unknown option + return -1; + } + } + + return 0; +} + +// Initialize promote request from parsed options +static cli_error_t init_promote_request(n20_msg_request_t *request, parsed_options_t const *opts) { + if (opts->compressed_input == NULL) { + fprintf(stderr, "Promote requires --compressed-input\n"); + return cli_error_invalid_argument; + } + + request->request_type = n20_msg_request_type_promote_e; + return parse_hex_to_slice( + &request->payload.promote.compressed_context, opts->compressed_input, "compressed input"); +} + +// Initialize CDI cert request from parsed options +static cli_error_t init_cdi_cert_request(n20_msg_request_t *request, parsed_options_t const *opts) { + cli_error_t err; + + request->request_type = n20_msg_request_type_issue_cdi_cert_e; + request->payload.issue_cdi_cert.subject_key_type = opts->subject_key_type; + request->payload.issue_cdi_cert.issuer_key_type = opts->issuer_key_type; + request->payload.issue_cdi_cert.certificate_format = opts->certificate_format; + + // Validate required fields + if (opts->subject_key_type == n20_crypto_key_type_none_e) { + fprintf(stderr, "Invalid or missing --key-type\n"); + return cli_error_invalid_argument; + } + if (opts->issuer_key_type == n20_crypto_key_type_none_e) { + fprintf(stderr, "Invalid or missing --parent-key-type\n"); + return cli_error_invalid_argument; + } + if (opts->certificate_format == n20_certificate_format_none_e) { + fprintf(stderr, "Invalid or missing --certificate-format\n"); + return cli_error_invalid_argument; + } + + // Parse CDI fields + err = parse_hex_to_slice(&request->payload.issue_cdi_cert.next_context.code_hash, + opts->cdi_fields.code_hash, + "code hash"); + if (err != cli_error_ok) return err; + + err = parse_hex_to_slice(&request->payload.issue_cdi_cert.next_context.code_descriptor, + opts->cdi_fields.code_desc, + "code descriptor"); + if (err != cli_error_ok) return err; + + err = parse_hex_to_slice(&request->payload.issue_cdi_cert.next_context.configuration_hash, + opts->cdi_fields.conf_hash, + "configuration hash"); + if (err != cli_error_ok) return err; + + err = parse_hex_to_slice(&request->payload.issue_cdi_cert.next_context.configuration_descriptor, + opts->cdi_fields.conf_desc, + "configuration descriptor"); + if (err != cli_error_ok) return err; + + err = parse_hex_to_slice(&request->payload.issue_cdi_cert.next_context.authority_hash, + opts->cdi_fields.auth_hash, + "authority hash"); + if (err != cli_error_ok) return err; + + err = parse_hex_to_slice(&request->payload.issue_cdi_cert.next_context.authority_descriptor, + opts->cdi_fields.auth_desc, + "authority descriptor"); + if (err != cli_error_ok) return err; + + err = parse_hex_to_slice( + &request->payload.issue_cdi_cert.next_context.hidden, opts->cdi_fields.hidden, "hidden"); + if (err != cli_error_ok) return err; + + request->payload.issue_cdi_cert.next_context.mode = opts->cdi_fields.mode; + + if (opts->cdi_fields.profile_name) { + request->payload.issue_cdi_cert.next_context.profile_name.buffer = + opts->cdi_fields.profile_name; + request->payload.issue_cdi_cert.next_context.profile_name.size = + strlen(opts->cdi_fields.profile_name); + } + + // Build parent path + for (size_t i = 0; i < opts->parent_path.count; ++i) { + if (!add_parent_path_decoded(&request->payload.issue_cdi_cert.parent_path, + opts->parent_path.elements[i])) { + fprintf(stderr, "Failed to add parent path element\n"); + return cli_error_invalid_argument; + } + } + + // Parse parent path hex strings + for (size_t i = 0; i < request->payload.issue_cdi_cert.parent_path.length; ++i) { + err = parse_hex_to_slice( + (n20_slice_t *)&request->payload.issue_cdi_cert.parent_path.decoded[i], + (char const *)request->payload.issue_cdi_cert.parent_path.decoded[i].buffer, + "parent path element"); + if (err != cli_error_ok) return err; + } + + return cli_error_ok; +} + +// Initialize ECA cert request from parsed options +static cli_error_t init_eca_cert_request(n20_msg_request_t *request, parsed_options_t const *opts) { + cli_error_t err; + + request->request_type = n20_msg_request_type_issue_eca_cert_e; + request->payload.issue_eca_cert.subject_key_type = opts->subject_key_type; + request->payload.issue_eca_cert.issuer_key_type = opts->issuer_key_type; + request->payload.issue_eca_cert.certificate_format = opts->certificate_format; + + // Validate required fields + if (opts->subject_key_type == n20_crypto_key_type_none_e) { + fprintf(stderr, "Invalid or missing --key-type\n"); + return cli_error_invalid_argument; + } + if (opts->issuer_key_type == n20_crypto_key_type_none_e) { + fprintf(stderr, "Invalid or missing --parent-key-type\n"); + return cli_error_invalid_argument; + } + if (opts->certificate_format == n20_certificate_format_none_e) { + fprintf(stderr, "Invalid or missing --certificate-format\n"); + return cli_error_invalid_argument; + } + + // Parse challenge if provided + err = parse_hex_to_slice( + &request->payload.issue_eca_cert.challenge, opts->challenge, "challenge"); + if (err != cli_error_ok) return err; + + // Build parent path + for (size_t i = 0; i < opts->parent_path.count; ++i) { + if (!add_parent_path_decoded(&request->payload.issue_eca_cert.parent_path, + opts->parent_path.elements[i])) { + fprintf(stderr, "Failed to add parent path element\n"); + return cli_error_invalid_argument; + } + } + + // Parse parent path hex strings + for (size_t i = 0; i < request->payload.issue_eca_cert.parent_path.length; ++i) { + err = parse_hex_to_slice( + (n20_slice_t *)&request->payload.issue_eca_cert.parent_path.decoded[i], + (char const *)request->payload.issue_eca_cert.parent_path.decoded[i].buffer, + "parent path element"); + if (err != cli_error_ok) return err; + } + + return cli_error_ok; +} + +// Initialize ECA EE cert request from parsed options +static cli_error_t init_eca_ee_cert_request(n20_msg_request_t *request, + parsed_options_t const *opts, + uint8_t key_usage[2]) { + cli_error_t err; + + request->request_type = n20_msg_request_type_issue_eca_ee_cert_e; + request->payload.issue_eca_ee_cert.subject_key_type = opts->subject_key_type; + request->payload.issue_eca_ee_cert.issuer_key_type = opts->issuer_key_type; + request->payload.issue_eca_ee_cert.certificate_format = opts->certificate_format; + + // Validate required fields + if (opts->subject_key_type == n20_crypto_key_type_none_e) { + fprintf(stderr, "Invalid or missing --key-type\n"); + return cli_error_invalid_argument; + } + if (opts->issuer_key_type == n20_crypto_key_type_none_e) { + fprintf(stderr, "Invalid or missing --parent-key-type\n"); + return cli_error_invalid_argument; + } + if (opts->certificate_format == n20_certificate_format_none_e) { + fprintf(stderr, "Invalid or missing --certificate-format\n"); + return cli_error_invalid_argument; + } + + // Set name + if (opts->ee_fields.name) { + request->payload.issue_eca_ee_cert.name.buffer = opts->ee_fields.name; + request->payload.issue_eca_ee_cert.name.size = strlen(opts->ee_fields.name); + } + + // Parse key usage + if (opts->ee_fields.key_usage_str) { + parse_key_usage(opts->ee_fields.key_usage_str, key_usage); + request->payload.issue_eca_ee_cert.key_usage.buffer = key_usage; + request->payload.issue_eca_ee_cert.key_usage.size = 2; + } + + // Parse challenge if provided + err = parse_hex_to_slice( + &request->payload.issue_eca_ee_cert.challenge, opts->challenge, "challenge"); + if (err != cli_error_ok) return err; + + // Build parent path + for (size_t i = 0; i < opts->parent_path.count; ++i) { + if (!add_parent_path_decoded(&request->payload.issue_eca_ee_cert.parent_path, + opts->parent_path.elements[i])) { + fprintf(stderr, "Failed to add parent path element\n"); + return cli_error_invalid_argument; + } + } + + // Parse parent path hex strings + for (size_t i = 0; i < request->payload.issue_eca_ee_cert.parent_path.length; ++i) { + err = parse_hex_to_slice( + (n20_slice_t *)&request->payload.issue_eca_ee_cert.parent_path.decoded[i], + (char const *)request->payload.issue_eca_ee_cert.parent_path.decoded[i].buffer, + "parent path element"); + if (err != cli_error_ok) return err; + } + + return cli_error_ok; +} + +// Initialize ECA EE sign request from parsed options +static cli_error_t init_eca_ee_sign_request(n20_msg_request_t *request, + parsed_options_t const *opts, + uint8_t key_usage[2]) { + cli_error_t err; + + request->request_type = n20_msg_request_type_eca_ee_sign_e; + request->payload.eca_ee_sign.subject_key_type = opts->subject_key_type; + + // Validate required fields + if (opts->subject_key_type == n20_crypto_key_type_none_e) { + fprintf(stderr, "Invalid or missing --key-type\n"); + return cli_error_invalid_argument; + } + + // Set name + if (opts->ee_fields.name) { + request->payload.eca_ee_sign.name.buffer = opts->ee_fields.name; + request->payload.eca_ee_sign.name.size = strlen(opts->ee_fields.name); + } + + // Parse key usage + if (opts->ee_fields.key_usage_str) { + parse_key_usage(opts->ee_fields.key_usage_str, key_usage); + request->payload.eca_ee_sign.key_usage.buffer = key_usage; + request->payload.eca_ee_sign.key_usage.size = 2; + } + + // Parse message + err = parse_hex_to_slice(&request->payload.eca_ee_sign.message, opts->message, "message"); + if (err != cli_error_ok) return err; + + // Build parent path + for (size_t i = 0; i < opts->parent_path.count; ++i) { + if (!add_parent_path_decoded(&request->payload.eca_ee_sign.parent_path, + opts->parent_path.elements[i])) { + fprintf(stderr, "Failed to add parent path element\n"); + return cli_error_invalid_argument; + } + } + + // Parse parent path hex strings + for (size_t i = 0; i < request->payload.eca_ee_sign.parent_path.length; ++i) { + err = parse_hex_to_slice( + (n20_slice_t *)&request->payload.eca_ee_sign.parent_path.decoded[i], + (char const *)request->payload.eca_ee_sign.parent_path.decoded[i].buffer, + "parent path element"); + if (err != cli_error_ok) return err; + } + + return cli_error_ok; +} + +// Helper to write binary data to file or print as hex +static cli_error_t output_binary_data(uint8_t const *data, + size_t size, + char const *output_file, + char const *data_type) { + if (output_file) { + FILE *file = fopen(output_file, "wb"); + if (!file) { + perror("fopen"); + return cli_error_io; + } + size_t written = fwrite(data, 1, size, file); + if (written != size) { + fprintf(stderr, "Failed to write full %s to file\n", data_type); + fclose(file); + return cli_error_io; + } + fclose(file); + printf("%s written to %s\n", data_type, output_file); + } else { + printf("%s data: ", data_type); + for (size_t i = 0; i < size; ++i) { + printf("%02x", data[i]); + } + printf("\n"); + } + return cli_error_ok; +} + +// Handle promote response +static cli_error_t handle_promote_response(n20_slice_t response_slice) { + n20_msg_error_response_t response; + n20_error_t n20_err = n20_msg_error_response_read(&response, response_slice); + if (n20_err != n20_error_ok_e) { + fprintf(stderr, + "Failed to read promote response. libnat20 error: %d (0x%x)\n", + n20_err, + n20_err); + return cli_error_libnat20; + } + if (response.error_code != n20_error_ok_e) { + fprintf(stderr, + "Promote request failed. Server returned libnat20 error: %d (0x%x)\n", + response.error_code, + response.error_code); + return cli_error_server; + } + printf("Promote request successful\n"); + return cli_error_ok; +} + +// Handle certificate response (common for cdi-cert, eca-cert, eca-ee-cert) +static cli_error_t handle_cert_response(n20_slice_t response_slice, + char const *output_file, + char const *cert_type_name, + bool print_debug) { + if (print_debug) { + printf("Raw response (%zu bytes): ", response_slice.size); + size_t preview_len = response_slice.size < 32 ? response_slice.size : 32; + for (size_t i = 0; i < preview_len; ++i) { + printf("%02x", response_slice.buffer[i]); + } + if (response_slice.size > 32) printf("..."); + printf("\n"); + } + + n20_msg_issue_cert_response_t response; + n20_error_t n20_err = n20_msg_issue_cert_response_read(&response, response_slice); + if (n20_err != n20_error_ok_e) { + fprintf(stderr, + "Failed to read %s response. libnat20 error: %d (0x%x)\n", + cert_type_name, + n20_err, + n20_err); + return cli_error_libnat20; + } + if (response.error_code != n20_error_ok_e) { + fprintf(stderr, + "%s request failed. Server returned libnat20 error: %d (0x%x)\n", + cert_type_name, + response.error_code, + response.error_code); + return cli_error_server; + } + printf("%s request successful, certificate size: %zu\n", + cert_type_name, + response.certificate.size); + + return output_binary_data( + response.certificate.buffer, response.certificate.size, output_file, "Certificate"); +} + +// Handle CDI cert response (includes compressed input output) +static cli_error_t handle_cdi_cert_response(n20_slice_t response_slice, + char const *output_file, + n20_open_dice_input_t const *next_context) { + cli_error_t err = handle_cert_response(response_slice, output_file, "CDI cert", true); + if (err != cli_error_ok) return err; + + // Compute and output compressed input + n20_compressed_input_t next_compressed_input; + n20_open_dice_cert_info_t cert_info; + cert_info.cert_type = n20_cert_type_cdi_e; + cert_info.open_dice_input = *next_context; + + n20_crypto_digest_context_t *digest_ctx = NULL; + + n20_error_t n20_err = n20_crypto_nat20_open(&digest_ctx); + if (n20_err != n20_error_ok_e) { + fprintf( + stderr, "Failed to open digest context. libnat20 error: %d (0x%x)\n", n20_err, n20_err); + return cli_error_libnat20; + } + + n20_err = n20_compress_input(digest_ctx, &cert_info, next_compressed_input); + n20_crypto_nat20_close(digest_ctx); + if (n20_err != n20_error_ok_e) { + fprintf(stderr, "Failed to compress input. libnat20 error: %d (0x%x)\n", n20_err, n20_err); + return cli_error_libnat20; + } + + printf("Compressed input: "); + for (size_t i = 0; i < sizeof(next_compressed_input); ++i) { + printf("%02x", next_compressed_input[i]); + } + printf("\n"); + + return cli_error_ok; +} + +// Handle ECA EE sign response +static cli_error_t handle_eca_ee_sign_response(n20_slice_t response_slice, + char const *output_file) { + // First try to read as an error response + n20_msg_error_response_t error_response; + n20_error_t n20_err = n20_msg_error_response_read(&error_response, response_slice); + if (n20_err == n20_error_ok_e && error_response.error_code != n20_error_ok_e) { + fprintf(stderr, + "ECA sign request failed. Server returned libnat20 error: %d (0x%x)\n", + error_response.error_code, + error_response.error_code); + return cli_error_server; + } + + // If not an error response, try to read as sign response + n20_msg_eca_ee_sign_response_t response; + n20_err = n20_msg_eca_ee_sign_response_read(&response, response_slice); + if (n20_err != n20_error_ok_e) { + fprintf(stderr, + "Failed to read ECA sign response. libnat20 error: %d (0x%x)\n", + n20_err, + n20_err); + return cli_error_libnat20; + } + if (response.error_code != n20_error_ok_e) { + fprintf(stderr, + "ECA sign request failed. Server returned libnat20 error: %d (0x%x)\n", + response.error_code, + response.error_code); + return cli_error_server; + } + printf("ECA sign request successful, signature size: %zu\n", response.signature.size); + + return output_binary_data( + response.signature.buffer, response.signature.size, output_file, "Signature"); +} + +int main(int argc, char *argv[]) { + // Stage 1: Parse command options + parsed_options_t opts = { + .request_type = n20_msg_request_type_none_e, + .subject_key_type = n20_crypto_key_type_none_e, + .issuer_key_type = n20_crypto_key_type_none_e, + .certificate_format = n20_certificate_format_none_e, + .cdi_fields = {.mode = n20_open_dice_mode_not_configured_e}, + }; + + if (parse_command_options(argc, argv, &opts) != 0) { + print_usage(argv[0]); + cleanup_parsed_options(&opts); + exit(EXIT_FAILURE); + } + + // Stage 2: Determine command + if (optind >= argc) { + fprintf(stderr, "No command specified\n"); + print_usage(argv[0]); + cleanup_parsed_options(&opts); + exit(EXIT_FAILURE); + } + + int request_type = parse_request_type(argv[optind]); + if (request_type == n20_msg_request_type_none_e) { + fprintf(stderr, "Unknown command: %s\n", argv[optind]); + print_usage(argv[0]); + cleanup_parsed_options(&opts); + exit(EXIT_FAILURE); + } + + opts.request_type = request_type; + + // Stage 3: Initialize request from parsed options + n20_msg_request_t request = {0}; + uint8_t key_usage[2] = {0}; + cli_error_t cli_err = cli_error_ok; + + switch (request_type) { + case n20_msg_request_type_promote_e: + cli_err = init_promote_request(&request, &opts); + break; + case n20_msg_request_type_issue_cdi_cert_e: + cli_err = init_cdi_cert_request(&request, &opts); + break; + case n20_msg_request_type_issue_eca_cert_e: + cli_err = init_eca_cert_request(&request, &opts); + break; + case n20_msg_request_type_issue_eca_ee_cert_e: + cli_err = init_eca_ee_cert_request(&request, &opts, key_usage); + break; + case n20_msg_request_type_eca_ee_sign_e: + cli_err = init_eca_ee_sign_request(&request, &opts, key_usage); + break; + default: + fprintf(stderr, "Unsupported request type: %d\n", request_type); + print_usage(argv[0]); + cleanup_parsed_options(&opts); + exit(EXIT_FAILURE); + } + + if (cli_err != cli_error_ok) { + fprintf(stderr, "Failed to initialize request. CLI error: %d\n", cli_err); + print_usage(argv[0]); + cleanup_parsed_options(&opts); + exit(EXIT_FAILURE); + } + + uint8_t msg_buffer[1024]; + + size_t msg_size = sizeof(msg_buffer); + + n20_error_t n20_err = n20_msg_request_write(&request, msg_buffer, &msg_size); + if (n20_err != n20_error_ok_e) { + fprintf(stderr, "Failed to write request. libnat20 error: %d (0x%x)\n", n20_err, n20_err); + print_usage(argv[0]); + exit(EXIT_FAILURE); + } + + clean_up_request(&request); + + int dice_dev_fd = open("/dev/nat200", O_RDWR); + if (dice_dev_fd < 0) { + perror("open"); + exit(EXIT_FAILURE); + } + + ssize_t bytes_written = + write(dice_dev_fd, msg_buffer + (sizeof(msg_buffer) - msg_size), msg_size); + if (bytes_written < 0) { + perror("write"); + close(dice_dev_fd); + exit(EXIT_FAILURE); + } + + uint8_t response_buffer[1024]; + + ssize_t bytes_received = read(dice_dev_fd, response_buffer, sizeof(response_buffer)); + if (bytes_received < 0) { + perror("read"); + close(dice_dev_fd); + exit(EXIT_FAILURE); + } + close(dice_dev_fd); + + printf("Bytes written: %zd, Bytes received: %zd\n", bytes_written, bytes_received); + + n20_slice_t response_slice = { + .buffer = response_buffer, + .size = (size_t)bytes_received, + }; + + // Handle response based on request type + switch (request.request_type) { + case n20_msg_request_type_promote_e: + cli_err = handle_promote_response(response_slice); + break; + case n20_msg_request_type_issue_cdi_cert_e: + cli_err = handle_cdi_cert_response( + response_slice, opts.output_file, &request.payload.issue_cdi_cert.next_context); + break; + case n20_msg_request_type_issue_eca_cert_e: + cli_err = handle_cert_response(response_slice, opts.output_file, "ECA cert", true); + break; + case n20_msg_request_type_issue_eca_ee_cert_e: + cli_err = + handle_cert_response(response_slice, opts.output_file, "ECA end-entity cert", true); + break; + case n20_msg_request_type_eca_ee_sign_e: + cli_err = handle_eca_ee_sign_response(response_slice, opts.output_file); + break; + default: + fprintf(stderr, "Unknown request type in response\n"); + cleanup_parsed_options(&opts); + exit(EXIT_FAILURE); + } + + cleanup_parsed_options(&opts); + + if (cli_err != cli_error_ok) { + exit(EXIT_FAILURE); + } + + return 0; +} diff --git a/examples/linux/nat20crypto/Makefile b/examples/linux/nat20crypto/Makefile index 9e8ba2a..070b638 100644 --- a/examples/linux/nat20crypto/Makefile +++ b/examples/linux/nat20crypto/Makefile @@ -34,7 +34,7 @@ # . KDIR ?= /lib/modules/`uname -r`/build -INSTALL_MOD_PATH ?= /lib/modules/`uname -r`/extra +INSTALL_MOD_DIR ?= extra NAT20CRYPTO_NAT20LIB_DIR ?= $(PWD)/../nat20lib @@ -44,7 +44,7 @@ modules: $(MAKE) -C $(KDIR) M=$$PWD NAT20CRYPTO_NAT20LIB_DIR="$(NAT20CRYPTO_NAT20LIB_DIR)" modules modules_install: - $(MAKE) -C $(KDIR) M=$$PWD INSTALL_MOD_PATH="$(INSTALL_MOD_PATH)" modules_install + $(MAKE) -C $(KDIR) M=$$PWD INSTALL_MOD_DIR=$(INSTALL_MOD_DIR) modules_install clean: $(MAKE) -C $(KDIR) M=$$PWD clean diff --git a/examples/linux/nat20device/Makefile b/examples/linux/nat20device/Makefile index 69f48d9..e978e5f 100644 --- a/examples/linux/nat20device/Makefile +++ b/examples/linux/nat20device/Makefile @@ -34,7 +34,7 @@ # . KDIR ?= /lib/modules/`uname -r`/build -INSTALL_MOD_PATH ?= /lib/modules/`uname -r`/extra +INSTALL_MOD_DIR ?= extra all: modules @@ -42,7 +42,7 @@ modules: $(MAKE) -C $(KDIR) M=$$PWD modules modules_install: - $(MAKE) -C $(KDIR) M=$$PWD INSTALL_MOD_PATH="$(INSTALL_MOD_PATH)" modules_install + $(MAKE) -C $(KDIR) M=$$PWD INSTALL_MOD_DIR=$(INSTALL_MOD_DIR) modules_install clean: $(MAKE) -C $(KDIR) M=$$PWD clean diff --git a/examples/linux/nat20lib/Makefile b/examples/linux/nat20lib/Makefile index c0f2fe4..c4b3b30 100644 --- a/examples/linux/nat20lib/Makefile +++ b/examples/linux/nat20lib/Makefile @@ -34,7 +34,7 @@ # . KDIR ?= /lib/modules/`uname -r`/build -INSTALL_MOD_PATH ?= /lib/modules/`uname -r`/extra +INSTALL_MOD_DIR ?= extra all: modules @@ -43,7 +43,7 @@ modules: $(MAKE) -C $(KDIR) M=$$PWD modules modules_install: - $(MAKE) -C $(KDIR) M=$$PWD INSTALL_MOD_PATH="$(INSTALL_MOD_PATH)" modules_install + $(MAKE) -C $(KDIR) M=$$PWD INSTALL_MOD_DIR=$(INSTALL_MOD_DIR) modules_install clean: $(MAKE) -C $(KDIR) M=$$PWD clean 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..561d09d --- /dev/null +++ b/examples/linux/nat20test/test/nat20_integration_test.c @@ -0,0 +1,1173 @@ +/* + * 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(); +} + +#if N20_WITH_COSE == 1 +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(); +} +#endif + +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++) { + 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]); + } + } + + /* 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++) { + 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]); + } + } + } + + /* 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++) { + 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]); + } + } + + /* 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++) { + 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]); + } + } + + /* Signature: ee_subject_key_type, parent_path depth 2 */ + for (size_t si = 0; si < NUM_KEY_TYPES; si++) { + level1_artifacts.signature_valid[si] = eca_ee_sign(KEY_TYPES[si], + path_depth2, + test_message, + sizeof(test_message) - 1, + &level1_artifacts.signature[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); + if (!test_extract_x509_pubkey(level1_artifacts.eca[ii2][ii].data, + level1_artifacts.eca[ii2][ii].size, + eca_pubkey, + &eca_pubkey_size)) + continue; + + 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); + if (!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)) + continue; + + 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..1d52bec --- /dev/null +++ b/examples/linux/nat20test/test/test_helpers.c @@ -0,0 +1,525 @@ +/* + * 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) { + return n20_error_insufficient_buffer_size_e; + } + + err = n20_compress_input(digest_ctx, &cert_info, compressed_out); + 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; + 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);