Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions .github/workflows/interop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
name: Interop

on:
push:
branches: [ 'main', 'release/**' ]
pull_request:
branches: [ '**' ]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
TCOSE_SHA: ff4c5f7c6fbbe27bb582214ff1878bf58ebc6c43
QCBOR_SHA: 930708bb86481e88879eb1d87fd4d664f1d69503

jobs:
discover:
uses: ./.github/workflows/_resolve-wolfssl.yml

interop:
name: wolfCOSE <-> t_cose (OpenSSL, wolfSSL ${{ matrix.wolfssl-version }})
needs: discover
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.discover.outputs.matrix) }}

steps:
- uses: actions/checkout@v4

- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y autoconf automake libtool libssl-dev

- name: Cache wolfSSL (${{ matrix.wolfssl-version }})
if: matrix.wolfssl-version != 'master'
id: cache-wolfssl
uses: actions/cache@v4
with:
path: ~/wolfssl-install
key: wolfssl-interop-${{ matrix.wolfssl-version }}-v1

- name: Build wolfSSL (${{ matrix.wolfssl-version }})
if: matrix.wolfssl-version == 'master' || steps.cache-wolfssl.outputs.cache-hit != 'true'
run: |
cd ~
git clone --depth 1 --branch ${{ matrix.wolfssl-ref }} \
https://github.com/wolfSSL/wolfssl.git
cd wolfssl
./autogen.sh
./configure --enable-ecc --enable-ed25519 --enable-ed448 \
--enable-curve25519 --enable-aesgcm --enable-aesccm \
--enable-sha384 --enable-sha512 --enable-keygen \
--enable-rsapss --enable-hkdf --enable-aeskeywrap \
--prefix=$HOME/wolfssl-install
make -j$(nproc)
make install

# QCBOR and t_cose are BSD-3-Clause; fetched at pinned SHAs, not vendored.
- name: Build QCBOR @ pinned SHA
run: |
cd ~
mkdir -p interop-deps/QCBOR && cd interop-deps/QCBOR
git init -q
git fetch --depth 1 -q https://github.com/laurencelundblade/QCBOR.git $QCBOR_SHA
git checkout -q FETCH_HEAD
make libqcbor.a

- name: Build t_cose @ pinned SHA (OpenSSL adapter)
run: |
cd ~/interop-deps
mkdir -p t_cose && cd t_cose
git init -q
git fetch --depth 1 -q https://github.com/laurencelundblade/t_cose.git $TCOSE_SHA
git checkout -q FETCH_HEAD
make -f Makefile.ossl libt_cose.a \
QCBOR_INC="-I $HOME/interop-deps/QCBOR/inc" \
QCBOR_LIB="$HOME/interop-deps/QCBOR/libqcbor.a"

- name: Build wolfCOSE
run: |
export WOLFSSL_DIR=$HOME/wolfssl-install
make CFLAGS="-std=c99 -DHAVE_ANONYMOUS_INLINE_AGGREGATES=1 -Os -Wall -Wextra -I./include -isystem $WOLFSSL_DIR/include" \
LDFLAGS="-L$WOLFSSL_DIR/lib -lwolfssl"

- name: Run t_cose interop
run: |
export WOLFSSL_DIR=$HOME/wolfssl-install
export LD_LIBRARY_PATH=$WOLFSSL_DIR/lib
make interop-tcose \
TCOSE_DIR=$HOME/interop-deps/t_cose \
QCBOR_DIR=$HOME/interop-deps/QCBOR \
TCOSE_CRYPTO_LIB="-lcrypto" \
CFLAGS="-std=c99 -DHAVE_ANONYMOUS_INLINE_AGGREGATES=1 -Os -Wall -Wextra -I./include -isystem $WOLFSSL_DIR/include" \
LDFLAGS="-L$WOLFSSL_DIR/lib -lwolfssl"
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
tests/test_wolfcose
tools/wolfcose_tool
examples/lifecycle_demo
tests/interop/t_cose/interop_tcose
tests/interop/t_cose/QCBOR/
tests/interop/t_cose/t_cose/

# Editor / OS
*.swp
Expand Down
24 changes: 23 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ SCEN_IOTFLEET = examples/scenarios/iot_fleet_config
SCEN_SENSOR = examples/scenarios/sensor_attestation
SCEN_BROADCAST = examples/scenarios/group_broadcast_mac

.PHONY: all shared test coverage tool tool-test cmdline-test demo demos comprehensive scenarios c99-check clean
.PHONY: all shared test coverage tool tool-test cmdline-test demo demos comprehensive scenarios interop-tcose c99-check clean

# --- Core library ---
all: $(LIB_A)
Expand Down Expand Up @@ -164,6 +164,27 @@ scenarios: $(LIB_A)
./$(SCEN_BROADCAST) || exit 1
@echo "=== All scenario examples passed ==="

# --- t_cose wire-interop (t_cose on OpenSSL; t_cose + QCBOR fetched at pinned SHAs) ---
# The harness TU never includes OpenSSL headers (they collide with wolfSSL on
# SHA256 etc.); the t_cose-side key loader is a separate TU. CI overrides
# TCOSE_CRYPTO_INC / TCOSE_CRYPTO_LIB per platform (system libssl on Linux).
TCOSE_DIR ?= $(HOME)/interop-deps/t_cose
QCBOR_DIR ?= $(HOME)/interop-deps/QCBOR
TCOSE_CRYPTO_INC ?=
TCOSE_CRYPTO_LIB ?= -lcrypto
INTEROP_DIR = tests/interop/t_cose
INTEROP_BIN = $(INTEROP_DIR)/interop_tcose
INTEROP_CFLAGS = $(CFLAGS) -std=c99 -I$(TCOSE_DIR)/inc -I$(QCBOR_DIR)/inc

interop-tcose: $(LIB_A)
$(CC) $(INTEROP_CFLAGS) -DT_COSE_USE_OPENSSL_CRYPTO -c $(INTEROP_DIR)/interop_tcose.c -o $(INTEROP_DIR)/interop_tcose.o
$(CC) -std=c99 -Wall -Wextra -I$(TCOSE_DIR)/inc -I$(QCBOR_DIR)/inc $(TCOSE_CRYPTO_INC) \
-c $(INTEROP_DIR)/interop_key_ossl.c -o $(INTEROP_DIR)/interop_key.o
$(CC) -o $(INTEROP_BIN) $(INTEROP_DIR)/interop_tcose.o $(INTEROP_DIR)/interop_key.o \
$(LIB_A) $(TCOSE_DIR)/libt_cose.a $(QCBOR_DIR)/libqcbor.a \
$(TCOSE_CRYPTO_LIB) $(LDFLAGS) -lm
./$(INTEROP_BIN)

# --- C99 conformance gate ---
# Compiles every translation unit (core, tests, tool, examples) under strict
# ISO C99 with -pedantic-errors -Werror so any non-C99 construct fails the
Expand Down Expand Up @@ -195,5 +216,6 @@ clean:
rm -f $(OBJ) $(TEST_BIN) $(TOOL_BIN) $(DEMO_BIN) $(ENC_DEMO) $(MAC_DEMO) \
$(SIGN1_DEMO) $(COMP_SIGN) $(COMP_ENCRYPT) $(COMP_MAC) $(COMP_ERRORS) \
$(SCEN_FIRMWARE) $(SCEN_MULTIPARTY) $(SCEN_IOTFLEET) $(SCEN_SENSOR) $(SCEN_BROADCAST) \
$(INTEROP_DIR)/*.o $(INTEROP_DIR)/*.su $(INTEROP_BIN) \
$(LIB_A) $(LIB_SO) src/*.su tests/*.su examples/comprehensive/*.su examples/scenarios/*.su \
src/*.gcno src/*.gcda tests/*.gcno tests/*.gcda *.gcov
20 changes: 20 additions & 0 deletions docs/Testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,26 @@ Runs real-world scenario examples:
- Sensor attestation
- Group broadcast MAC

### Interoperability (t_cose)

```bash
make interop-tcose \
TCOSE_DIR=/path/to/t_cose QCBOR_DIR=/path/to/QCBOR \
TCOSE_CRYPTO_LIB="-lcrypto"
```

Proves RFC 9052 wire interoperability between wolfCOSE (on wolfCrypt) and
[t_cose](https://github.com/laurencelundblade/t_cose) (on OpenSSL): each library
produces COSE messages the other consumes, both directions, across every
algorithm both implement — ES256/384/512, PS256/384/512, EdDSA (Ed25519, Ed448),
HMAC 256/384/512, and AES-GCM 128/192/256. The bytes on the wire are the only
interface; the two APIs are never reconciled. Each primitive class also exercises
a tamper case that wolfCOSE must reject.

t_cose and QCBOR are BSD-3-Clause and are not vendored; the
[Interop CI job](../.github/workflows/interop.yml) fetches them at pinned
SHAs. See `tests/interop/t_cose/README.md` for the fixed test-key provenance.

---

## Code Coverage
Expand Down
91 changes: 91 additions & 0 deletions tests/interop/t_cose/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# wolfCOSE ⇄ t_cose Interop

A wire-interoperability conformance suite: wolfCOSE (on wolfCrypt) and
[t_cose](https://github.com/laurencelundblade/t_cose) (on OpenSSL) each produce
COSE messages the other consumes, in both directions, for every algorithm both
implement. The COSE bytes on the wire are the only interface — the two libraries'
APIs are never reconciled. This demonstrates RFC 9052/9053 conformance, not a
performance or feature comparison.

## What is covered

| Message | Algorithms (both directions) |
|-------------|------------------------------------------------------|
| COSE_Sign1 | ES256/384/512, PS256/384/512, EdDSA (Ed25519, Ed448) |
| COSE_Mac0 | HMAC 256/384/512 |
| COSE_Encrypt0 | AES-GCM 128/192/256 |

Each primitive class also runs a negative case — a tampered signature, MAC tag,
or AEAD tag — that wolfCOSE must reject.

CBOR byte-for-byte equality is an explicit non-goal: CBOR permits multiple valid
encodings, so the suite verifies that each side *reconstructs and validates* the
other's output, never that the two producers emit identical bytes.

## Dependencies (not vendored)

t_cose and QCBOR are BSD-3-Clause. They are **not** redistributed here; CI fetches
them at pinned SHAs and links them:

- t_cose `dev` @ `ff4c5f7c6fbbe27bb582214ff1878bf58ebc6c43`
- QCBOR @ `930708bb86481e88879eb1d87fd4d664f1d69503`

wolfCOSE itself stays zero-allocation; this harness is test-only.

## Building locally

Fetch the pinned SHAs (same sequence CI uses) and build, from this directory:

```bash
# QCBOR @ pinned SHA
mkdir -p QCBOR && cd QCBOR && git init -q && \
git fetch --depth 1 -q https://github.com/laurencelundblade/QCBOR.git \
930708bb86481e88879eb1d87fd4d664f1d69503 && \
git checkout -q FETCH_HEAD && make libqcbor.a && cd ..

# t_cose @ pinned SHA (OpenSSL adapter)
mkdir -p t_cose && cd t_cose && git init -q && \
git fetch --depth 1 -q https://github.com/laurencelundblade/t_cose.git \
ff4c5f7c6fbbe27bb582214ff1878bf58ebc6c43 && \
git checkout -q FETCH_HEAD && \
make -f Makefile.ossl libt_cose.a \
QCBOR_INC="-I ../QCBOR/inc" QCBOR_LIB="../QCBOR/libqcbor.a" && cd ..

# the suite (from the repo root)
make interop-tcose \
TCOSE_DIR=$PWD/tests/interop/t_cose/t_cose \
QCBOR_DIR=$PWD/tests/interop/t_cose/QCBOR \
TCOSE_CRYPTO_LIB="-lcrypto"
```

## Files

| File | Role |
|-----------------------|---------------------------------------------------------------|
| `interop_tcose.c` | Table-driven harness; wolfCrypt side; both directions per case |
| `interop_key_ossl.c` | t_cose-side OpenSSL key loader (isolated TU — wolfSSL and OpenSSL headers collide on `SHA256`) |
| `interop_cases.h` | Shared key-id enum so a key means the same on both sides |
| `interop_keys.h` | Fixed DER/symmetric test-key fixtures (provenance below) |

## Test-key provenance (fixtures, not production keys)

Committed for determinism. Generated once with OpenSSL:

```bash
# EC private keys (SEC1/RFC 5915 DER)
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -outform DER
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-384 -outform DER
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-521 -outform DER

# EdDSA private keys (PKCS#8 DER)
openssl genpkey -algorithm ED25519 -outform DER
openssl genpkey -algorithm ED448 -outform DER

# RSA-2048 private key (PKCS#1 DER)
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 | \
openssl rsa -outform DER -traditional
```

OpenSSL loads all of these via `d2i_AutoPrivateKey`; wolfCrypt via the matching
`wc_{Ecc,Ed25519,Ed448,Rsa}PrivateKeyDecode`. Symmetric keys are fixed bytes,
loaded backend-agnostically through `t_cose_key_init_symmetric`.
23 changes: 23 additions & 0 deletions tests/interop/t_cose/interop_cases.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/* interop_cases.h — shared identifiers for the wolfCOSE<->t_cose interop matrix.
* Both the wolfCrypt-side harness and the t_cose-side key loader include this so
* a key id means the same thing on both sides. Test-only.
*/
#ifndef WOLFCOSE_INTEROP_CASES_H
#define WOLFCOSE_INTEROP_CASES_H

/* Which fixed key fixture a case uses (see interop_keys.h). */
enum it_key {
IT_KEY_P256 = 0,
IT_KEY_P384,
IT_KEY_P521,
IT_KEY_ED25519,
IT_KEY_ED448,
IT_KEY_RSA2048,
IT_KEY_SYM16,
IT_KEY_SYM24,
IT_KEY_SYM32,
IT_KEY_SYM48,
IT_KEY_SYM64
};

#endif /* WOLFCOSE_INTEROP_CASES_H */
53 changes: 53 additions & 0 deletions tests/interop/t_cose/interop_key_ossl.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/* interop_key_ossl.c — t_cose-side asymmetric key loader, OpenSSL backend (test-only).
*
* Isolated from the wolfCrypt side: this TU includes OpenSSL + t_cose's
* backend-agnostic key header ONLY (no wolfSSL), so the two crypto stacks never
* collide in one translation unit (they both define SHA256 etc.). Loads the same
* shared DER bytes the wolfCrypt side uses, via d2i_AutoPrivateKey (SEC1, PKCS#1
* and PKCS#8 are all auto-detected). Symmetric keys are handled in the harness
* via the backend-agnostic t_cose_key_init_symmetric().
*
* Copyright (C) 2026 wolfSSL Inc. GPL-3.0-or-later (see wolfCOSE LICENSE).
*/

#include "t_cose/t_cose_key.h"
#include <openssl/evp.h>
#include <openssl/x509.h>

#include "interop_cases.h"
#include "interop_keys.h"

struct t_cose_key interop_tcose_load(int key_id);
void interop_tcose_free(struct t_cose_key key);

static const unsigned char* der_for(int key_id, long* len)
{
switch (key_id) {
case IT_KEY_P256: *len = (long)p256_der_len; return p256_der;
case IT_KEY_P384: *len = (long)p384_der_len; return p384_der;
case IT_KEY_P521: *len = (long)p521_der_len; return p521_der;
case IT_KEY_ED25519: *len = (long)ed25519_der_len; return ed25519_der;
case IT_KEY_ED448: *len = (long)ed448_der_len; return ed448_der;
case IT_KEY_RSA2048: *len = (long)rsa2048_der_len; return rsa2048_der;
default: *len = 0; return NULL;
}
}

struct t_cose_key interop_tcose_load(int key_id)
{
struct t_cose_key k;
long len = 0;
const unsigned char* der = der_for(key_id, &len);
const unsigned char* p = der;

k.key.ptr = NULL;
if (der != NULL) {
k.key.ptr = d2i_AutoPrivateKey(NULL, &p, len);
}
return k;
}

void interop_tcose_free(struct t_cose_key key)
{
EVP_PKEY_free((EVP_PKEY*)key.key.ptr);
}
Loading
Loading