Skip to content
Merged
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
4 changes: 4 additions & 0 deletions .github/workflows/c99-compliance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ jobs:
run: |
make c99-check CC=${{ matrix.cc }} WOLFSSL_INC=$HOME/wolfssl-install/include

- name: Countersignature configuration gate
run: |
make countersign-config-test CC=${{ matrix.cc }} WOLFSSL_INC=$HOME/wolfssl-install/include

- name: Experimental-feature acknowledgement gate
run: |
make experimental-check CC=${{ matrix.cc }} WOLFSSL_INC=$HOME/wolfssl-install/include
49 changes: 48 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ SRC = src/wolfcose_cbor.c \
src/wolfcose_recipient.c \
src/wolfcose_sign1.c \
src/wolfcose_sign.c \
src/wolfcose_countersign.c \
src/wolfcose_encrypt0.c \
src/wolfcose_mac0.c \
src/wolfcose_encrypt.c \
Expand Down Expand Up @@ -120,7 +121,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 pkg-config-test ecdsa-policy-test rsapss-policy-test zero-alloc-check zeroize-test ecc-import-policy-test ext-sign-test ext-sign-demo ext-sign-force-failure coverage tool tool-test cmdline-test demo demos lean-verify mldsa-demo mldsa-verify lms-demo lms-verify comprehensive scenarios interop-tcose tcose-upstream interop-go-cose interop-python-cwt interop-rust-coset c99-check c99-check-lms experimental-check clean FORCE
.PHONY: all shared test pkg-config-test ecdsa-policy-test rsapss-policy-test countersign-config-test zero-alloc-check zeroize-test ecc-import-policy-test ext-sign-test ext-sign-demo ext-sign-force-failure coverage tool tool-test cmdline-test demo demos lean-verify mldsa-demo mldsa-verify lms-demo lms-verify comprehensive scenarios interop-tcose tcose-upstream interop-go-cose interop-python-cwt interop-rust-coset c99-check c99-check-lms experimental-check clean FORCE

# --- Core library ---
all: $(LIB_A)
Expand Down Expand Up @@ -315,6 +316,52 @@ rsapss-policy-test:
done
@echo "PASS: RSA-PSS operation guards compile cleanly"

# Countersignatures decode their target and encode a Countersign_structure in
# both directions. Verify the independent operation guards and CBOR dependency.
COUNTERSIGN_CONFIG_FLAGS = $(C99_FLAGS) -Werror=unused-function \
-Werror=implicit-function-declaration -DWOLFCOSE_LEAN \
-DWOLFCOSE_ENABLE_COUNTERSIGN -DWOLFCOSE_NO_SIGN1 \
-DWOLFCOSE_NO_ENCRYPT0 -DWOLFCOSE_NO_MAC0 \
-DWOLFCOSE_NO_KEY_ENCODE -DWOLFCOSE_NO_KEY_DECODE

countersign-config-test:
@set -e; \
log_file=$$(mktemp "$${TMPDIR:-/tmp}/wolfcose-countersign.XXXXXX"); \
trap 'rm -f "$$log_file"' 0 1 2 3 15; \
$(CC) $(COUNTERSIGN_CONFIG_FLAGS) -DWOLFCOSE_NO_COUNTERSIGN_SIGN \
-fsyntax-only $(SRC); \
$(CC) $(COUNTERSIGN_CONFIG_FLAGS) -DWOLFCOSE_NO_COUNTERSIGN_VERIFY \
-fsyntax-only $(SRC); \
$(CC) $(COUNTERSIGN_CONFIG_FLAGS) -DWOLFCOSE_NO_COUNTERSIGN_SIGN \
-DWOLFCOSE_NO_COUNTERSIGN_VERIFY -fsyntax-only $(SRC); \
if $(CC) $(COUNTERSIGN_CONFIG_FLAGS) -DWOLFCOSE_NO_COUNTERSIGN_SIGN \
-DWOLFCOSE_NO_CBOR_ENCODE -fsyntax-only $(SRC) \
>"$$log_file" 2>&1; then \
echo "FAIL: countersign verify compiled without CBOR encode"; \
exit 1; \
fi; \
grep -q "WOLFCOSE_NO_CBOR_ENCODE conflicts" "$$log_file"; \
if $(CC) $(COUNTERSIGN_CONFIG_FLAGS) -DWOLFCOSE_NO_COUNTERSIGN_VERIFY \
-DWOLFCOSE_NO_CBOR_DECODE -fsyntax-only $(SRC) \
>"$$log_file" 2>&1; then \
echo "FAIL: countersign creation compiled without CBOR decode"; \
exit 1; \
fi; \
grep -q "WOLFCOSE_NO_CBOR_DECODE conflicts" "$$log_file"; \
if $(CC) $(COUNTERSIGN_CONFIG_FLAGS) -DWOLFCOSE_ENABLE_RSAPSS \
-DWOLFCOSE_MAX_SCRATCH_SZ=512u -fsyntax-only \
src/wolfcose_countersign.c >"$$log_file" 2>&1; then \
echo "FAIL: RSA-PSS countersigning compiled with no structure room"; \
exit 1; \
fi; \
grep -q "countersigning needs WOLFCOSE_MAX_SCRATCH_SZ" "$$log_file"; \
printf '%s\n' '#include "wolfcose/wolfcose.h"' \
'#if WOLFCOSE_MAX_SCRATCH_SZ != 512u' \
'#error "RSA-PSS without countersigning must keep the 512-byte scratch default"' \
'#endif' | $(CC) $(C99_FLAGS) -DWOLFCOSE_LEAN -DWOLFCOSE_ENABLE_RSAPSS \
-DWOLFCOSE_NO_COUNTERSIGN -x c -fsyntax-only -
@echo "PASS: countersignature operation guards compile cleanly"

zero-alloc-check:
sh scripts/check_zero_alloc.sh

Expand Down
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
# wolfCOSE

wolfCOSE is a lightweight C library implementing [CBOR (RFC 8949)](https://www.rfc-editor.org/rfc/rfc8949), [COSE (RFC 9052/9053)](https://www.rfc-editor.org/rfc/rfc9052), and post-quantum [ML-DSA for COSE (RFC 9964)](https://www.rfc-editor.org/rfc/rfc9964) and [HSS/LMS for COSE (RFC 8778)](https://www.rfc-editor.org/rfc/rfc8778) using [wolfSSL](https://www.wolfssl.com/) as the crypto backend.
wolfCOSE is a lightweight C library implementing [CBOR (RFC 8949)](https://www.rfc-editor.org/rfc/rfc8949), [COSE (RFC 9052/9053)](https://www.rfc-editor.org/rfc/rfc9052), [COSE countersignatures (RFC 9338)](https://www.rfc-editor.org/rfc/rfc9338), and post-quantum [ML-DSA for COSE (RFC 9964)](https://www.rfc-editor.org/rfc/rfc9964) and [HSS/LMS for COSE (RFC 8778)](https://www.rfc-editor.org/rfc/rfc8778) using [wolfSSL](https://www.wolfssl.com/) as the crypto backend.

## Main Features

- **Complete RFC 9052 message set**: all six COSE message types, including multi-signer
`COSE_Sign` and multi-recipient `COSE_Encrypt` / `COSE_Mac`
- **[RFC 9338 countersignature support](https://www.rfc-editor.org/rfc/rfc9338)**:
standards-conformant full and abbreviated V2 countersignatures for all six
tagged COSE message types, with multiple countersigners and in-place
operation
- **Post-quantum signing**: ML-DSA (FIPS 204) at all three security levels, with RFC 9964 `COSE_Key` (AKP key type, seed-based private keys)
- **Stateful hash-based signing**: HSS/LMS (RFC 8778, NIST SP 800-208) with `COSE_Key` type 5 — the CNSA 2.0 algorithm for firmware and software signing, verify-only friendly for constrained devices
- **41 algorithms** across signing, encryption, MAC, and key distribution
Expand Down Expand Up @@ -44,6 +48,11 @@ wolfCOSE has implemented all RFC 9052 messages both single-actor and multi-actor
| `COSE_Mac` | Sec. 6.1 | `wc_CoseMac_Create` / `wc_CoseMac_Verify` | **Multi-recipient** MAC (shared MAC key, distributed to recipients) |
| `COSE_Key` / `COSE_KeySet` | Sec. 7 | `wc_CoseKey_Encode` / `wc_CoseKey_Decode` | Key serialization for all key types |

RFC 9338 countersignatures can be attached to any tagged message in this
table. Use `wc_Cose_AddCounterSignature()` or
`wc_Cose_AddCounterSignature0()` to add one, then verify it independently with
the corresponding `wc_Cose_VerifyCounterSignature*()` API.

## Prerequisites (wolfSSL)

wolfCOSE requires [wolfSSL](https://www.wolfssl.com/) as its crypto backend.
Expand Down Expand Up @@ -191,7 +200,7 @@ make coverage-force-failure # Include crypto failure path testing
Full documentation is available in the [Wiki](https://github.com/wolfSSL/wolfCOSE/wiki):

- [Getting Started](https://github.com/wolfSSL/wolfCOSE/wiki/Getting-Started): Build instructions and first steps
- [Message Types](https://github.com/wolfSSL/wolfCOSE/wiki/Message-Types): All six RFC 9052 messages (Sign1, Sign, Encrypt0, Encrypt, Mac0, Mac) with code samples
- [Message Types](https://github.com/wolfSSL/wolfCOSE/wiki/Message-Types): All six RFC 9052 messages (Sign1, Sign, Encrypt0, Encrypt, Mac0, Mac) and RFC 9338 countersignatures with code samples
- [Algorithms](https://github.com/wolfSSL/wolfCOSE/wiki/Algorithms): Complete list of 41 supported algorithms with COSE IDs
- [API Reference](https://github.com/wolfSSL/wolfCOSE/wiki/API-Reference): Function signatures, data structures, error codes
- [Macros](https://github.com/wolfSSL/wolfCOSE/wiki/Macros): Compile-time configuration, size tuning, and ECDSA nonce policy
Expand Down
135 changes: 130 additions & 5 deletions docs/API-Reference.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
# API Reference

Complete API documentation for wolfCOSE (RFC 9052/9053 COSE implementation).
Complete API documentation for wolfCOSE (RFC 9052/9053 COSE and RFC 9338
countersignature implementation).

## Table of Contents

- [Data Structures](#data-structures)
- [COSE_Key API](#cose_key-api)
- [COSE_Sign1 API](#cose_sign1-api)
- [COSE Countersignature API](#cose-countersignature-api)
- [COSE_Encrypt0 API](#cose_encrypt0-api)
- [COSE_Mac0 API](#cose_mac0-api)
- [COSE_Sign API (Multi-Signer)](#cose_sign-api-multi-signer)
Expand Down Expand Up @@ -89,6 +91,28 @@ Signer information for COSE_Sign multi-signer messages.

---

### WOLFCOSE_COUNTERSIGNATURE

```c
typedef struct WOLFCOSE_COUNTERSIGNATURE {
int32_t algId;
WOLFCOSE_KEY* key;
const uint8_t* kid;
size_t kidLen;
} WOLFCOSE_COUNTERSIGNATURE;

typedef struct WOLFCOSE_COUNTERSIGNATURE0 {
int32_t algId;
WOLFCOSE_KEY* key;
} WOLFCOSE_COUNTERSIGNATURE0;
```

The full form carries protected algorithm metadata and an optional key ID.
The abbreviated form carries only signature bytes, so its algorithm and key
selection must be supplied by trusted application context.

---

### WOLFCOSE_RECIPIENT

```c
Expand Down Expand Up @@ -691,6 +715,100 @@ Verify a COSE_Sign1 message and extract payload.

---

## COSE Countersignature API

These RFC 9338 APIs operate on tagged `COSE_Sign1`, `COSE_Sign`,
`COSE_Encrypt0`, `COSE_Encrypt`, `COSE_Mac0`, and `COSE_Mac` messages. A
countersignature authenticates the target message's protected header, payload,
and existing cryptographic output where applicable.

Countersignatures support ECDSA, EdDSA, RSA-PSS, and ML-DSA.
`WOLFCOSE_ALG_HSS_LMS` is rejected with `WOLFCOSE_E_COSE_BAD_ALG` before any
signer runs, and a build whose only signature algorithm is HSS/LMS does not
enable the feature.

### wc_Cose_AddCounterSignature

```c
int wc_Cose_AddCounterSignature(
const WOLFCOSE_COUNTERSIGNATURE* counterSigner,
const uint8_t* in, size_t inSz,
const uint8_t* detachedPayload, size_t detachedLen,
const uint8_t* extAad, size_t extAadLen,
uint8_t* scratch, size_t scratchSz,
uint8_t* out, size_t outSz, size_t* outLen,
WC_RNG* rng
);
```

Add a full V2 countersignature in unprotected header parameter 11. Existing
full countersignatures are retained and the new value is appended. `out` may
equal `in` for exact in-place growth; other overlapping buffers are rejected.
`scratch` must be disjoint from all input and output buffers.

### wc_Cose_AddCounterSignature0

```c
int wc_Cose_AddCounterSignature0(
const WOLFCOSE_COUNTERSIGNATURE0* counterSigner,
const uint8_t* in, size_t inSz,
const uint8_t* detachedPayload, size_t detachedLen,
const uint8_t* extAad, size_t extAadLen,
uint8_t* scratch, size_t scratchSz,
uint8_t* out, size_t outSz, size_t* outLen,
WC_RNG* rng
);
```

Add one abbreviated V2 countersignature in unprotected header parameter 12.
Only one abbreviated countersignature is allowed per target. `scratch` must be
disjoint from all input and output buffers.

### wc_Cose_VerifyCounterSignature

```c
int wc_Cose_VerifyCounterSignature(
const WOLFCOSE_KEY* key, size_t counterIndex,
const uint8_t* in, size_t inSz,
const uint8_t* detachedPayload, size_t detachedLen,
const uint8_t* extAad, size_t extAadLen,
uint8_t* scratch, size_t scratchSz,
WOLFCOSE_HDR* counterHdr
);
```

Verify a full countersignature selected by zero-based index and return its
parsed headers. V2 label 11 and legacy RFC 8152 label 7 are accepted. V2 is
preferred when both are present. If the countersignature algorithm is carried
only in the unprotected header bucket, `key->alg` must pin the same algorithm;
an unset or mismatched key policy is rejected. `scratch` must be disjoint from
the message, detached payload, external AAD, and `counterHdr`.

### wc_Cose_VerifyCounterSignature0

```c
int wc_Cose_VerifyCounterSignature0(
const WOLFCOSE_COUNTERSIGNATURE0* counterSigner,
const uint8_t* in, size_t inSz,
const uint8_t* detachedPayload, size_t detachedLen,
const uint8_t* extAad, size_t extAadLen,
uint8_t* scratch, size_t scratchSz
);
```

Verify an abbreviated countersignature using the algorithm and key supplied by
the application. V2 label 12 and legacy RFC 8152 label 9 are accepted.
`scratch` must be disjoint from the message, detached payload, and external
AAD.

All four APIs require the detached payload or ciphertext, when used by the
target, to match the original operation. Verification must also receive the
same countersignature external AAD that was supplied when the countersignature
was created; it is independent of any AAD the target operation used. They
return `WOLFCOSE_SUCCESS` or a negative wolfCOSE error code.

---

## COSE_Encrypt0 API

### wc_CoseEncrypt0_Encrypt
Expand Down Expand Up @@ -1170,6 +1288,11 @@ unsupported item must be skipped.
A parser can therefore write the dispatch once instead of duplicating a
`wc_CBOR_PeekType()` branch at every map.

Major type 3 validates UTF-8 and, when valid, fills `text`/`textLen` with
`isText == 1` and no copy. Invalid UTF-8 returns
`WOLFCOSE_E_CBOR_MALFORMED`; any other major type returns
`WOLFCOSE_E_CBOR_TYPE`.

`wc_CBOR_LabelIsInt()` and `wc_CBOR_LabelIsText()` return 1 on match and 0
otherwise, including for a `NULL` label. Text comparison is byte-exact: no
Unicode normalization or case folding, matching how CTAP2 and COSE compare
Expand All @@ -1188,10 +1311,12 @@ else {
}
```

Note that `wc_CoseKey_Decode()` and the COSE header parsers accept integer
labels only, by design: silently skipping text labels would break their
duplicate-label enforcement. `wc_CBOR_DecodeLabel()` is for caller-written
parsers of protocol maps such as CTAP2.
COSE header parsers accept both integer and text labels and enforce duplicate
labels within and across the protected and unprotected buckets. Unknown,
non-critical text-labeled parameters are preserved in the encoded message and
ignored during processing. `wc_CoseKey_Decode()` accepts the integer labels
defined for COSE_Key. `wc_CBOR_DecodeLabel()` is also available for
caller-written protocol parsers such as CTAP2.

**Returns:** `WOLFCOSE_SUCCESS` or error code

Expand Down
6 changes: 4 additions & 2 deletions docs/Getting-Started.md
Original file line number Diff line number Diff line change
Expand Up @@ -391,8 +391,10 @@ Related strictness that surprises integrators for the same reason:
- EC2 coordinates must be exactly the curve size, with leading zeros preserved
(RFC 9053 Section 7.1.1) - a 31-byte P-256 `x` is rejected, not left-padded.
- A duplicate label in a header or `COSE_Key` map is rejected.
- `COSE_Key` and COSE header maps accept integer labels only. For your own
protocol maps that mix integer and text labels, use
- `COSE_Key` maps accept the registered integer labels. COSE header maps accept
both integer and text labels, retain unknown non-critical parameters in the
encoded message, and reject duplicates within or across header buckets. For
caller-written protocol maps, use
[`wc_CBOR_DecodeLabel()`](API-Reference.md#wc_cbor_decodelabel).

None of this is configurable: relaxing it would let a signature or MAC be
Expand Down
26 changes: 23 additions & 3 deletions docs/Macros.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Defining `WOLFCOSE_LEAN` keeps only the core — `COSE_Sign1`/`Encrypt0`/`Mac0`
| `WOLFCOSE_LEAN` | Core-only base; all extensions become opt-in |
| `WOLFCOSE_ENABLE_<X>` | Opt in a single extension (see list below) |

Extension names for `WOLFCOSE_ENABLE_<X>`: `ES384`, `ES512`, `EDDSA`, `ED448`, `RSAPSS`, `MLDSA`, `LMS`, `HMAC384`, `HMAC512`, `AESCCM`, `CHACHA20`, `AESMAC`, `AESWRAP`, `ECDH_ES`, `SIGN` (multi-signer), `ENCRYPT` (multi-recipient), `MAC` (multi-recipient).
Extension names for `WOLFCOSE_ENABLE_<X>`: `ES384`, `ES512`, `EDDSA`, `ED448`, `RSAPSS`, `MLDSA`, `LMS`, `HMAC384`, `HMAC512`, `AESCCM`, `CHACHA20`, `AESMAC`, `AESWRAP`, `ECDH_ES`, `SIGN` (multi-signer), `ENCRYPT` (multi-recipient), `MAC` (multi-recipient), and `COUNTERSIGN` (RFC 9338 countersignatures).

An extension is compiled in when it is explicitly enabled (`WOLFCOSE_ENABLE_<X>`), or — in a non-lean build — when wolfSSL provides the primitive and it is not opted out with `WOLFCOSE_NO_<X>`. Enabling an extension wolfSSL cannot provide is a compile error. The resolved state is exposed internally as read-only `WOLFCOSE_HAVE_<X>` gates (e.g. `WOLFCOSE_HAVE_MLDSA`); sources, tests, and examples compile against those, so you set `WOLFCOSE_ENABLE_*`/`WOLFCOSE_NO_*`, not `WOLFCOSE_HAVE_*`.

Expand Down Expand Up @@ -132,6 +132,24 @@ off by default.

## Message Type Gates

### COSE Countersignatures

| Define | Description | Default |
|--------|-------------|---------|
| `WOLFCOSE_COUNTERSIGN` | Enable RFC 9338 countersignatures | Enabled |
| `WOLFCOSE_ENABLE_COUNTERSIGN` | Opt in under `WOLFCOSE_LEAN` | - |
| `WOLFCOSE_NO_COUNTERSIGN` | Disable countersignatures entirely | - |
| `WOLFCOSE_COUNTERSIGN_SIGN` | Enable countersignature creation | Enabled |
| `WOLFCOSE_NO_COUNTERSIGN_SIGN` | Disable countersignature creation | - |
| `WOLFCOSE_COUNTERSIGN_VERIFY` | Enable countersignature verification | Enabled |
| `WOLFCOSE_NO_COUNTERSIGN_VERIFY` | Disable countersignature verification | - |

The default full build includes countersignatures. `WOLFCOSE_LEAN` excludes
them unless `WOLFCOSE_ENABLE_COUNTERSIGN` is defined. Creation and verification
both require CBOR encoding and decoding because each operation decodes the
target message and encodes its `Countersign_structure`. Creation also requires
a signing algorithm, and verification requires a verification algorithm.

### COSE_Sign1 (Single Signer)

| Define | Description | Default |
Expand Down Expand Up @@ -275,7 +293,7 @@ Two limits worth knowing before designing around this:

| Define | Description | Default |
|--------|-------------|---------|
| `WOLFCOSE_MAX_SCRATCH_SZ` | Scratch buffer size for Sig_structure/Enc_structure | 512 |
| `WOLFCOSE_MAX_SCRATCH_SZ` | Scratch buffer size for Sig_structure/Enc_structure | 512 (1024 with RSA-PSS countersigning) |
| `WOLFCOSE_PROTECTED_HDR_MAX` | Max protected header size | 64 |
| `WOLFCOSE_CBOR_MAX_DEPTH` | Max CBOR nesting depth | 8 |
| `WOLFCOSE_MIN_BUFFERS` | Trim the working set to the minimum that fits the enabled algorithms | - |
Expand All @@ -287,12 +305,14 @@ One define that trims the caller working set to the minimum that still fits the
| Enabled signature algorithm | `WOLFCOSE_MAX_SIG_SZ` | `WOLFCOSE_MAX_SCRATCH_SZ` |
|---|---|---|
| ES256/384/512, EdDSA (Ed25519/Ed448) | 132 | 512 |
| RSA-PSS (PS256/384/512) | 512 | 512 |
| RSA-PSS (PS256/384/512) | 512 | 512 (1024 with countersigning) |
| ML-DSA-44/65/87 | 4627 | 8192 |
| HSS-LMS | 10240 | 11264 |

Because the floor follows the algorithm, `WOLFCOSE_MIN_BUFFERS` stays valid with any algorithm — ML-DSA, HSS-LMS, and RSA-PSS simply use that algorithm's floor rather than the ECC floor. HSS-LMS uses the largest default floors because its signature size follows the key's parameter set rather than a fixed constant: the `WOLFCOSE_MAX_SIG_SZ` default of 10240 and `WOLFCOSE_MAX_SCRATCH_SZ` default of 11264 (scratch also holds the `Sig_structure`) cover every W4 and W8 predefined set, the largest being L4_H10_W4 at 10204 bytes. The low-Winternitz (W1/W2) multi-level sets reach 18012 bytes and need a `-D` override; a key whose signature exceeds these buffers is rejected via `wc_LmsKey_GetSigLen()` before any signing, so no one-time state is consumed. It stays zero-heap and shrinks buffers, not stack frames. An explicit `-D` override of any individual limit takes precedence.

**Countersignature sizing.** Countersigning builds the `Countersign_structure` and then keeps the signature after it in scratch, so `WOLFCOSE_MAX_SCRATCH_SZ` must be at least `WOLFCOSE_MAX_SIG_SZ` + 256. The RSA-PSS default therefore rises to 1024 (a 4096-bit signature is 512 bytes) only when countersignature creation is enabled; RSA-PSS builds without it keep 512. An override below the floor is a build error.

---

## Tuning for Size
Expand Down
Loading
Loading