From 998c3c32c55a0c955200978281cb1112f471342a Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Tue, 1 Sep 2026 15:17:00 -0700 Subject: [PATCH 01/11] Add experimental COSE HPKE support --- docs/Macros.md | 93 +++ include/wolfcose/settings.h | 92 +++ include/wolfcose/wolfcose.h | 90 ++- src/wolfcose.c | 1087 ++++++++++++++++++++++++++++++++--- tests/test_cose.c | 661 ++++++++++++++++++++- 5 files changed, 1950 insertions(+), 73 deletions(-) diff --git a/docs/Macros.md b/docs/Macros.md index 3d7575f..aa698f7 100644 --- a/docs/Macros.md +++ b/docs/Macros.md @@ -38,6 +38,10 @@ cc -DWOLFCOSE_ENABLE_EXPERIMENTAL_EXAMPLE ... **Graduation policy.** When a draft is published as an RFC, its `WOLFCOSE_EXPERIMENTAL` requirement is removed in a focused follow-up and the feature becomes an ordinary gate (default-on full build, `WOLFCOSE_ENABLE_` under `WOLFCOSE_LEAN`, `WOLFCOSE_NO_` to strip), following the [Algorithm Gates](#algorithm-gates) convention. +COSE-HPKE is currently an experimental feature. It stays off in every build and +requires the master acknowledgement plus the relevant operation gate. Its +specific gates are documented in [COSE-HPKE (experimental)](#cose-hpke-experimental). + ## Lean Configuration Layer Defining `WOLFCOSE_LEAN` keeps only the core — `COSE_Sign1`/`Encrypt0`/`Mac0` with ES256, AES-GCM, and HMAC-SHA256 — and turns every other algorithm into an opt-in. This is the recommended starting point for constrained targets. @@ -49,6 +53,9 @@ Defining `WOLFCOSE_LEAN` keeps only the core — `COSE_Sign1`/`Encrypt0`/`Mac0` Extension names for `WOLFCOSE_ENABLE_`: `ES384`, `ES512`, `EDDSA`, `ED448`, `RSAPSS`, `MLDSA`, `HMAC384`, `HMAC512`, `AESCCM`, `CHACHA20`, `AESMAC`, `AESWRAP`, `ECDH_ES`, `SIGN` (multi-signer), `ENCRYPT` (multi-recipient), `MAC` (multi-recipient). +COSE-HPKE uses its own explicit gates rather than this generic extension rule, +so it never becomes enabled merely because wolfSSL provides HPKE. + An extension is compiled in when it is explicitly enabled (`WOLFCOSE_ENABLE_`), or — in a non-lean build — when wolfSSL provides the primitive and it is not opted out with `WOLFCOSE_NO_`. Enabling an extension wolfSSL cannot provide is a compile error. The resolved state is exposed internally as read-only `WOLFCOSE_HAVE_` gates (e.g. `WOLFCOSE_HAVE_MLDSA`); sources, tests, and examples compile against those, so you set `WOLFCOSE_ENABLE_*`/`WOLFCOSE_NO_*`, not `WOLFCOSE_HAVE_*`. ## Algorithm Gates @@ -213,6 +220,91 @@ Resolved internally as read-only `WOLFCOSE_KEY_WRAP`, `WOLFCOSE_ECDH`, and `WOLF --- +## COSE-HPKE (experimental) + +COSE-HPKE implements the P0 subset of +[draft-ietf-cose-hpke-26](https://datatracker.ietf.org/doc/draft-ietf-cose-hpke/). +It is deliberately disabled by default in both full and lean builds because the +COSE binding and its IANA values are still an Internet-Draft. HPKE itself is +standardized by RFC 9180, but the COSE profile is not yet an RFC. + +P0 fixes every cryptographic choice: `DHKEM(P-256, HKDF-SHA256)`, +`HKDF-SHA256`, and `AES-128-GCM`, in HPKE base mode only. There are no hidden +P-384, P-521, X25519, PSK, authenticated, or alternate-AEAD paths to enable. +That keeps each enabled wire operation auditable and lets targets compile out +all unsupported suites. + +P0 is intended for confidential provisioning, configuration, and credential +delivery. It can encrypt a payload directly to one recipient, or encrypt the +content once and independently protect its content-encryption key for multiple +recipients. Base mode provides recipient-only confidentiality; pair the result +with a COSE signature or MAC when the sender must be authenticated. + +| Define | Description | Default | +|--------|-------------|---------| +| `WOLFCOSE_ENABLE_HPKE_0` | Enable both send and receive for single-recipient integrated `COSE_Encrypt0` HPKE-0 | off | +| `WOLFCOSE_ENABLE_HPKE_0_ENCRYPT` | Enable only integrated Encrypt0 send | off | +| `WOLFCOSE_ENABLE_HPKE_0_DECRYPT` | Enable only integrated Encrypt0 receive | off | +| `WOLFCOSE_ENABLE_HPKE_0_KE` | Enable both send and receive for multi-recipient `COSE_Encrypt` HPKE-0-KE | off | +| `WOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT` | Enable only multi-recipient HPKE key-encryption send | off | +| `WOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT` | Enable only multi-recipient HPKE key-encryption receive | off | +| `WOLFCOSE_NO_HPKE_0` | Prohibit all HPKE-0 enables; conflicts with any HPKE-0 enable | off | +| `WOLFCOSE_NO_HPKE_0_ENCRYPT` / `WOLFCOSE_NO_HPKE_0_DECRYPT` | Compile out the corresponding integrated Encrypt0 direction | off | +| `WOLFCOSE_NO_HPKE_0_KE_ENCRYPT` / `WOLFCOSE_NO_HPKE_0_KE_DECRYPT` | Compile out the corresponding multi-recipient key-encryption direction | off | + +The convenience gates respect their per-direction `NO_*` gates. Defining an +individual `ENABLE_*` and its matching `NO_*` is a compile error; defining +`WOLFCOSE_NO_HPKE_0` with any enable is also a compile error. + +An enabled HPKE operation requires wolfSSL `HAVE_HPKE`, `HAVE_ECC` with P-256, +SHA-256, and `HAVE_AESGCM`. Integrated mode also needs the matching +`COSE_Encrypt0` direction. Key-encryption mode additionally needs the matching +`COSE_Encrypt` direction and recipient support; under `WOLFCOSE_LEAN`, add +`WOLFCOSE_ENABLE_ENCRYPT` before enabling an HPKE-0-KE operation. + +Build the wolfSSL backend with HPKE, ECC P-256, AES-GCM, SHA-256, and key +generation. For example, a cryptography-only backend can use: + +```bash +./configure --enable-cryptonly --enable-hpke --enable-ecc --enable-aesgcm \ + --enable-keygen +``` + +The public identifiers are provisional draft values: `HPKE-0` is algorithm 35, +`HPKE-0-KE` is algorithm 46, and `ek` is header label -4. Applications must +treat them as draft values and update when the COSE-HPKE RFC or IANA registry +changes. P0 accepts only base mode: `psk_id`, a PSK, and external HPKE `info` +are intentionally not exposed. Application external AAD is still bound through +the normal COSE `Enc_structure` API argument. + +For integrated mode, call `wc_CoseHpkeEncrypt0_Encrypt()` and +`wc_CoseHpkeEncrypt0_Decrypt()`. It encrypts payloads directly for one +recipient. For key-encryption mode, use `wc_CoseEncrypt_Encrypt()` and +`wc_CoseEncrypt_Decrypt()` with every `WOLFCOSE_RECIPIENT.algId` set to +`WOLFCOSE_ALG_HPKE_0_KE`; wolfCOSE generates one CEK, encrypts the content +once, and HPKE-wraps that CEK separately for every recipient. HPKE base mode +does not authenticate the sender, so add a COSE signature or MAC when sender +authentication is required. + +```bash +# Single-recipient integrated HPKE: send and receive. +make EXTRA_CFLAGS="-DWOLFCOSE_ENABLE_HPKE_0" + +# Receive-only provisioning target: no HPKE sender code. +make EXTRA_CFLAGS="-DWOLFCOSE_ENABLE_HPKE_0_DECRYPT" + +# Multi-recipient provisioning server, including the lean COSE_Encrypt gate. +make EXTRA_CFLAGS="-DWOLFCOSE_LEAN -DWOLFCOSE_ENABLE_ENCRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT" +``` + +Draft -26's published Section 5.2 key-encryption sample encodes a 16-byte +`A128GCM` IV. RFC 9053 fixes the COSE AES-GCM nonce at 12 bytes, so wolfCOSE +rejects that malformed sample and requires a 12-byte IV for all COSE content +encryption. + +--- + ## Delegated Signing | Define | Description | Default | @@ -416,6 +508,7 @@ wolfCOSE requires these wolfSSL features for full functionality: | `WOLFSSL_SHA512` | SHA-512 for ES512, HMAC-512 | | `HAVE_AES_KEYWRAP` | AES Key Wrap distribution | | `HAVE_HKDF` | ECDH-ES key derivation | +| `HAVE_HPKE` + `HAVE_ECC` P-256 + SHA-256 + `HAVE_AESGCM` | Experimental COSE-HPKE P0 | --- diff --git a/include/wolfcose/settings.h b/include/wolfcose/settings.h index 8886e88..1102e6b 100644 --- a/include/wolfcose/settings.h +++ b/include/wolfcose/settings.h @@ -493,6 +493,98 @@ extern "C" { #endif #endif +/* ----- COSE-HPKE (draft-ietf-cose-hpke-26) ----- + * + * HPKE is deliberately opt-in in every build. The P0 implementation supports + * only the draft's HPKE-0 ciphersuite: DHKEM(P-256, HKDF-SHA256), + * HKDF-SHA256, and AES-128-GCM in base mode. Each wire operation has a + * separate enable gate so send-only and receive-only targets do not carry the + * other direction. The two convenience switches enable both directions for + * their respective COSE construction. + */ +#if defined(WOLFCOSE_ENABLE_HPKE_0) + #if defined(WOLFCOSE_NO_HPKE_0) + #error "WOLFCOSE_ENABLE_HPKE_0 conflicts with WOLFCOSE_NO_HPKE_0" + #endif + #if !defined(WOLFCOSE_NO_HPKE_0_ENCRYPT) + #define WOLFCOSE_ENABLE_HPKE_0_ENCRYPT + #endif + #if !defined(WOLFCOSE_NO_HPKE_0_DECRYPT) + #define WOLFCOSE_ENABLE_HPKE_0_DECRYPT + #endif +#endif + +#if defined(WOLFCOSE_ENABLE_HPKE_0_KE) + #if defined(WOLFCOSE_NO_HPKE_0) + #error "WOLFCOSE_ENABLE_HPKE_0_KE conflicts with WOLFCOSE_NO_HPKE_0" + #endif + #if !defined(WOLFCOSE_NO_HPKE_0_KE_ENCRYPT) + #define WOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT + #endif + #if !defined(WOLFCOSE_NO_HPKE_0_KE_DECRYPT) + #define WOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT + #endif +#endif + +#if defined(WOLFCOSE_NO_HPKE_0) && \ + (defined(WOLFCOSE_ENABLE_HPKE_0_ENCRYPT) || \ + defined(WOLFCOSE_ENABLE_HPKE_0_DECRYPT) || \ + defined(WOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT) || \ + defined(WOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT)) + #error "WOLFCOSE_NO_HPKE_0 conflicts with an HPKE-0 operation enable" +#endif + +#if (defined(WOLFCOSE_ENABLE_HPKE_0_ENCRYPT) && \ + defined(WOLFCOSE_NO_HPKE_0_ENCRYPT)) || \ + (defined(WOLFCOSE_ENABLE_HPKE_0_DECRYPT) && \ + defined(WOLFCOSE_NO_HPKE_0_DECRYPT)) || \ + (defined(WOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT) && \ + defined(WOLFCOSE_NO_HPKE_0_KE_ENCRYPT)) || \ + (defined(WOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT) && \ + defined(WOLFCOSE_NO_HPKE_0_KE_DECRYPT)) + #error "An HPKE-0 operation cannot be both enabled and disabled" +#endif + +#if defined(WOLFCOSE_ENABLE_HPKE_0_ENCRYPT) || \ + defined(WOLFCOSE_ENABLE_HPKE_0_DECRYPT) || \ + defined(WOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT) || \ + defined(WOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT) + #if !defined(HAVE_HPKE) || !defined(HAVE_ECC) || \ + !defined(HAVE_AESGCM) || defined(NO_SHA256) || \ + defined(NO_ECC256) + #error "HPKE-0 requires wolfSSL HAVE_HPKE, HAVE_ECC, P-256, SHA-256, and HAVE_AESGCM" + #endif + #define WOLFCOSE_HAVE_HPKE_0 +#endif + +#if defined(WOLFCOSE_ENABLE_HPKE_0_ENCRYPT) + #if !defined(WOLFCOSE_ENCRYPT0_ENCRYPT) + #error "WOLFCOSE_ENABLE_HPKE_0_ENCRYPT requires COSE_Encrypt0 encrypt" + #endif + #define WOLFCOSE_HPKE_0_ENCRYPT +#endif + +#if defined(WOLFCOSE_ENABLE_HPKE_0_DECRYPT) + #if !defined(WOLFCOSE_ENCRYPT0_DECRYPT) + #error "WOLFCOSE_ENABLE_HPKE_0_DECRYPT requires COSE_Encrypt0 decrypt" + #endif + #define WOLFCOSE_HPKE_0_DECRYPT +#endif + +#if defined(WOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT) + #if !defined(WOLFCOSE_ENCRYPT_ENCRYPT) || !defined(WOLFCOSE_RECIPIENTS) + #error "WOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT requires COSE_Encrypt recipients and encrypt" + #endif + #define WOLFCOSE_HPKE_0_KE_ENCRYPT +#endif + +#if defined(WOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT) + #if !defined(WOLFCOSE_ENCRYPT_DECRYPT) || !defined(WOLFCOSE_RECIPIENTS) + #error "WOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT requires COSE_Encrypt recipients and decrypt" + #endif + #define WOLFCOSE_HPKE_0_KE_DECRYPT +#endif + /* ----- COSE_Key serialization — core ----- */ #ifndef WOLFCOSE_NO_KEY_ENCODE #define WOLFCOSE_KEY_ENCODE diff --git a/include/wolfcose/wolfcose.h b/include/wolfcose/wolfcose.h index 20ab12e..9565dc0 100644 --- a/include/wolfcose/wolfcose.h +++ b/include/wolfcose/wolfcose.h @@ -145,6 +145,10 @@ extern "C" { #define WOLFCOSE_HDR_IV 5 #define WOLFCOSE_HDR_PARTIAL_IV 6 #define WOLFCOSE_HDR_EPHEMERAL_KEY (-1) /* Ephemeral COSE_Key for ECDH */ +#if defined(WOLFCOSE_HAVE_HPKE_0) + /* draft-ietf-cose-hpke-26, provisional label -4. */ + #define WOLFCOSE_HDR_HPKE_EK (-4) /* HPKE encapsulated key */ +#endif /* * Security considerations for the algorithms below: @@ -222,6 +226,12 @@ extern "C" { #define WOLFCOSE_ALG_ECDH_ES_A192KW (-30) /* ECDH-ES + A192KW */ #define WOLFCOSE_ALG_ECDH_ES_A256KW (-31) /* ECDH-ES + A256KW */ +#if defined(WOLFCOSE_HAVE_HPKE_0) +/* draft-ietf-cose-hpke-26 provisional COSE algorithm identifiers. */ +#define WOLFCOSE_ALG_HPKE_0 35 /* P-256, HKDF-SHA256, AES-128-GCM */ +#define WOLFCOSE_ALG_HPKE_0_KE 46 /* HPKE-0 key encryption */ +#endif + #define WOLFCOSE_ALG_ML_DSA_44 (-48) /* ML-DSA Level 2 */ #define WOLFCOSE_ALG_ML_DSA_65 (-49) /* ML-DSA Level 3 */ #define WOLFCOSE_ALG_ML_DSA_87 (-50) /* ML-DSA Level 5 */ @@ -318,6 +328,8 @@ typedef struct WOLFCOSE_HDR { size_t ivLen; /**< IV length */ const uint8_t* partialIv; /**< Partial IV pointer */ size_t partialIvLen; /**< Partial IV length */ + const uint8_t* hpkeEk; /**< HPKE encapsulated key pointer */ + size_t hpkeEkLen; /**< HPKE encapsulated key length */ int32_t contentType; /**< Content type from either header bucket */ uint8_t flags; /**< Header flags (see WOLFCOSE_HDR_FLAG_*) */ } WOLFCOSE_HDR; @@ -326,6 +338,10 @@ typedef struct WOLFCOSE_HDR { #define WOLFCOSE_HDR_FLAG_DETACHED 0x01u /** \brief Flag indicating an unprotected content-type label was present */ #define WOLFCOSE_HDR_FLAG_CONTENT_TYPE_UNPROTECTED 0x02u +#if defined(WOLFCOSE_HAVE_HPKE_0) +/** \brief Flag indicating an HPKE encapsulated key is present. */ +#define WOLFCOSE_HDR_FLAG_HPKE_EK 0x04u +#endif /** * \brief Caller-supplied signature callback (RFC 9052 Section 4.4). @@ -412,7 +428,7 @@ typedef struct WOLFCOSE_KEY { */ typedef struct WOLFCOSE_RECIPIENT { int32_t algId; /**< Key distribution algorithm; direct mode requires explicit WOLFCOSE_ALG_DIRECT (-6) on both encrypt and MAC create (-3..-31, -6) */ - WOLFCOSE_KEY* key; /**< Caller-owned key (KEK for wrap, recipient pubkey for ECDH) */ + WOLFCOSE_KEY* key; /**< Caller-owned key (KEK for wrap; recipient public/private key for ECDH or HPKE) */ const uint8_t* kid; /**< Key ID for recipient lookup */ size_t kidLen; /**< Key ID length */ } WOLFCOSE_RECIPIENT; @@ -1210,6 +1226,65 @@ WOLFCOSE_API int wc_CoseEncrypt0_Decrypt(const WOLFCOSE_KEY* key, uint8_t* plaintext, size_t plaintextSz, size_t* plaintextLen); #endif /* WOLFCOSE_ENCRYPT0_DECRYPT */ +/* ----- COSE-HPKE Integrated Encryption (draft-ietf-cose-hpke-26) ----- */ + +#if defined(WOLFCOSE_HPKE_0_ENCRYPT) +/** + * \brief Encrypt a payload with COSE HPKE-0 integrated encryption. + * + * Uses only HPKE base mode with DHKEM(P-256, HKDF-SHA256), HKDF-SHA256, + * and AES-128-GCM. The recipient key must be an EC2 P-256 public key and may + * be pinned to WOLFCOSE_ALG_HPKE_0. The encapsulated key is written as the + * draft's provisional ek header parameter (-4). HPKE info is empty and PSK + * mode is not supported by this P0 API. + * + * \param recipientKey Recipient EC2 P-256 public key. + * \param kid Optional recipient key identifier. + * \param kidLen Recipient key identifier length. + * \param payload Plaintext payload (non-NULL, may be empty). + * \param payloadLen Plaintext length. + * \param detachedPayload Detached ciphertext destination (NULL if attached). + * \param detachedSz Detached ciphertext destination size. + * \param detachedLen Output: detached ciphertext length. + * \param extAad External additional authenticated data (NULL if none). + * \param extAadLen External additional authenticated data length. + * \param scratch Working buffer for Enc_structure. + * \param scratchSz Working buffer size. + * \param out Output COSE_Encrypt0 buffer. + * \param outSz Output buffer size. + * \param outLen Output: bytes written. + * \param rng Initialized random number generator. + * \return WOLFCOSE_SUCCESS or a negative error code. + */ +WOLFCOSE_API int wc_CoseHpkeEncrypt0_Encrypt( + const WOLFCOSE_KEY* recipientKey, + const uint8_t* kid, size_t kidLen, + const uint8_t* payload, size_t payloadLen, + uint8_t* detachedPayload, size_t detachedSz, 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); +#endif /* WOLFCOSE_HPKE_0_ENCRYPT */ + +#if defined(WOLFCOSE_HPKE_0_DECRYPT) +/** + * \brief Decrypt a COSE HPKE-0 integrated encryption message. + * + * The recipient key must be an EC2 P-256 private key and may be pinned to + * WOLFCOSE_ALG_HPKE_0. Only HPKE base mode with empty HPKE info is accepted; + * PSK mode is rejected. + */ +WOLFCOSE_API int wc_CoseHpkeEncrypt0_Decrypt( + const WOLFCOSE_KEY* recipientKey, + const uint8_t* in, size_t inSz, + const uint8_t* detachedCt, size_t detachedCtLen, + const uint8_t* extAad, size_t extAadLen, + uint8_t* scratch, size_t scratchSz, + WOLFCOSE_HDR* hdr, + uint8_t* plaintext, size_t plaintextSz, size_t* plaintextLen); +#endif /* WOLFCOSE_HPKE_0_DECRYPT */ + /* ----- COSE_Mac0 API (RFC 9052 Section 6.2) ----- */ #if defined(WOLFCOSE_MAC0_CREATE) && (defined(WOLFCOSE_HAVE_HMAC) || defined(WOLFCOSE_HAVE_AESMAC)) @@ -1361,10 +1436,11 @@ WOLFCOSE_API int wc_CoseSign_Verify(const WOLFCOSE_KEY* verifyKey, * COSE_Encrypt = [Headers, ciphertext, recipients : [+ COSE_recipient]] * * Depending on build configuration, recipient key management supports direct, - * AES Key Wrap, and ECDH-ES direct key agreement. Direct mode uses a - * pre-shared content encryption key (CEK). AES Key Wrap generates one CEK and - * wraps it separately for each recipient. ECDH-ES derives the CEK for one - * recipient and puts the ephemeral public key in the unprotected header. + * AES Key Wrap, ECDH-ES direct key agreement, and HPKE-0 key encryption. + * Direct mode uses a pre-shared content encryption key (CEK). AES Key Wrap + * and HPKE generate one CEK and protect it separately for each recipient. + * ECDH-ES derives the CEK for one recipient and puts the ephemeral public key + * in the unprotected header. * * \param recipients Array of WOLFCOSE_RECIPIENT with keys. * \param recipientCount Number of recipients (must be >= 1). @@ -1385,8 +1461,8 @@ WOLFCOSE_API int wc_CoseSign_Verify(const WOLFCOSE_KEY* verifyKey, * \param out Output buffer. * \param outSz Output buffer size. * \param outLen Output: bytes written to out. - * \param rng Initialized WC_RNG for AES Key Wrap or ECDH-ES; - * may be NULL for direct mode. + * \param rng Initialized WC_RNG for AES Key Wrap, ECDH-ES, or + * HPKE; may be NULL for direct mode. * \return WOLFCOSE_SUCCESS or negative error code. */ WOLFCOSE_API int wc_CoseEncrypt_Encrypt(const WOLFCOSE_RECIPIENT* recipients, diff --git a/src/wolfcose.c b/src/wolfcose.c index 990c3d0..c084ba1 100644 --- a/src/wolfcose.c +++ b/src/wolfcose.c @@ -43,6 +43,9 @@ #if defined(WOLFCOSE_HAVE_CHACHA20) #include #endif +#if defined(WOLFCOSE_HAVE_HPKE_0) + #include +#endif #include /* ----- Forced failure injection for testing error paths ----- */ @@ -391,6 +394,13 @@ WOLFCOSE_LOCAL const uint8_t WOLFCOSE_CTX_ENCRYPT0[8] = { WOLFCOSE_LOCAL const uint8_t WOLFCOSE_CTX_ENCRYPT[7] = { 0x45u, 0x6Eu, 0x63u, 0x72u, 0x79u, 0x70u, 0x74u }; +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) || \ + defined(WOLFCOSE_HPKE_0_KE_DECRYPT) +static const uint8_t wolfCose_HpkeRecipientContext[14] = { + 0x48u, 0x50u, 0x4Bu, 0x45u, 0x20u, 0x52u, 0x65u, + 0x63u, 0x69u, 0x70u, 0x69u, 0x65u, 0x6Eu, 0x74u +}; +#endif /* ----- Internal helpers: algorithm dispatch ----- */ @@ -1334,6 +1344,19 @@ int wolfCose_DecodeProtectedHdr(const uint8_t* data, size_t dataLen, hdr->partialIvLen = pivBstrLen; } } +#if defined(WOLFCOSE_HAVE_HPKE_0) + else if ((ret == WOLFCOSE_SUCCESS) && + (label == WOLFCOSE_HDR_HPKE_EK)) { + const uint8_t* ekData; + size_t ekLen; + ret = wc_CBOR_DecodeBstr(&ctx, &ekData, &ekLen); + if (ret == WOLFCOSE_SUCCESS) { + hdr->hpkeEk = ekData; + hdr->hpkeEkLen = ekLen; + hdr->flags |= WOLFCOSE_HDR_FLAG_HPKE_EK; + } + } +#endif else { if (ret == WOLFCOSE_SUCCESS) { /* Skip unknown header */ @@ -1430,6 +1453,17 @@ int wolfCose_DecodeUnprotectedHdr(WOLFCOSE_CBOR_CTX* ctx, WOLFCOSE_HDR* hdr, hdr->partialIvLen = bstrLen; } } +#if defined(WOLFCOSE_HAVE_HPKE_0) + else if ((ret == WOLFCOSE_SUCCESS) && + (label == WOLFCOSE_HDR_HPKE_EK)) { + ret = wc_CBOR_DecodeBstr(ctx, &bstrData, &bstrLen); + if (ret == WOLFCOSE_SUCCESS) { + hdr->hpkeEk = bstrData; + hdr->hpkeEkLen = bstrLen; + hdr->flags |= WOLFCOSE_HDR_FLAG_HPKE_EK; + } + } +#endif else if ((ret == WOLFCOSE_SUCCESS) && (label == WOLFCOSE_HDR_ALG)) { if ((ctx->idx < ctx->bufSz) && @@ -5234,6 +5268,339 @@ static int wolfCose_LenFitsWord32(size_t n) } #endif +#if defined(WOLFCOSE_HAVE_HPKE_0) + +#define WOLFCOSE_HPKE_0_ENC_SZ 65u +#define WOLFCOSE_HPKE_0_TAG_SZ 16u +#define WOLFCOSE_HPKE_0_PSK_ID_LABEL (-5) + +/* Validate the fixed P-256 HPKE-0 key representation. The caller-provided + * COSE metadata and the attached wolfCrypt key must both agree on P-256. */ +static int wolfCose_Hpke0ValidateKey(const WOLFCOSE_KEY* key, + int32_t alg, int needPrivate) +{ + int ret = WOLFCOSE_SUCCESS; + + if (key == NULL) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else if ((key->kty != WOLFCOSE_KTY_EC2) || + (key->crv != WOLFCOSE_CRV_P256) || + (key->key.ecc == NULL) || + (wc_ecc_size(key->key.ecc) != 32)) { + ret = WOLFCOSE_E_COSE_KEY_TYPE; + } + else if ((key->alg != WOLFCOSE_ALG_UNSET) && (key->alg != alg)) { + ret = WOLFCOSE_E_COSE_BAD_ALG; + } + else if ((needPrivate != 0) && (key->hasPrivate != 1u)) { + ret = WOLFCOSE_E_COSE_KEY_TYPE; + } + else { + /* Key is valid. */ + } + + return ret; +} + +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) || \ + defined(WOLFCOSE_HPKE_0_KE_DECRYPT) +/* Serialize the deterministic Recipient_structure used as HPKE info for + * HPKE-0-KE. recipient_extra_info is intentionally the empty bstr in P0. */ +static int wolfCose_Hpke0BuildRecipientInfo(int32_t nextLayerAlg, + const uint8_t* recipientProtected, size_t recipientProtectedLen, + uint8_t* scratch, size_t scratchSz, size_t* infoLen) +{ + int ret; + WOLFCOSE_CBOR_CTX ctx; + + if ((recipientProtected == NULL) || (scratch == NULL) || + (infoLen == NULL)) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else { + ctx.buf = scratch; + ctx.bufSz = scratchSz; + ctx.idx = 0u; + + ret = wc_CBOR_EncodeArrayStart(&ctx, 4u); + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeTstr(&ctx, wolfCose_HpkeRecipientContext, + sizeof(wolfCose_HpkeRecipientContext)); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeInt(&ctx, (int64_t)nextLayerAlg); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeBstr(&ctx, recipientProtected, + recipientProtectedLen); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeBstr(&ctx, NULL, 0u); + } + if (ret == WOLFCOSE_SUCCESS) { + *infoLen = ctx.idx; + } + } + + return ret; +} +#endif /* WOLFCOSE_HPKE_0_KE_ENCRYPT || WOLFCOSE_HPKE_0_KE_DECRYPT */ + +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) +/* Encode recipient protected headers in deterministic label order. Including + * kid here binds recipient selection into the HPKE Recipient_structure. */ +static int wolfCose_Hpke0EncodeRecipientProtectedHdr(int32_t alg, + const uint8_t* kid, size_t kidLen, + uint8_t* buf, size_t bufSz, size_t* outLen) +{ + int ret; + WOLFCOSE_CBOR_CTX ctx; + size_t mapEntries; + + if ((buf == NULL) || (outLen == NULL) || + ((kid == NULL) && (kidLen != 0u)) || + ((kid != NULL) && (kidLen == 0u))) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else { + mapEntries = (kid != NULL) ? 2u : 1u; + ctx.buf = buf; + ctx.bufSz = bufSz; + ctx.idx = 0u; + + ret = wc_CBOR_EncodeMapStart(&ctx, mapEntries); + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_HDR_ALG); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeInt(&ctx, (int64_t)alg); + } + if ((ret == WOLFCOSE_SUCCESS) && (kid != NULL)) { + ret = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_HDR_KID); + } + if ((ret == WOLFCOSE_SUCCESS) && (kid != NULL)) { + ret = wc_CBOR_EncodeBstr(&ctx, kid, kidLen); + } + if (ret == WOLFCOSE_SUCCESS) { + *outLen = ctx.idx; + } + } + + return ret; +} +#endif /* WOLFCOSE_HPKE_0_KE_ENCRYPT */ + +#if defined(WOLFCOSE_HPKE_0_ENCRYPT) || \ + defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) +typedef struct WOLFCOSE_HPKE_0_SEAL_CTX { + Hpke hpke; + ecc_key ephemeralKey; + int ephemeralInited; +} WOLFCOSE_HPKE_0_SEAL_CTX; + +/* Generate and serialize the ephemeral P-256 key before the COSE headers are + * emitted. The caller then seals into its final ciphertext destination. */ +static int wolfCose_Hpke0SealInit(WOLFCOSE_HPKE_0_SEAL_CTX* sealCtx, + const WOLFCOSE_KEY* recipientKey, int32_t alg, WC_RNG* rng, + uint8_t* enc, size_t encSz) +{ + int ret = WOLFCOSE_SUCCESS; + int hpkeRet; + word16 encLen = (word16)WOLFCOSE_HPKE_0_ENC_SZ; + + if (sealCtx != NULL) { + (void)XMEMSET(sealCtx, 0, sizeof(*sealCtx)); + } + + if ((sealCtx == NULL) || (rng == NULL) || (enc == NULL)) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else if (encSz < WOLFCOSE_HPKE_0_ENC_SZ) { + ret = WOLFCOSE_E_BUFFER_TOO_SMALL; + } + else { + ret = wolfCose_Hpke0ValidateKey(recipientKey, alg, 0); + } + + if (ret == WOLFCOSE_SUCCESS) { + hpkeRet = wc_HpkeInit(&sealCtx->hpke, DHKEM_P256_HKDF_SHA256, + HKDF_SHA256, HPKE_AES_128_GCM, NULL); + if (hpkeRet != 0) { + ret = WOLFCOSE_E_CRYPTO; + } + } + if (ret == WOLFCOSE_SUCCESS) { + hpkeRet = wc_ecc_init(&sealCtx->ephemeralKey); + if (hpkeRet != 0) { + ret = WOLFCOSE_E_CRYPTO; + } + else { + sealCtx->ephemeralInited = 1; + } + } + if (ret == WOLFCOSE_SUCCESS) { + hpkeRet = wc_ecc_make_key_ex(rng, 32, &sealCtx->ephemeralKey, + ECC_SECP256R1); + if (hpkeRet != 0) { + ret = WOLFCOSE_E_CRYPTO; + } + } + if (ret == WOLFCOSE_SUCCESS) { + hpkeRet = wc_HpkeSerializePublicKey(&sealCtx->hpke, + &sealCtx->ephemeralKey, + enc, &encLen); + if ((hpkeRet != 0) || (encLen != WOLFCOSE_HPKE_0_ENC_SZ)) { + ret = WOLFCOSE_E_CRYPTO; + } + } + + if ((ret != WOLFCOSE_SUCCESS) && (sealCtx != NULL)) { + if (sealCtx->ephemeralInited != 0) { + (void)wc_ecc_free(&sealCtx->ephemeralKey); + } + (void)wolfCose_ForceZero(sealCtx, sizeof(*sealCtx)); + } + + return ret; +} + +/* Perform the one-shot HPKE seal with a prepared ephemeral key. */ +static int wolfCose_Hpke0Seal(WOLFCOSE_HPKE_0_SEAL_CTX* sealCtx, + const WOLFCOSE_KEY* recipientKey, + const uint8_t* info, size_t infoLen, + const uint8_t* aad, size_t aadLen, + const uint8_t* plaintext, size_t plaintextLen, + uint8_t* ciphertext, size_t ciphertextSz) +{ + int ret = WOLFCOSE_SUCCESS; + int hpkeRet; + + if ((sealCtx == NULL) || (recipientKey == NULL) || + (plaintext == NULL) || (ciphertext == NULL)) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else if ((info == NULL) && (infoLen != 0u)) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else if ((aad == NULL) && (aadLen != 0u)) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else if ((plaintextLen > (SIZE_MAX - WOLFCOSE_HPKE_0_TAG_SZ)) || + (ciphertextSz < (plaintextLen + WOLFCOSE_HPKE_0_TAG_SZ))) { + ret = WOLFCOSE_E_BUFFER_TOO_SMALL; + } +#ifdef WOLFCOSE_CHECK_WORD32_LEN + else if ((wolfCose_LenFitsWord32(infoLen) == 0) || + (wolfCose_LenFitsWord32(aadLen) == 0) || + (wolfCose_LenFitsWord32(plaintextLen) == 0)) { + ret = WOLFCOSE_E_INVALID_ARG; + } +#endif + else if (sealCtx->ephemeralInited == 0) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else { + /* Seal context is valid. */ + } + + if (ret == WOLFCOSE_SUCCESS) { + hpkeRet = wc_HpkeSealBase(&sealCtx->hpke, + &sealCtx->ephemeralKey, (void*)recipientKey->key.ecc, + (byte*)info, (word32)infoLen, + (byte*)aad, (word32)aadLen, + (byte*)plaintext, (word32)plaintextLen, ciphertext); + if (hpkeRet != 0) { + ret = WOLFCOSE_E_CRYPTO; + } + } + + return ret; +} + +static void wolfCose_Hpke0SealFree(WOLFCOSE_HPKE_0_SEAL_CTX* sealCtx) +{ + if (sealCtx != NULL) { + if (sealCtx->ephemeralInited != 0) { + (void)wc_ecc_free(&sealCtx->ephemeralKey); + } + (void)wolfCose_ForceZero(sealCtx, sizeof(*sealCtx)); + } +} +#endif /* WOLFCOSE_HPKE_0_ENCRYPT || WOLFCOSE_HPKE_0_KE_ENCRYPT */ + +#if defined(WOLFCOSE_HPKE_0_DECRYPT) || \ + defined(WOLFCOSE_HPKE_0_KE_DECRYPT) +/* Perform a single HPKE base-mode open with the fixed HPKE-0 suite. */ +static int wolfCose_Hpke0Open(const WOLFCOSE_KEY* recipientKey, + int32_t alg, + const uint8_t* info, size_t infoLen, + const uint8_t* aad, size_t aadLen, + const uint8_t* enc, size_t encLen, + const uint8_t* ciphertext, size_t ciphertextLen, + uint8_t* plaintext, size_t plaintextSz) +{ + int ret = WOLFCOSE_SUCCESS; + int hpkeRet; + Hpke hpke; + size_t ciphertextBodyLen = 0u; + + if ((enc == NULL) || (ciphertext == NULL) || (plaintext == NULL)) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else if ((info == NULL) && (infoLen != 0u)) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else if ((aad == NULL) && (aadLen != 0u)) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else if ((encLen != WOLFCOSE_HPKE_0_ENC_SZ) || + (ciphertextLen < WOLFCOSE_HPKE_0_TAG_SZ)) { + ret = WOLFCOSE_E_COSE_BAD_HDR; + } + else { + ciphertextBodyLen = ciphertextLen - WOLFCOSE_HPKE_0_TAG_SZ; + if (plaintextSz < ciphertextBodyLen) { + ret = WOLFCOSE_E_BUFFER_TOO_SMALL; + } + } +#ifdef WOLFCOSE_CHECK_WORD32_LEN + if ((ret == WOLFCOSE_SUCCESS) && + ((wolfCose_LenFitsWord32(infoLen) == 0) || + (wolfCose_LenFitsWord32(aadLen) == 0) || + (wolfCose_LenFitsWord32(ciphertextBodyLen) == 0))) { + ret = WOLFCOSE_E_INVALID_ARG; + } +#endif + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_Hpke0ValidateKey(recipientKey, alg, 1); + } + if (ret == WOLFCOSE_SUCCESS) { + hpkeRet = wc_HpkeInit(&hpke, DHKEM_P256_HKDF_SHA256, + HKDF_SHA256, HPKE_AES_128_GCM, NULL); + if (hpkeRet != 0) { + ret = WOLFCOSE_E_CRYPTO; + } + } + if (ret == WOLFCOSE_SUCCESS) { + hpkeRet = wc_HpkeOpenBase(&hpke, (void*)recipientKey->key.ecc, + enc, (word16)encLen, + (byte*)info, (word32)infoLen, + (byte*)aad, (word32)aadLen, + (byte*)ciphertext, (word32)ciphertextBodyLen, plaintext); + if (hpkeRet != 0) { + ret = WOLFCOSE_E_COSE_DECRYPT_FAIL; + } + } + + (void)wolfCose_ForceZero(&hpke, sizeof(hpke)); + + return ret; +} +#endif /* WOLFCOSE_HPKE_0_DECRYPT || WOLFCOSE_HPKE_0_KE_DECRYPT */ + +#endif /* WOLFCOSE_HAVE_HPKE_0 */ + /* ----- COSE_Sign1 API ----- */ /* Used by both COSE_Sign1 and COSE_Sign, so kept outside the @@ -8169,58 +8536,367 @@ int wc_CoseEncrypt0_Decrypt(const WOLFCOSE_KEY* key, #endif /* WOLFCOSE_ENCRYPT0 && (WOLFCOSE_HAVE_AESGCM || WOLFCOSE_HAVE_AESCCM || (WOLFCOSE_HAVE_CHACHA20)) */ -/* ----- - * COSE_Mac0 API (RFC 9052 Section 6.2) - * Supports HMAC (RFC 9053 Section 3.1) and AES-CBC-MAC (RFC 9053 Section 3.2) - * ----- */ - -#if (defined(WOLFCOSE_MAC0) || defined(WOLFCOSE_MAC)) && \ - (defined(WOLFCOSE_HAVE_HMAC) || defined(WOLFCOSE_HAVE_AESMAC)) - -#if defined(WOLFCOSE_MAC0) -/** - * Build the MAC_structure for COSE_Mac0 (wrapper for unified builder): - * ["MAC0", body_protected, external_aad, payload] - */ -static int wolfCose_BuildMacStructure(const uint8_t* protectedHdr, - size_t protectedLen, - const uint8_t* extAad, - size_t extAadLen, - const uint8_t* payload, - size_t payloadLen, - uint8_t* scratch, size_t scratchSz, - size_t* structLen) -{ - /* Use unified builder with "MAC0" context, no sign_protected */ - return wolfCose_BuildToBeSignedMaced( - WOLFCOSE_CTX_MAC0, sizeof(WOLFCOSE_CTX_MAC0), - protectedHdr, protectedLen, - NULL, 0, /* no sign_protected for Mac0 */ - extAad, extAadLen, - payload, payloadLen, - scratch, scratchSz, structLen); -} -#endif +/* ----- COSE-HPKE Integrated Encryption API ----- */ -/** - * Get MAC tag size for a COSE MAC algorithm (HMAC or AES-CBC-MAC). - */ -static int wolfCose_MacTagSize(int32_t alg, size_t* tagSz) +#if defined(WOLFCOSE_HPKE_0_ENCRYPT) +int wc_CoseHpkeEncrypt0_Encrypt(const WOLFCOSE_KEY* recipientKey, + const uint8_t* kid, size_t kidLen, + const uint8_t* payload, size_t payloadLen, + uint8_t* detachedPayload, size_t detachedSz, 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) { int ret = WOLFCOSE_SUCCESS; + uint8_t protectedBuf[WOLFCOSE_PROTECTED_HDR_MAX]; + uint8_t enc[WOLFCOSE_HPKE_0_ENC_SZ]; + size_t protectedLen = 0u; + size_t encStructLen = 0u; + size_t ciphertextLen = 0u; + size_t ciphertextOffset = 0u; + WOLFCOSE_CBOR_CTX ctx; + WOLFCOSE_HPKE_0_SEAL_CTX sealCtx; + int isDetached; - if (tagSz == NULL) { + (void)XMEMSET(&sealCtx, 0, sizeof(sealCtx)); + isDetached = (detachedPayload != NULL) ? 1 : 0; + + if ((recipientKey == NULL) || (payload == NULL) || (scratch == NULL) || + (out == NULL) || (outLen == NULL) || (rng == NULL)) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else if (((kid == NULL) && (kidLen != 0u)) || + ((kid != NULL) && (kidLen == 0u))) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else if ((payloadLen > (SIZE_MAX - WOLFCOSE_HPKE_0_TAG_SZ)) || + ((isDetached != 0) && + ((detachedLen == NULL) || + (detachedSz < (payloadLen + WOLFCOSE_HPKE_0_TAG_SZ))))) { + ret = WOLFCOSE_E_BUFFER_TOO_SMALL; + } +#ifdef WOLFCOSE_CHECK_WORD32_LEN + else if ((wolfCose_LenFitsWord32(payloadLen) == 0) || + (wolfCose_LenFitsWord32(extAadLen) == 0) || + (wolfCose_LenFitsWord32(detachedSz) == 0) || + (wolfCose_LenFitsWord32(outSz) == 0) || + (wolfCose_LenFitsWord32(scratchSz) == 0)) { ret = WOLFCOSE_E_INVALID_ARG; } - else { - switch (alg) { -#ifdef WOLFCOSE_HAVE_HMAC -#ifdef WOLFCOSE_HAVE_HMAC256 - case WOLFCOSE_ALG_HMAC_256_256: - *tagSz = 32; /* SHA-256 output */ - break; #endif -#ifdef WOLFCOSE_HAVE_HMAC384 + else { + /* Input parameters are structurally valid. */ + } + + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_EncodeProtectedHdr(WOLFCOSE_ALG_HPKE_0, + protectedBuf, + sizeof(protectedBuf), + &protectedLen); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_BuildEncStructure0(protectedBuf, protectedLen, + extAad, extAadLen, + scratch, scratchSz, &encStructLen); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_Hpke0SealInit(&sealCtx, recipientKey, + WOLFCOSE_ALG_HPKE_0, rng, + enc, sizeof(enc)); + } + if (ret == WOLFCOSE_SUCCESS) { + ciphertextLen = payloadLen + WOLFCOSE_HPKE_0_TAG_SZ; + ctx.buf = out; + ctx.bufSz = outSz; + ctx.idx = 0u; + ret = wc_CBOR_EncodeTag(&ctx, WOLFCOSE_TAG_ENCRYPT0); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeArrayStart(&ctx, 3u); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeBstr(&ctx, protectedBuf, protectedLen); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeMapStart(&ctx, (kid != NULL) ? 2u : 1u); + } + if ((ret == WOLFCOSE_SUCCESS) && (kid != NULL)) { + ret = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_HDR_KID); + } + if ((ret == WOLFCOSE_SUCCESS) && (kid != NULL)) { + ret = wc_CBOR_EncodeBstr(&ctx, kid, kidLen); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_HDR_HPKE_EK); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeBstr(&ctx, enc, sizeof(enc)); + } + + if ((ret == WOLFCOSE_SUCCESS) && (isDetached != 0)) { + ret = wc_CBOR_EncodeNull(&ctx); + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_Hpke0Seal(&sealCtx, recipientKey, + NULL, 0u, scratch, encStructLen, + payload, payloadLen, detachedPayload, detachedSz); + } + if (ret == WOLFCOSE_SUCCESS) { + *detachedLen = ciphertextLen; + } + } + else if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_CBOR_EncodeHead(&ctx, WOLFCOSE_CBOR_BSTR, + (uint64_t)ciphertextLen); + if ((ret == WOLFCOSE_SUCCESS) && + ((ctx.idx > ctx.bufSz) || + (ciphertextLen > (ctx.bufSz - ctx.idx)))) { + ret = WOLFCOSE_E_BUFFER_TOO_SMALL; + } + if (ret == WOLFCOSE_SUCCESS) { + ciphertextOffset = ctx.idx; + ret = wolfCose_Hpke0Seal(&sealCtx, recipientKey, + NULL, 0u, scratch, encStructLen, + payload, payloadLen, &out[ciphertextOffset], ciphertextLen); + } + if (ret == WOLFCOSE_SUCCESS) { + ctx.idx += ciphertextLen; + } + } + else { + /* No action required. */ + } + + if (ret == WOLFCOSE_SUCCESS) { + *outLen = ctx.idx; + } + + wolfCose_Hpke0SealFree(&sealCtx); + (void)wolfCose_ForceZero(enc, sizeof(enc)); + (void)wolfCose_ForceZero(protectedBuf, sizeof(protectedBuf)); + if (scratch != NULL) { + (void)wolfCose_ForceZero(scratch, scratchSz); + } + if (ret != WOLFCOSE_SUCCESS) { + if (out != NULL) { + (void)wolfCose_ForceZero(out, outSz); + } + if ((isDetached != 0) && (detachedPayload != NULL)) { + (void)wolfCose_ForceZero(detachedPayload, detachedSz); + } + if (detachedLen != NULL) { + *detachedLen = 0u; + } + } + + return ret; +} +#endif /* WOLFCOSE_HPKE_0_ENCRYPT */ + +#if defined(WOLFCOSE_HPKE_0_DECRYPT) +int wc_CoseHpkeEncrypt0_Decrypt(const WOLFCOSE_KEY* recipientKey, + const uint8_t* in, size_t inSz, + const uint8_t* detachedCt, size_t detachedCtLen, + const uint8_t* extAad, size_t extAadLen, + uint8_t* scratch, size_t scratchSz, + WOLFCOSE_HDR* hdr, + uint8_t* plaintext, size_t plaintextSz, size_t* plaintextLen) +{ + int ret = WOLFCOSE_SUCCESS; + WOLFCOSE_CBOR_CTX ctx; + WOLFCOSE_CBOR_ITEM item; + uint64_t tag = 0u; + size_t arrayCount = 0u; + const uint8_t* protectedData = NULL; + size_t protectedLen = 0u; + const uint8_t* ciphertext = NULL; + size_t ciphertextLen = 0u; + size_t encStructLen = 0u; + size_t payloadLen = 0u; + WOLFCOSE_HDR_STATE hdrState; + + if ((recipientKey == NULL) || (in == NULL) || (inSz == 0u) || + (scratch == NULL) || (hdr == NULL) || (plaintext == NULL) || + (plaintextLen == NULL)) { + ret = WOLFCOSE_E_INVALID_ARG; + } +#ifdef WOLFCOSE_CHECK_WORD32_LEN + else if ((wolfCose_LenFitsWord32(inSz) == 0) || + (wolfCose_LenFitsWord32(detachedCtLen) == 0) || + (wolfCose_LenFitsWord32(extAadLen) == 0) || + (wolfCose_LenFitsWord32(plaintextSz) == 0) || + (wolfCose_LenFitsWord32(scratchSz) == 0)) { + ret = WOLFCOSE_E_INVALID_ARG; + } +#endif + else { + (void)XMEMSET(hdr, 0, sizeof(*hdr)); + ctx.cbuf = in; + ctx.bufSz = inSz; + ctx.idx = 0u; + } + + if ((ret == WOLFCOSE_SUCCESS) && (ctx.idx < ctx.bufSz) && + (wc_CBOR_PeekType(&ctx) == WOLFCOSE_CBOR_TAG)) { + ret = wc_CBOR_DecodeTag(&ctx, &tag); + if ((ret == WOLFCOSE_SUCCESS) && (tag != WOLFCOSE_TAG_ENCRYPT0)) { + ret = WOLFCOSE_E_COSE_BAD_TAG; + } + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_DecodeArrayStart(&ctx, &arrayCount); + } + if ((ret == WOLFCOSE_SUCCESS) && (arrayCount != 3u)) { + ret = WOLFCOSE_E_CBOR_MALFORMED; + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_DecodeBstr(&ctx, &protectedData, &protectedLen); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_DecodeProtectedHdr(protectedData, protectedLen, hdr, + &hdrState); + } + if ((ret == WOLFCOSE_SUCCESS) && + ((wolfCose_HdrStateContains(&hdrState, WOLFCOSE_HDR_ALG) == 0) || + (hdr->alg != WOLFCOSE_ALG_HPKE_0) || + (wolfCose_HdrStateContains(&hdrState, + WOLFCOSE_HDR_HPKE_EK) != 0) || + (wolfCose_HdrStateContains(&hdrState, + WOLFCOSE_HPKE_0_PSK_ID_LABEL) != 0))) { + ret = WOLFCOSE_E_COSE_BAD_HDR; + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_DecodeUnprotectedHdr(&ctx, hdr, &hdrState); + } + if ((ret == WOLFCOSE_SUCCESS) && + ((hdr->iv != NULL) || (hdr->partialIv != NULL) || + ((hdr->flags & WOLFCOSE_HDR_FLAG_HPKE_EK) == 0u) || + (hdr->hpkeEkLen != WOLFCOSE_HPKE_0_ENC_SZ) || + (wolfCose_HdrStateContains(&hdrState, + WOLFCOSE_HPKE_0_PSK_ID_LABEL) != 0))) { + ret = WOLFCOSE_E_COSE_BAD_HDR; + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_CBOR_DecodeHead(&ctx, &item); + } + if (ret == WOLFCOSE_SUCCESS) { + if ((item.majorType == WOLFCOSE_CBOR_SIMPLE) && (item.val == 22u)) { + if (detachedCt == NULL) { + hdr->flags |= WOLFCOSE_HDR_FLAG_DETACHED; + ret = WOLFCOSE_E_DETACHED_PAYLOAD; + } + else { + ciphertext = detachedCt; + ciphertextLen = detachedCtLen; + hdr->flags |= WOLFCOSE_HDR_FLAG_DETACHED; + } + } + else if (item.majorType == WOLFCOSE_CBOR_BSTR) { + ciphertext = item.data; + ciphertextLen = item.dataLen; + } + else { + ret = WOLFCOSE_E_CBOR_TYPE; + } + } + if ((ret == WOLFCOSE_SUCCESS) && (ctx.idx != ctx.bufSz)) { + ret = WOLFCOSE_E_CBOR_MALFORMED; + } + if ((ret == WOLFCOSE_SUCCESS) && + (ciphertextLen < WOLFCOSE_HPKE_0_TAG_SZ)) { + ret = WOLFCOSE_E_CBOR_MALFORMED; + } + if (ret == WOLFCOSE_SUCCESS) { + payloadLen = ciphertextLen - WOLFCOSE_HPKE_0_TAG_SZ; + if (payloadLen > plaintextSz) { + ret = WOLFCOSE_E_BUFFER_TOO_SMALL; + } + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_BuildEncStructure0(protectedData, protectedLen, + extAad, extAadLen, + scratch, scratchSz, &encStructLen); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_Hpke0Open(recipientKey, WOLFCOSE_ALG_HPKE_0, + NULL, 0u, scratch, encStructLen, + hdr->hpkeEk, hdr->hpkeEkLen, + ciphertext, ciphertextLen, plaintext, plaintextSz); + } + if (ret == WOLFCOSE_SUCCESS) { + *plaintextLen = payloadLen; + } + else if (plaintextLen != NULL) { + *plaintextLen = 0u; + } + else { + /* No action required. */ + } + + wolfCose_HdrClearOnFail(ret, hdr); + if (scratch != NULL) { + (void)wolfCose_ForceZero(scratch, scratchSz); + } + if ((ret != WOLFCOSE_SUCCESS) && (plaintext != NULL)) { + (void)wolfCose_ForceZero(plaintext, plaintextSz); + } + + return ret; +} +#endif /* WOLFCOSE_HPKE_0_DECRYPT */ + +/* ----- + * COSE_Mac0 API (RFC 9052 Section 6.2) + * Supports HMAC (RFC 9053 Section 3.1) and AES-CBC-MAC (RFC 9053 Section 3.2) + * ----- */ + +#if (defined(WOLFCOSE_MAC0) || defined(WOLFCOSE_MAC)) && \ + (defined(WOLFCOSE_HAVE_HMAC) || defined(WOLFCOSE_HAVE_AESMAC)) + +#if defined(WOLFCOSE_MAC0) +/** + * Build the MAC_structure for COSE_Mac0 (wrapper for unified builder): + * ["MAC0", body_protected, external_aad, payload] + */ +static int wolfCose_BuildMacStructure(const uint8_t* protectedHdr, + size_t protectedLen, + const uint8_t* extAad, + size_t extAadLen, + const uint8_t* payload, + size_t payloadLen, + uint8_t* scratch, size_t scratchSz, + size_t* structLen) +{ + /* Use unified builder with "MAC0" context, no sign_protected */ + return wolfCose_BuildToBeSignedMaced( + WOLFCOSE_CTX_MAC0, sizeof(WOLFCOSE_CTX_MAC0), + protectedHdr, protectedLen, + NULL, 0, /* no sign_protected for Mac0 */ + extAad, extAadLen, + payload, payloadLen, + scratch, scratchSz, structLen); +} +#endif + +/** + * Get MAC tag size for a COSE MAC algorithm (HMAC or AES-CBC-MAC). + */ +static int wolfCose_MacTagSize(int32_t alg, size_t* tagSz) +{ + int ret = WOLFCOSE_SUCCESS; + + if (tagSz == NULL) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else { + switch (alg) { +#ifdef WOLFCOSE_HAVE_HMAC +#ifdef WOLFCOSE_HAVE_HMAC256 + case WOLFCOSE_ALG_HMAC_256_256: + *tagSz = 32; /* SHA-256 output */ + break; +#endif +#ifdef WOLFCOSE_HAVE_HMAC384 case WOLFCOSE_ALG_HMAC_384_384: *tagSz = 48; /* SHA-384 output */ break; @@ -8980,10 +9656,11 @@ static int wolfCose_ValidateRecipientKeyAlg(const WOLFCOSE_KEY* key, * Structure: [Headers, ciphertext, recipients: [+ COSE_recipient]] * Each COSE_recipient: [protected, unprotected, ciphertext] * - * Recipient key management supports direct, AES Key Wrap, and ECDH-ES direct - * modes when enabled. Direct uses a pre-shared CEK. AES Key Wrap generates one - * CEK and wraps it separately for each recipient. ECDH-ES derives the CEK for - * one recipient and carries the ephemeral public key in its unprotected header. + * Recipient key management supports direct, AES Key Wrap, ECDH-ES direct, + * and optional HPKE key-encryption modes. Direct uses a pre-shared CEK. AES + * Key Wrap and HPKE generate one CEK and protect it for each recipient. + * ECDH-ES derives the CEK for one recipient and carries the ephemeral public + * key in its unprotected header. */ int wc_CoseEncrypt_Encrypt(const WOLFCOSE_RECIPIENT* recipients, size_t recipientCount, @@ -9028,6 +9705,19 @@ int wc_CoseEncrypt_Encrypt(const WOLFCOSE_RECIPIENT* recipients, size_t wrappedCekLen = 0; int useKeyWrap = 0; #endif +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) + uint8_t cekHpke[32]; /* Random CEK wrapped for every recipient */ + uint8_t hpkeEnc[WOLFCOSE_HPKE_0_ENC_SZ]; + size_t hpkeRecipientInfoLen = 0u; + WOLFCOSE_HPKE_0_SEAL_CTX hpkeSealCtx; + int useHpke = 0; +#endif + +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) + (void)XMEMSET(cekHpke, 0, sizeof(cekHpke)); + (void)XMEMSET(hpkeEnc, 0, sizeof(hpkeEnc)); + (void)XMEMSET(&hpkeSealCtx, 0, sizeof(hpkeSealCtx)); +#endif /* Parameter validation */ if ((recipients == NULL) || (recipientCount == 0u) || @@ -9097,7 +9787,35 @@ int wc_CoseEncrypt_Encrypt(const WOLFCOSE_RECIPIENT* recipients, } } - /* Validate first recipient and determine key mode */ + /* Validate first recipient and determine key mode. */ +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) + if ((ret == WOLFCOSE_SUCCESS) && + (recipients[0].algId == WOLFCOSE_ALG_HPKE_0_KE)) { + if (rng == NULL) { + ret = WOLFCOSE_E_INVALID_ARG; + } + for (i = 0; (ret == WOLFCOSE_SUCCESS) && (i < recipientCount); i++) { + if (recipients[i].algId != WOLFCOSE_ALG_HPKE_0_KE) { + ret = WOLFCOSE_E_COSE_BAD_ALG; + } + else { + ret = wolfCose_Hpke0ValidateKey(recipients[i].key, + WOLFCOSE_ALG_HPKE_0_KE, 0); + } + } + if (ret == WOLFCOSE_SUCCESS) { + int rngRet = wc_RNG_GenerateBlock(rng, cekHpke, (word32)keyLen); + if (rngRet != 0) { + ret = WOLFCOSE_E_CRYPTO; + } + } + if (ret == WOLFCOSE_SUCCESS) { + useHpke = 1; + encKey = cekHpke; + } + } + else +#endif #if defined(WOLFCOSE_ECDH_ES_DIRECT) && defined(HAVE_ECC) && defined(HAVE_HKDF) if ((ret == WOLFCOSE_SUCCESS) && (wolfCose_IsEcdhEsDirectAlg(recipients[0].algId) != 0)) { /* ECDH-ES direct is single-recipient only */ @@ -9397,19 +10115,44 @@ int wc_CoseEncrypt_Encrypt(const WOLFCOSE_RECIPIENT* recipients, /* Encode each recipient */ for (i = 0; (ret == WOLFCOSE_SUCCESS) && (i < recipientCount); i++) { - /* For direct key agreement, the wrapped CEK is empty */ - /* COSE_recipient = [protected, unprotected, ciphertext] */ - - /* Encode recipient protected header. RFC 9053 Section 6.1: the direct - * key algorithm uses a zero-length protected header, so treat an - * explicit WOLFCOSE_ALG_DIRECT the same as the unset direct case. */ - if (recipients[i].algId != WOLFCOSE_ALG_DIRECT) { - ret = wolfCose_EncodeProtectedHdr(recipients[i].algId, + /* COSE_recipient = [protected, unprotected, ciphertext]. */ + +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) + (void)XMEMSET(&hpkeSealCtx, 0, sizeof(hpkeSealCtx)); + if (useHpke != 0) { + /* The protected header is authenticated through the HPKE info. + * Keep kid protected as well so it selects the intended key. */ + ret = wolfCose_Hpke0EncodeRecipientProtectedHdr( + WOLFCOSE_ALG_HPKE_0_KE, + recipients[i].kid, recipients[i].kidLen, recipientProtectedBuf, sizeof(recipientProtectedBuf), &recipientProtectedLen); - } else { - /* Direct key - no alg in protected, use empty bstr */ - recipientProtectedLen = 0; + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_Hpke0BuildRecipientInfo(contentAlgId, + recipientProtectedBuf, recipientProtectedLen, + scratch, scratchSz, &hpkeRecipientInfoLen); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_Hpke0SealInit(&hpkeSealCtx, recipients[i].key, + WOLFCOSE_ALG_HPKE_0_KE, rng, hpkeEnc, sizeof(hpkeEnc)); + } + } + else +#endif + { + /* RFC 9053 Section 6.1: the direct key algorithm uses a + * zero-length protected header, so treat explicit direct as the + * unset direct case. */ + if ((recipients[i].algId != WOLFCOSE_ALG_UNSET) && + (recipients[i].algId != WOLFCOSE_ALG_DIRECT)) { + ret = wolfCose_EncodeProtectedHdr(recipients[i].algId, + recipientProtectedBuf, sizeof(recipientProtectedBuf), + &recipientProtectedLen); + } + else { + /* Direct key: no alg in protected, use an empty bstr. */ + recipientProtectedLen = 0u; + } } /* Start recipient array [protected, unprotected, ciphertext] */ @@ -9424,6 +10167,18 @@ int wc_CoseEncrypt_Encrypt(const WOLFCOSE_RECIPIENT* recipients, /* [1] unprotected header map */ if (ret == WOLFCOSE_SUCCESS) { +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) + if (useHpke != 0) { + ret = wc_CBOR_EncodeMapStart(&ctx, 1u); + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_HDR_HPKE_EK); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeBstr(&ctx, hpkeEnc, sizeof(hpkeEnc)); + } + } + else +#endif #if defined(WOLFCOSE_ECDH_ES_DIRECT) && defined(HAVE_ECC) && defined(HAVE_HKDF) if (useEcdhEs != 0) { /* ECDH-ES: encode kid (label 4 -> 0x04) before ephemeral @@ -9485,9 +10240,32 @@ int wc_CoseEncrypt_Encrypt(const WOLFCOSE_RECIPIENT* recipients, } } - /* [2] wrapped CEK (empty for direct key and ECDH-ES, computed - * per-recipient for key wrap). */ + /* [2] wrapped CEK. Direct and ECDH-ES use an empty bstr; key wrap and + * HPKE create one ciphertext per recipient. */ if (ret == WOLFCOSE_SUCCESS) { +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) + if (useHpke != 0) { + size_t hpkeCiphertextLen = keyLen + WOLFCOSE_HPKE_0_TAG_SZ; + + ret = wolfCose_CBOR_EncodeHead(&ctx, WOLFCOSE_CBOR_BSTR, + hpkeCiphertextLen); + if ((ret == WOLFCOSE_SUCCESS) && + ((ctx.idx > ctx.bufSz) || + (hpkeCiphertextLen > (ctx.bufSz - ctx.idx)))) { + ret = WOLFCOSE_E_CBOR_OVERFLOW; + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_Hpke0Seal(&hpkeSealCtx, recipients[i].key, + scratch, hpkeRecipientInfoLen, + NULL, 0u, cekHpke, keyLen, + &ctx.buf[ctx.idx], hpkeCiphertextLen); + } + if (ret == WOLFCOSE_SUCCESS) { + ctx.idx += hpkeCiphertextLen; + } + } + else +#endif #if defined(WOLFCOSE_KEY_WRAP) if (useKeyWrap != 0) { ret = wolfCose_KeyWrap(recipients[i].algId, @@ -9505,6 +10283,10 @@ int wc_CoseEncrypt_Encrypt(const WOLFCOSE_RECIPIENT* recipients, ret = wc_CBOR_EncodeBstr(&ctx, NULL, 0); } } + +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) + wolfCose_Hpke0SealFree(&hpkeSealCtx); +#endif } /* Set output length on success */ @@ -9524,6 +10306,11 @@ int wc_CoseEncrypt_Encrypt(const WOLFCOSE_RECIPIENT* recipients, #endif #if defined(WOLFCOSE_ECDH_ES_DIRECT) && defined(HAVE_ECC) && defined(HAVE_HKDF) (void)wolfCose_ForceZero(cek, sizeof(cek)); +#endif +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) + wolfCose_Hpke0SealFree(&hpkeSealCtx); + (void)wolfCose_ForceZero(cekHpke, sizeof(cekHpke)); + (void)wolfCose_ForceZero(hpkeEnc, sizeof(hpkeEnc)); #endif if (scratch != NULL) { (void)wolfCose_ForceZero(scratch, scratchSz); @@ -9579,6 +10366,9 @@ int wc_CoseEncrypt_Decrypt(const WOLFCOSE_RECIPIENT* recipient, WOLFCOSE_HDR_STATE hdrState; WOLFCOSE_HDR_STATE recipientHdrState; int bodyAlgProtected = 0; +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + int recipientAlgProtected = 0; +#endif #if defined(WOLFCOSE_ECDH_ES_DIRECT) && defined(HAVE_ECC) && defined(HAVE_HKDF) uint8_t cek[32]; uint8_t ephemPubX[66]; @@ -9596,6 +10386,20 @@ int wc_CoseEncrypt_Decrypt(const WOLFCOSE_RECIPIENT* recipient, size_t unwrappedCekLen = 0; int useKeyWrap = 0; #endif +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + uint8_t cekHpke[32]; + const uint8_t* hpkeWrappedCekData = NULL; + size_t hpkeWrappedCekLen = 0u; + size_t hpkeRecipientInfoLen = 0u; + WOLFCOSE_HDR recipUnprotHdr; + int useHpke = 0; +#endif + +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + (void)XMEMSET(cekHpke, 0, sizeof(cekHpke)); + (void)XMEMSET(&recipientHdr, 0, sizeof(recipientHdr)); + (void)XMEMSET(&recipUnprotHdr, 0, sizeof(recipUnprotHdr)); +#endif /* Parameter validation */ if ((recipient == NULL) || (in == NULL) || (inSz == 0u) || @@ -9738,7 +10542,84 @@ int wc_CoseEncrypt_Decrypt(const WOLFCOSE_RECIPIENT* recipient, ret = wolfCose_DecodeProtectedHdr(recipientProtectedData, recipientProtectedLen, &recipientHdr, &recipientHdrState); + if (ret == WOLFCOSE_SUCCESS) { + recipientAlgId = recipientHdr.alg; + } + } +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + if (ret == WOLFCOSE_SUCCESS) { + recipientAlgProtected = wolfCose_HdrStateContains(&recipientHdrState, + WOLFCOSE_HDR_ALG); + } + if ((ret == WOLFCOSE_SUCCESS) && + (recipientAlgId == WOLFCOSE_ALG_HPKE_0_KE) && + ((wolfCose_HdrStateContains(&recipientHdrState, + WOLFCOSE_HDR_ALG) == 0) || + (wolfCose_HdrStateContains(&recipientHdrState, + WOLFCOSE_HDR_HPKE_EK) != 0) || + (wolfCose_HdrStateContains(&recipientHdrState, + WOLFCOSE_HPKE_0_PSK_ID_LABEL) != 0))) { + ret = WOLFCOSE_E_COSE_BAD_HDR; + } +#endif + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_ValidateRecipientKeyAlg(recipient->key, recipientAlgId, + alg); + } + + /* Classify the recipient key-management algorithm. Only direct, ECDH-ES + * direct, AES key wrap, and enabled HPKE are supported. */ + if (ret == WOLFCOSE_SUCCESS) { + int recipModeOk = 0; + if ((recipientAlgId == WOLFCOSE_ALG_UNSET) || + (recipientAlgId == WOLFCOSE_ALG_DIRECT)) { + recipModeOk = 1; + } +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + else if (recipientAlgId == WOLFCOSE_ALG_HPKE_0_KE) { + recipModeOk = 1; + } +#endif +#if defined(WOLFCOSE_ECDH_ES_DIRECT) && defined(HAVE_ECC) && defined(HAVE_HKDF) + else if (wolfCose_IsEcdhEsDirectAlg(recipientAlgId) != 0) { + recipModeOk = 1; + } +#endif +#if defined(WOLFCOSE_KEY_WRAP) + else if (wolfCose_IsKeyWrapAlg(recipientAlgId) != 0) { + recipModeOk = 1; + } +#endif + else { + /* No action required */ + } + if (recipModeOk == 0) { + ret = WOLFCOSE_E_COSE_BAD_ALG; + } + } + + /* Enforce the caller's recipient->algId policy when set. A message in + * implicit direct mode has no recipient alg, so normalize it to direct. */ + if ((ret == WOLFCOSE_SUCCESS) && + (recipient->algId != WOLFCOSE_ALG_UNSET)) { + int32_t gotAlg = recipientAlgId; + if (gotAlg == WOLFCOSE_ALG_UNSET) { + gotAlg = WOLFCOSE_ALG_DIRECT; + } + if (recipient->algId != gotAlg) { + ret = WOLFCOSE_E_COSE_BAD_ALG; + } + } + +#if defined(WOLFCOSE_ECDH_ES_DIRECT) && defined(HAVE_ECC) && defined(HAVE_HKDF) + /* RFC 9052 Section 8.5.5: direct key agreement carries exactly one + * recipient. */ + if ((ret == WOLFCOSE_SUCCESS) && + (wolfCose_IsEcdhEsDirectAlg(recipientAlgId) != 0) && + ((recipientsCount != 1u) || (recipientIndex != 0u))) { + ret = WOLFCOSE_E_COSE_BAD_HDR; } +#endif /* [1] recipient unprotected header */ #if defined(WOLFCOSE_ECDH_ES_DIRECT) && defined(HAVE_ECC) && defined(HAVE_HKDF) @@ -9807,8 +10688,27 @@ int wc_CoseEncrypt_Decrypt(const WOLFCOSE_RECIPIENT* recipient, if (ret == WOLFCOSE_SUCCESS) { /* Decode the recipient unprotected map with duplicate-label tracking * (within the map and against the recipient protected bucket). */ - ret = wolfCose_DecodeUnprotectedHdr(&ctx, &recipientHdr, +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + if (recipientAlgId == WOLFCOSE_ALG_HPKE_0_KE) { + (void)XMEMSET(&recipUnprotHdr, 0, sizeof(recipUnprotHdr)); + ret = wolfCose_DecodeUnprotectedHdr(&ctx, &recipUnprotHdr, + &recipientHdrState); + if ((ret == WOLFCOSE_SUCCESS) && + ((recipUnprotHdr.iv != NULL) || + (recipUnprotHdr.partialIv != NULL) || + ((recipUnprotHdr.flags & WOLFCOSE_HDR_FLAG_HPKE_EK) == 0u) || + (recipUnprotHdr.hpkeEkLen != WOLFCOSE_HPKE_0_ENC_SZ) || + (wolfCose_HdrStateContains(&recipientHdrState, + WOLFCOSE_HPKE_0_PSK_ID_LABEL) != 0))) { + ret = WOLFCOSE_E_COSE_BAD_HDR; + } + } + else +#endif + { + ret = wolfCose_DecodeUnprotectedHdr(&ctx, &recipientHdr, &recipientHdrState); + } } else { /* No action required */ @@ -9816,6 +10716,15 @@ int wc_CoseEncrypt_Decrypt(const WOLFCOSE_RECIPIENT* recipient, if (ret == WOLFCOSE_SUCCESS) { recipientAlgId = recipientHdr.alg; + } +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + if ((ret == WOLFCOSE_SUCCESS) && + (recipientAlgId == WOLFCOSE_ALG_HPKE_0_KE) && + (recipientAlgProtected == 0)) { + ret = WOLFCOSE_E_COSE_BAD_HDR; + } +#endif + if (ret == WOLFCOSE_SUCCESS) { ret = wolfCose_ValidateRecipientKeyAlg(recipient->key, recipientAlgId, alg); } @@ -9831,6 +10740,11 @@ int wc_CoseEncrypt_Decrypt(const WOLFCOSE_RECIPIENT* recipient, if (recipientAlgId == WOLFCOSE_ALG_DIRECT) { recipModeOk = 1; } +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + else if (recipientAlgId == WOLFCOSE_ALG_HPKE_0_KE) { + recipModeOk = 1; + } +#endif #if defined(WOLFCOSE_ECDH_ES_DIRECT) && defined(HAVE_ECC) && defined(HAVE_HKDF) else if (wolfCose_IsEcdhEsDirectAlg(recipientAlgId) != 0) { recipModeOk = 1; @@ -9888,6 +10802,21 @@ int wc_CoseEncrypt_Decrypt(const WOLFCOSE_RECIPIENT* recipient, #endif /* [2] wrapped CEK */ +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + if ((ret == WOLFCOSE_SUCCESS) && + (recipientAlgId == WOLFCOSE_ALG_HPKE_0_KE)) { + ret = wc_CBOR_DecodeBstr(&ctx, &hpkeWrappedCekData, + &hpkeWrappedCekLen); + if ((ret == WOLFCOSE_SUCCESS) && + (hpkeWrappedCekLen < WOLFCOSE_HPKE_0_TAG_SZ)) { + ret = WOLFCOSE_E_CBOR_MALFORMED; + } + if (ret == WOLFCOSE_SUCCESS) { + useHpke = 1; + } + } + else +#endif #if defined(WOLFCOSE_KEY_WRAP) if ((ret == WOLFCOSE_SUCCESS) && (wolfCose_IsKeyWrapAlg(recipientAlgId) != 0)) { @@ -9949,6 +10878,33 @@ int wc_CoseEncrypt_Decrypt(const WOLFCOSE_RECIPIENT* recipient, } /* Derive/validate decryption key */ +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + if ((ret == WOLFCOSE_SUCCESS) && (useHpke != 0)) { + if ((keyLen > (SIZE_MAX - WOLFCOSE_HPKE_0_TAG_SZ)) || + (hpkeWrappedCekLen != + (keyLen + WOLFCOSE_HPKE_0_TAG_SZ))) { + ret = WOLFCOSE_E_COSE_BAD_HDR; + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_Hpke0BuildRecipientInfo(alg, + recipientProtectedData, recipientProtectedLen, + scratch, scratchSz, &hpkeRecipientInfoLen); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_Hpke0Open(recipient->key, + WOLFCOSE_ALG_HPKE_0_KE, + scratch, hpkeRecipientInfoLen, + NULL, 0u, + recipUnprotHdr.hpkeEk, recipUnprotHdr.hpkeEkLen, + hpkeWrappedCekData, hpkeWrappedCekLen, + cekHpke, sizeof(cekHpke)); + } + if (ret == WOLFCOSE_SUCCESS) { + decKey = cekHpke; + } + } + else +#endif #if defined(WOLFCOSE_ECDH_ES_DIRECT) && defined(HAVE_ECC) && defined(HAVE_HKDF) if ((ret == WOLFCOSE_SUCCESS) && (useEcdhEs != 0)) { WOLFCOSE_KEY* recipientKey = recipient->key; @@ -10136,6 +11092,9 @@ int wc_CoseEncrypt_Decrypt(const WOLFCOSE_RECIPIENT* recipient, #endif #if defined(WOLFCOSE_ECDH_ES_DIRECT) && defined(HAVE_ECC) && defined(HAVE_HKDF) (void)wolfCose_ForceZero(cek, sizeof(cek)); +#endif +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + (void)wolfCose_ForceZero(cekHpke, sizeof(cekHpke)); #endif if (scratch != NULL) { (void)wolfCose_ForceZero(scratch, scratchSz); diff --git a/tests/test_cose.c b/tests/test_cose.c index c1c3fd9..0daa0cb 100644 --- a/tests/test_cose.c +++ b/tests/test_cose.c @@ -42,7 +42,7 @@ #include "../src/wolfcose_internal.h" /* For testing internal helpers */ #include "test_suite.h" #include -#ifdef WOLFCOSE_HAVE_ES256 +#if defined(WOLFCOSE_HAVE_ES256) || defined(WOLFCOSE_HAVE_HPKE_0) #include #endif #ifdef WOLFCOSE_HAVE_EDDSA @@ -197,6 +197,61 @@ static int find_recipient_direct_alg(const uint8_t* msg, size_t msgLen, } #endif +#ifdef WOLFCOSE_HAVE_HPKE_0 +/* Decode fixed, public draft-vector literals without adding a wolfSSL + * Base16 feature requirement to HPKE test builds. */ +static int test_cose_hex_digit(uint8_t hex, uint8_t* digit) +{ + int ret = WOLFCOSE_SUCCESS; + + if (digit == NULL) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else if ((hex >= (uint8_t)'0') && (hex <= (uint8_t)'9')) { + *digit = (uint8_t)(hex - (uint8_t)'0'); + } + else if ((hex >= (uint8_t)'a') && (hex <= (uint8_t)'f')) { + *digit = (uint8_t)(hex - (uint8_t)'a' + 10u); + } + else if ((hex >= (uint8_t)'A') && (hex <= (uint8_t)'F')) { + *digit = (uint8_t)(hex - (uint8_t)'A' + 10u); + } + else { + ret = WOLFCOSE_E_INVALID_ARG; + } + + return ret; +} + +static int test_cose_hex_decode(const uint8_t* hex, size_t hexLen, + uint8_t* out, size_t outSz, size_t* outLen) +{ + size_t i; + uint8_t high = 0u; + uint8_t low = 0u; + int ret = WOLFCOSE_SUCCESS; + + if ((hex == NULL) || (out == NULL) || (outLen == NULL) || + ((hexLen & 1u) != 0u) || (outSz < (hexLen / 2u))) { + ret = WOLFCOSE_E_INVALID_ARG; + } + for (i = 0u; (ret == WOLFCOSE_SUCCESS) && (i < hexLen); i += 2u) { + ret = test_cose_hex_digit(hex[i], &high); + if (ret == WOLFCOSE_SUCCESS) { + ret = test_cose_hex_digit(hex[i + 1u], &low); + } + if (ret == WOLFCOSE_SUCCESS) { + out[i / 2u] = (uint8_t)((high << 4) | low); + } + } + if (ret == WOLFCOSE_SUCCESS) { + *outLen = hexLen / 2u; + } + + return ret; +} +#endif /* WOLFCOSE_HAVE_HPKE_0 */ + /* ----- Internal helper tests ----- */ static void test_wolfcose_force_zero(void) { @@ -9449,6 +9504,595 @@ static void test_cose_encrypt_multi_recipient(void) wc_CoseKey_Free(&key2); } +#endif /* WOLFCOSE_ENCRYPT && WOLFCOSE_HAVE_AESGCM && WOLFCOSE_KEY_WRAP */ + +#if defined(WOLFCOSE_HPKE_0_ENCRYPT) && defined(WOLFCOSE_HPKE_0_DECRYPT) +static void test_cose_hpke_encrypt0(void) +{ + WOLFCOSE_KEY recipientKey; + WOLFCOSE_KEY wrongKey; + WOLFCOSE_HDR hdr; + ecc_key recipientEcc; + ecc_key wrongEcc; + WC_RNG rng; + int ret = WOLFCOSE_SUCCESS; + int rngInited = 0; + int recipientEccInited = 0; + int wrongEccInited = 0; + int recipientKeyInited = 0; + int wrongKeyInited = 0; + uint8_t out[512]; + uint8_t detached[128]; + uint8_t plaintext[128]; + uint8_t scratch[256]; + size_t outLen = 0u; + size_t detachedLen = 0u; + size_t plaintextLen = 0u; + const uint8_t kid[] = "hpke-recipient"; + const uint8_t payload[] = "COSE HPKE integrated encryption"; + const uint8_t aad[] = "hpke external aad"; + const uint8_t wrongAad[] = "wrong hpke external aad"; + + TEST_LOG(" [COSE HPKE-0 Encrypt0]\n"); + (void)XMEMSET(&recipientEcc, 0, sizeof(recipientEcc)); + (void)XMEMSET(&wrongEcc, 0, sizeof(wrongEcc)); + + ret = wc_InitRng(&rng); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke encrypt0 rng init"); + if (ret == WOLFCOSE_SUCCESS) { + rngInited = 1; + ret = wc_ecc_init(&recipientEcc); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke encrypt0 recipient ecc init"); + } + if (ret == WOLFCOSE_SUCCESS) { + recipientEccInited = 1; + ret = wc_ecc_make_key(&rng, 32, &recipientEcc); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke encrypt0 recipient key make"); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_ecc_init(&wrongEcc); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke encrypt0 wrong ecc init"); + } + if (ret == WOLFCOSE_SUCCESS) { + wrongEccInited = 1; + ret = wc_ecc_make_key(&rng, 32, &wrongEcc); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke encrypt0 wrong key make"); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CoseKey_Init(&recipientKey); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke encrypt0 recipient key init"); + } + if (ret == WOLFCOSE_SUCCESS) { + recipientKeyInited = 1; + ret = wc_CoseKey_SetEcc(&recipientKey, WOLFCOSE_CRV_P256, + &recipientEcc); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke encrypt0 recipient key set"); + } + if (ret == WOLFCOSE_SUCCESS) { + recipientKey.alg = WOLFCOSE_ALG_HPKE_0; + recipientKey.hasPrivate = 0u; + ret = wc_CoseKey_Init(&wrongKey); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke encrypt0 wrong key init"); + } + if (ret == WOLFCOSE_SUCCESS) { + wrongKeyInited = 1; + ret = wc_CoseKey_SetEcc(&wrongKey, WOLFCOSE_CRV_P256, &wrongEcc); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke encrypt0 wrong key set"); + } + if (ret == WOLFCOSE_SUCCESS) { + wrongKey.alg = WOLFCOSE_ALG_HPKE_0; + ret = wc_CoseHpkeEncrypt0_Encrypt(&recipientKey, kid, + sizeof(kid) - 1u, payload, sizeof(payload) - 1u, + NULL, 0u, NULL, aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), out, sizeof(out), &outLen, &rng); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke encrypt0 encrypt"); + } + if (ret == WOLFCOSE_SUCCESS) { + recipientKey.hasPrivate = 1u; + (void)XMEMSET(&hdr, 0, sizeof(hdr)); + ret = wc_CoseHpkeEncrypt0_Decrypt(&recipientKey, out, outLen, + NULL, 0u, aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), &hdr, + plaintext, sizeof(plaintext), &plaintextLen); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke encrypt0 decrypt"); + TEST_ASSERT(plaintextLen == (sizeof(payload) - 1u), + "hpke encrypt0 payload length"); + TEST_ASSERT(memcmp(plaintext, payload, plaintextLen) == 0, + "hpke encrypt0 payload matches"); + TEST_ASSERT(hdr.alg == WOLFCOSE_ALG_HPKE_0, + "hpke encrypt0 protected algorithm"); + TEST_ASSERT((hdr.flags & WOLFCOSE_HDR_FLAG_HPKE_EK) != 0u, + "hpke encrypt0 encapsulated key present"); + TEST_ASSERT(hdr.hpkeEkLen == 65u, + "hpke encrypt0 P-256 encapsulated key length"); + TEST_ASSERT((hdr.kidLen == (sizeof(kid) - 1u)) && + (memcmp(hdr.kid, kid, hdr.kidLen) == 0), + "hpke encrypt0 kid decoded"); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CoseHpkeEncrypt0_Decrypt(&recipientKey, out, outLen, + NULL, 0u, wrongAad, sizeof(wrongAad) - 1u, + scratch, sizeof(scratch), &hdr, + plaintext, sizeof(plaintext), &plaintextLen); + TEST_ASSERT(ret == WOLFCOSE_E_COSE_DECRYPT_FAIL, + "hpke encrypt0 wrong aad rejected"); + } + if ((outLen > 0u) && (recipientKey.hasPrivate == 1u)) { + out[outLen - 1u] ^= 0x01u; + ret = wc_CoseHpkeEncrypt0_Decrypt(&recipientKey, out, outLen, + NULL, 0u, aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), &hdr, + plaintext, sizeof(plaintext), &plaintextLen); + TEST_ASSERT(ret == WOLFCOSE_E_COSE_DECRYPT_FAIL, + "hpke encrypt0 tampered ciphertext rejected"); + out[outLen - 1u] ^= 0x01u; + } + if ((outLen > 0u) && (wrongKeyInited != 0)) { + ret = wc_CoseHpkeEncrypt0_Decrypt(&wrongKey, out, outLen, + NULL, 0u, aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), &hdr, + plaintext, sizeof(plaintext), &plaintextLen); + TEST_ASSERT(ret == WOLFCOSE_E_COSE_DECRYPT_FAIL, + "hpke encrypt0 wrong private key rejected"); + } + if (recipientKeyInited != 0) { + recipientKey.hasPrivate = 0u; + ret = wc_CoseHpkeEncrypt0_Encrypt(&recipientKey, kid, + sizeof(kid) - 1u, payload, sizeof(payload) - 1u, + detached, sizeof(detached), &detachedLen, + aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), out, sizeof(out), &outLen, &rng); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke encrypt0 detached encrypt"); + if (ret == WOLFCOSE_SUCCESS) { + recipientKey.hasPrivate = 1u; + (void)XMEMSET(&hdr, 0, sizeof(hdr)); + ret = wc_CoseHpkeEncrypt0_Decrypt(&recipientKey, out, outLen, + detached, detachedLen, aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), &hdr, + plaintext, sizeof(plaintext), &plaintextLen); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke encrypt0 detached decrypt"); + TEST_ASSERT((hdr.flags & WOLFCOSE_HDR_FLAG_DETACHED) != 0u, + "hpke encrypt0 detached header flag"); + TEST_ASSERT((plaintextLen == (sizeof(payload) - 1u)) && + (memcmp(plaintext, payload, plaintextLen) == 0), + "hpke encrypt0 detached payload matches"); + } + } + + if (wrongKeyInited != 0) { + wc_CoseKey_Free(&wrongKey); + } + if (recipientKeyInited != 0) { + wc_CoseKey_Free(&recipientKey); + } + if (wrongEccInited != 0) { + (void)wc_ecc_free(&wrongEcc); + } + if (recipientEccInited != 0) { + (void)wc_ecc_free(&recipientEcc); + } + if (rngInited != 0) { + (void)wc_FreeRng(&rng); + } +} +#endif /* WOLFCOSE_HPKE_0_ENCRYPT && WOLFCOSE_HPKE_0_DECRYPT */ + +#if defined(WOLFCOSE_HPKE_0_DECRYPT) +/* draft-ietf-cose-hpke-26, Section 5.1, Figure 2 and Figure 4. */ +static void test_cose_hpke_encrypt0_draft_vector(void) +{ + static const uint8_t encodedHex[] = + "d08344a1011823a20443626f622358410457229bdd99407b384a9e59fa15" + "53224d58b106e9ebebdaa06d2126bd96757674847669966ecb0dcdf21af5" + "623f19f0b799b0cddf3ee930b739dd474f6282de0158253f3c1595e9d252" + "e816215a9ce73f47ba4b57acb06ecc39ca5a03a14108bbe7807af5688d61"; + static const uint8_t xHex[] = + "02a8e3315f96bc7355dbf85740c6d8e53fb070cd8ba5c419be49a91d789ef55c"; + static const uint8_t yHex[] = + "96b6621abf5ca532e042dc5c346c1ef0c9186b83cb122e50a46f1458de023d35"; + static const uint8_t dHex[] = + "eca39300147c91a2a65d17e00ea278b57a14178245bf5686d9a404cca1816b8e"; + /* The draft's stated plaintext omits the trailing LF carried by its + * published ciphertext. Test the bytes on the wire. */ + static const uint8_t expected[] = "This is the content.\n"; + WOLFCOSE_KEY recipientKey; + WOLFCOSE_HDR hdr; + ecc_key eccKey; + uint8_t encoded[160]; + uint8_t x[32]; + uint8_t y[32]; + uint8_t d[32]; + uint8_t scratch[256]; + uint8_t plaintext[64]; + size_t plaintextLen = 0u; + size_t encodedLen = sizeof(encoded); + size_t xLen = sizeof(x); + size_t yLen = sizeof(y); + size_t dLen = sizeof(d); + int ret = WOLFCOSE_SUCCESS; + int eccInited = 0; + int keyInited = 0; + + TEST_LOG(" [COSE HPKE Encrypt0 draft vector]\n"); + (void)XMEMSET(&recipientKey, 0, sizeof(recipientKey)); + (void)XMEMSET(&hdr, 0, sizeof(hdr)); + + ret = test_cose_hex_decode(encodedHex, sizeof(encodedHex) - 1u, + encoded, sizeof(encoded), &encodedLen); + TEST_ASSERT(ret == 0, "hpke encrypt0 vector decode"); + if (ret == 0) { + ret = test_cose_hex_decode(xHex, sizeof(xHex) - 1u, + x, sizeof(x), &xLen); + } + if (ret == 0) { + ret = test_cose_hex_decode(yHex, sizeof(yHex) - 1u, + y, sizeof(y), &yLen); + } + if (ret == 0) { + ret = test_cose_hex_decode(dHex, sizeof(dHex) - 1u, + d, sizeof(d), &dLen); + } + TEST_ASSERT((ret == 0) && (xLen == sizeof(x)) && + (yLen == sizeof(y)) && (dLen == sizeof(d)), + "hpke encrypt0 vector key decode"); + if (ret == 0) { + ret = wc_ecc_init(&eccKey); + TEST_ASSERT(ret == 0, "hpke encrypt0 vector ecc init"); + if (ret == 0) { + eccInited = 1; + } + } + if (ret == 0) { + ret = wc_ecc_import_unsigned(&eccKey, x, y, d, ECC_SECP256R1); + TEST_ASSERT(ret == 0, "hpke encrypt0 vector key import"); + } + if (ret == 0) { + ret = wc_CoseKey_Init(&recipientKey); + TEST_ASSERT(ret == 0, "hpke encrypt0 vector cose key init"); + if (ret == 0) { + keyInited = 1; + } + } + if (ret == 0) { + ret = wc_CoseKey_SetEcc(&recipientKey, WOLFCOSE_CRV_P256, &eccKey); + TEST_ASSERT(ret == 0, "hpke encrypt0 vector cose key set"); + recipientKey.alg = WOLFCOSE_ALG_HPKE_0; + } + if (ret == 0) { + ret = wc_CoseHpkeEncrypt0_Decrypt(&recipientKey, encoded, encodedLen, + NULL, 0u, NULL, 0u, scratch, sizeof(scratch), &hdr, + plaintext, sizeof(plaintext), &plaintextLen); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke encrypt0 draft vector decrypt"); + TEST_ASSERT((plaintextLen == (sizeof(expected) - 1u)) && + (memcmp(plaintext, expected, plaintextLen) == 0), + "hpke encrypt0 draft vector plaintext"); + TEST_ASSERT((hdr.alg == WOLFCOSE_ALG_HPKE_0) && + (hdr.kidLen == 3u) && (hdr.hpkeEkLen == 65u), + "hpke encrypt0 draft vector headers"); + } + + if (keyInited != 0) { + wc_CoseKey_Free(&recipientKey); + } + if (eccInited != 0) { + (void)wc_ecc_free(&eccKey); + } +} +#endif /* WOLFCOSE_HPKE_0_DECRYPT */ + +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) && \ + defined(WOLFCOSE_HPKE_0_KE_DECRYPT) +static void test_cose_hpke_key_encryption(void) +{ + WOLFCOSE_KEY recipientKey1; + WOLFCOSE_KEY recipientKey2; + WOLFCOSE_KEY wrongKey; + WOLFCOSE_RECIPIENT recipients[2]; + WOLFCOSE_RECIPIENT wrongRecipient; + WOLFCOSE_HDR hdr; + ecc_key recipientEcc1; + ecc_key recipientEcc2; + ecc_key wrongEcc; + WC_RNG rng; + int ret = WOLFCOSE_SUCCESS; + int rngInited = 0; + int recipientEcc1Inited = 0; + int recipientEcc2Inited = 0; + int wrongEccInited = 0; + int recipientKey1Inited = 0; + int recipientKey2Inited = 0; + int wrongKeyInited = 0; + uint8_t out[1024]; + uint8_t plaintext[128]; + uint8_t scratch[512]; + size_t outLen = 0u; + size_t plaintextLen = 0u; + size_t i; + int foundKid = 0; + const uint8_t kid1[] = "hpke-recipient-1"; + const uint8_t kid2[] = "hpke-recipient-2"; + const uint8_t payload[] = "COSE HPKE key encryption"; + const uint8_t iv[12] = { + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c + }; + const uint8_t aad[] = "COSE Encrypt external aad"; + + TEST_LOG(" [COSE HPKE-0-KE Multi-Recipient]\n"); + (void)XMEMSET(&recipientEcc1, 0, sizeof(recipientEcc1)); + (void)XMEMSET(&recipientEcc2, 0, sizeof(recipientEcc2)); + (void)XMEMSET(&wrongEcc, 0, sizeof(wrongEcc)); + + ret = wc_InitRng(&rng); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke rng init"); + if (ret == WOLFCOSE_SUCCESS) { + rngInited = 1; + ret = wc_ecc_init(&recipientEcc1); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke recipient 1 ecc init"); + } + if (ret == WOLFCOSE_SUCCESS) { + recipientEcc1Inited = 1; + ret = wc_ecc_make_key(&rng, 32, &recipientEcc1); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke recipient 1 key make"); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_ecc_init(&recipientEcc2); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke recipient 2 ecc init"); + } + if (ret == WOLFCOSE_SUCCESS) { + recipientEcc2Inited = 1; + ret = wc_ecc_make_key(&rng, 32, &recipientEcc2); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke recipient 2 key make"); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_ecc_init(&wrongEcc); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke wrong ecc init"); + } + if (ret == WOLFCOSE_SUCCESS) { + wrongEccInited = 1; + ret = wc_ecc_make_key(&rng, 32, &wrongEcc); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke wrong key make"); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CoseKey_Init(&recipientKey1); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke recipient 1 key init"); + } + if (ret == WOLFCOSE_SUCCESS) { + recipientKey1Inited = 1; + ret = wc_CoseKey_SetEcc(&recipientKey1, WOLFCOSE_CRV_P256, + &recipientEcc1); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke recipient 1 key set"); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CoseKey_Init(&recipientKey2); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke recipient 2 key init"); + } + if (ret == WOLFCOSE_SUCCESS) { + recipientKey2Inited = 1; + ret = wc_CoseKey_SetEcc(&recipientKey2, WOLFCOSE_CRV_P256, + &recipientEcc2); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke recipient 2 key set"); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CoseKey_Init(&wrongKey); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke wrong key init"); + } + if (ret == WOLFCOSE_SUCCESS) { + wrongKeyInited = 1; + ret = wc_CoseKey_SetEcc(&wrongKey, WOLFCOSE_CRV_P256, &wrongEcc); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke wrong key set"); + } + if (ret == WOLFCOSE_SUCCESS) { + recipientKey1.alg = WOLFCOSE_ALG_HPKE_0_KE; + recipientKey2.alg = WOLFCOSE_ALG_HPKE_0_KE; + wrongKey.alg = WOLFCOSE_ALG_HPKE_0_KE; + recipientKey1.hasPrivate = 0u; + recipientKey2.hasPrivate = 0u; + recipients[0].algId = WOLFCOSE_ALG_HPKE_0_KE; + recipients[0].key = &recipientKey1; + recipients[0].kid = kid1; + recipients[0].kidLen = sizeof(kid1) - 1u; + recipients[1].algId = WOLFCOSE_ALG_HPKE_0_KE; + recipients[1].key = &recipientKey2; + recipients[1].kid = kid2; + recipients[1].kidLen = sizeof(kid2) - 1u; + + ret = wc_CoseEncrypt_Encrypt(recipients, 2, + WOLFCOSE_ALG_A128GCM, iv, sizeof(iv), + payload, sizeof(payload) - 1u, + NULL, 0u, aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), out, sizeof(out), &outLen, &rng); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke encrypt"); + } + if (ret == WOLFCOSE_SUCCESS) { + recipientKey1.hasPrivate = 1u; + recipientKey2.hasPrivate = 1u; + (void)XMEMSET(&hdr, 0, sizeof(hdr)); + ret = wc_CoseEncrypt_Decrypt(&recipients[0], 0, out, outLen, + NULL, 0u, aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), &hdr, + plaintext, sizeof(plaintext), &plaintextLen); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke recipient 1 decrypt"); + TEST_ASSERT((plaintextLen == (sizeof(payload) - 1u)) && + (memcmp(plaintext, payload, plaintextLen) == 0), + "hpke ke recipient 1 payload matches"); + } + if (ret == WOLFCOSE_SUCCESS) { + (void)XMEMSET(&hdr, 0, sizeof(hdr)); + ret = wc_CoseEncrypt_Decrypt(&recipients[1], 1, out, outLen, + NULL, 0u, aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), &hdr, + plaintext, sizeof(plaintext), &plaintextLen); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke recipient 2 decrypt"); + TEST_ASSERT((plaintextLen == (sizeof(payload) - 1u)) && + (memcmp(plaintext, payload, plaintextLen) == 0), + "hpke ke recipient 2 payload matches"); + } + if (ret == WOLFCOSE_SUCCESS) { + wrongRecipient.algId = WOLFCOSE_ALG_HPKE_0_KE; + wrongRecipient.key = &wrongKey; + wrongRecipient.kid = kid1; + wrongRecipient.kidLen = sizeof(kid1) - 1u; + ret = wc_CoseEncrypt_Decrypt(&wrongRecipient, 0, out, outLen, + NULL, 0u, aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), &hdr, + plaintext, sizeof(plaintext), &plaintextLen); + TEST_ASSERT(ret == WOLFCOSE_E_COSE_DECRYPT_FAIL, + "hpke ke wrong private key rejected"); + } + if (outLen >= (sizeof(kid1) - 1u)) { + for (i = 0u; i <= (outLen - (sizeof(kid1) - 1u)); i++) { + if (memcmp(&out[i], kid1, sizeof(kid1) - 1u) == 0) { + foundKid = 1; + out[i] ^= 0x01u; + break; + } + } + TEST_ASSERT(foundKid != 0, "hpke ke recipient kid located"); + if (foundKid != 0) { + ret = wc_CoseEncrypt_Decrypt(&recipients[0], 0, out, outLen, + NULL, 0u, aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), &hdr, + plaintext, sizeof(plaintext), &plaintextLen); + TEST_ASSERT(ret == WOLFCOSE_E_COSE_DECRYPT_FAIL, + "hpke ke recipient protected header bound"); + out[i] ^= 0x01u; + } + } + + if (wrongKeyInited != 0) { + wc_CoseKey_Free(&wrongKey); + } + if (recipientKey2Inited != 0) { + wc_CoseKey_Free(&recipientKey2); + } + if (recipientKey1Inited != 0) { + wc_CoseKey_Free(&recipientKey1); + } + if (wrongEccInited != 0) { + (void)wc_ecc_free(&wrongEcc); + } + if (recipientEcc2Inited != 0) { + (void)wc_ecc_free(&recipientEcc2); + } + if (recipientEcc1Inited != 0) { + (void)wc_ecc_free(&recipientEcc1); + } + if (rngInited != 0) { + (void)wc_FreeRng(&rng); + } +} +#endif /* WOLFCOSE_HPKE_0_KE_ENCRYPT && WOLFCOSE_HPKE_0_KE_DECRYPT */ + +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) +/* draft-ietf-cose-hpke-26, Section 5.2, encodes a 16-byte IV for + * A128GCM. RFC 9053 fixes the COSE AES-GCM nonce size at 96 bits, so reject + * the non-conforming draft vector before attempting to unwrap its CEK. */ +static void test_cose_hpke_key_encryption_draft_vector_rejected(void) +{ + static const uint8_t encodedHex[] = + "d8608443a10101a1055089115f10ecc1c7fd834442cb87929bc15825534d" + "b92f5366e3cadd096774a9576bb8d8867e75ea38c329ecfc7b8793c5a4ae" + "9603e5b0b6818349a201182e0443626f62a12358410417cd85837981ddb1" + "4963061ab5fb7308988eb922f87cf6cf6ef83556f7657922c9815947e41b" + "9bc932e48c6f1c4677d9a5506a30d694587628b5193a4cde2f3f58204b50" + "8a340e463c317f4e62fb8d08c887cac4788087ad022562d05855a50ca4a0"; + static const uint8_t xHex[] = + "d832916778598ea6203af974c97b45970ac0266fc6a3b7f213ba9f8b591b9297"; + static const uint8_t yHex[] = + "8d9410599a8e83d00eb46d67b34d4dac8fbd4b8b1f08864599659cee9ef09184"; + static const uint8_t dHex[] = + "b1162c568efcba91c8e4e82f66e36b45aa10bc55228cf65ecd3bb29cfb09f989"; + WOLFCOSE_KEY recipientKey; + WOLFCOSE_RECIPIENT recipient; + WOLFCOSE_HDR hdr; + ecc_key eccKey; + uint8_t encoded[256]; + uint8_t x[32]; + uint8_t y[32]; + uint8_t d[32]; + uint8_t scratch[256]; + uint8_t plaintext[64]; + size_t plaintextLen = 0u; + size_t encodedLen = sizeof(encoded); + size_t xLen = sizeof(x); + size_t yLen = sizeof(y); + size_t dLen = sizeof(d); + int ret = WOLFCOSE_SUCCESS; + int eccInited = 0; + int keyInited = 0; + + TEST_LOG(" [COSE HPKE key-encryption draft vector rejection]\n"); + (void)XMEMSET(&recipientKey, 0, sizeof(recipientKey)); + (void)XMEMSET(&hdr, 0, sizeof(hdr)); + + ret = test_cose_hex_decode(encodedHex, sizeof(encodedHex) - 1u, + encoded, sizeof(encoded), &encodedLen); + TEST_ASSERT(ret == 0, "hpke ke vector decode"); + if (ret == 0) { + ret = test_cose_hex_decode(xHex, sizeof(xHex) - 1u, + x, sizeof(x), &xLen); + } + if (ret == 0) { + ret = test_cose_hex_decode(yHex, sizeof(yHex) - 1u, + y, sizeof(y), &yLen); + } + if (ret == 0) { + ret = test_cose_hex_decode(dHex, sizeof(dHex) - 1u, + d, sizeof(d), &dLen); + } + TEST_ASSERT((ret == 0) && (xLen == sizeof(x)) && + (yLen == sizeof(y)) && (dLen == sizeof(d)), + "hpke ke vector key decode"); + if (ret == 0) { + ret = wc_ecc_init(&eccKey); + TEST_ASSERT(ret == 0, "hpke ke vector ecc init"); + if (ret == 0) { + eccInited = 1; + } + } + if (ret == 0) { + ret = wc_ecc_import_unsigned(&eccKey, x, y, d, ECC_SECP256R1); + TEST_ASSERT(ret == 0, "hpke ke vector key import"); + } + if (ret == 0) { + ret = wc_CoseKey_Init(&recipientKey); + TEST_ASSERT(ret == 0, "hpke ke vector cose key init"); + if (ret == 0) { + keyInited = 1; + } + } + if (ret == 0) { + ret = wc_CoseKey_SetEcc(&recipientKey, WOLFCOSE_CRV_P256, &eccKey); + TEST_ASSERT(ret == 0, "hpke ke vector cose key set"); + recipientKey.alg = WOLFCOSE_ALG_HPKE_0_KE; + } + if (ret == 0) { + recipient.algId = WOLFCOSE_ALG_HPKE_0_KE; + recipient.key = &recipientKey; + recipient.kid = NULL; + recipient.kidLen = 0u; + ret = wc_CoseEncrypt_Decrypt(&recipient, 0u, encoded, encodedLen, + NULL, 0u, NULL, 0u, scratch, sizeof(scratch), &hdr, + plaintext, sizeof(plaintext), &plaintextLen); + TEST_ASSERT(ret == WOLFCOSE_E_COSE_BAD_HDR, + "hpke ke draft vector rejects 16-byte GCM IV"); + } + + if (keyInited != 0) { + wc_CoseKey_Free(&recipientKey); + } + if (eccInited != 0) { + (void)wc_ecc_free(&eccKey); + } +} +#endif /* WOLFCOSE_HPKE_0_KE_DECRYPT */ + +#if defined(WOLFCOSE_ENCRYPT) && defined(WOLFCOSE_HAVE_AESGCM) && \ + defined(WOLFCOSE_KEY_WRAP) + static void test_cose_encrypt_with_aad(void) { WOLFCOSE_KEY key; @@ -11863,7 +12507,7 @@ static void test_cose_encrypt_direct_wrong_key_type(void) } #endif /* WOLFCOSE_HAVE_ES256 */ -#endif /* WOLFCOSE_ENCRYPT && WOLFCOSE_HAVE_AESGCM */ +#endif /* WOLFCOSE_ENCRYPT && WOLFCOSE_HAVE_AESGCM && WOLFCOSE_KEY_WRAP */ /* ----- COSE_Mac Multi-Recipient Tests (RFC 9052 Section 6.1) ----- */ #if defined(WOLFCOSE_MAC) && defined(WOLFCOSE_HAVE_HMAC256) @@ -23708,6 +24352,12 @@ int test_cose(void) #if defined(SIZE_MAX) && (SIZE_MAX > 0xFFFFFFFFUL) test_cose_encrypt0_word32_overflow_guard(); #endif +#if defined(WOLFCOSE_HPKE_0_ENCRYPT) && defined(WOLFCOSE_HPKE_0_DECRYPT) + test_cose_hpke_encrypt0(); +#endif +#if defined(WOLFCOSE_HPKE_0_DECRYPT) + test_cose_hpke_encrypt0_draft_vector(); +#endif #endif /* ChaCha20-Poly1305 encryption tests */ @@ -23842,6 +24492,13 @@ int test_cose(void) #ifdef WOLFCOSE_HAVE_ES256 test_cose_encrypt_direct_wrong_key_type(); #endif +#endif +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) && \ + defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + test_cose_hpke_key_encryption(); +#endif +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + test_cose_hpke_key_encryption_draft_vector_rejected(); #endif /* Multi-recipient MAC tests */ From fd082ff86d356df7e1f9b1ea729480340662b337 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Tue, 1 Sep 2026 16:38:22 -0700 Subject: [PATCH 02/11] Add COSE HPKE example and CLI coverage --- .github/workflows/cose-hpke.yml | 88 ++++ Makefile | 60 ++- docs/Getting-Started.md | 61 +++ docs/Testing.md | 30 ++ examples/hpke_demo.c | 220 ++++++++++ include/wolfcose/settings.h | 2 +- scripts/cmdline-test.sh | 86 ++++ tools/wolfcose_tool.c | 714 +++++++++++++++++++++++++++++++- 8 files changed, 1255 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/cose-hpke.yml create mode 100644 examples/hpke_demo.c diff --git a/.github/workflows/cose-hpke.yml b/.github/workflows/cose-hpke.yml new file mode 100644 index 0000000..4825a35 --- /dev/null +++ b/.github/workflows/cose-hpke.yml @@ -0,0 +1,88 @@ +name: Experimental COSE-HPKE + +on: + push: + branches: [ 'main', 'release/**' ] + pull_request: + branches: [ '*' ] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + cose-hpke: + name: HPKE P0 unit, example, and CLI coverage + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y autoconf automake libtool + + - name: Build HPKE-enabled wolfSSL + run: | + git clone --depth 1 https://github.com/wolfSSL/wolfssl.git "$HOME/wolfssl" + cd "$HOME/wolfssl" + ./autogen.sh + ./configure --enable-ecc --enable-ed25519 --enable-ed448 \ + --enable-curve25519 --enable-curve448 \ + --enable-aesgcm --enable-aesccm \ + --enable-sha384 --enable-sha512 \ + --enable-keygen --enable-hkdf --enable-aeskeywrap \ + --enable-chacha --enable-poly1305 --enable-mldsa \ + --enable-rsapss --enable-hpke \ + --prefix="$HOME/wolfssl-install" + make -j"$(nproc)" + make install + + - name: Run experimental HPKE unit tests + run: | + export WOLFSSL_DIR="$HOME/wolfssl-install" + export LD_LIBRARY_PATH="$WOLFSSL_DIR/lib" + HPKE_FLAGS="-DWOLFCOSE_ENABLE_HPKE_0_ENCRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_DECRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT" + WOLFCOSE_CFLAGS="-std=c99 -DHAVE_ANONYMOUS_INLINE_AGGREGATES=1 \ + -Os -Wall -Wextra -Wpedantic -Wshadow -Wconversion -Wvla \ + -Werror=vla -I./include -I$WOLFSSL_DIR/include $HPKE_FLAGS" + make clean + make test CFLAGS="$WOLFCOSE_CFLAGS" \ + LDFLAGS="-L$WOLFSSL_DIR/lib -lwolfssl" + + - name: Run experimental HPKE example + run: | + export WOLFSSL_DIR="$HOME/wolfssl-install" + export LD_LIBRARY_PATH="$WOLFSSL_DIR/lib" + WOLFCOSE_CFLAGS="-std=c99 -DHAVE_ANONYMOUS_INLINE_AGGREGATES=1 \ + -Os -Wall -Wextra -Wpedantic -Wshadow -Wconversion -Wvla \ + -Werror=vla -I./include -I$WOLFSSL_DIR/include" + make clean + make hpke-demo CFLAGS="$WOLFCOSE_CFLAGS" \ + LDFLAGS="-L$WOLFSSL_DIR/lib -lwolfssl" + + - name: Run experimental HPKE strict C99 check + run: | + export WOLFSSL_DIR="$HOME/wolfssl-install" + make c99-hpke-check WOLFSSL_INC="$WOLFSSL_DIR/include" + + - name: Run experimental HPKE CLI commands + run: | + export WOLFSSL_DIR="$HOME/wolfssl-install" + export LD_LIBRARY_PATH="$WOLFSSL_DIR/lib" + HPKE_FLAGS="-DWOLFCOSE_ENABLE_HPKE_0_ENCRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_DECRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT" + WOLFCOSE_CFLAGS="-std=c99 -DHAVE_ANONYMOUS_INLINE_AGGREGATES=1 \ + -Os -Wall -Wextra -Wpedantic -Wshadow -Wconversion -Wvla \ + -Werror=vla -I./include -I$WOLFSSL_DIR/include $HPKE_FLAGS" + make clean + EXPECT_HPKE=true EXPECT_PQC=true make cmdline-test \ + CFLAGS="$WOLFCOSE_CFLAGS" \ + LDFLAGS="-L$WOLFSSL_DIR/lib -lwolfssl" diff --git a/Makefile b/Makefile index dc8d21a..2830627 100644 --- a/Makefile +++ b/Makefile @@ -83,6 +83,7 @@ TOOL_BIN = tools/wolfcose_tool DEMO_SRC = examples/lifecycle_demo.c DEMO_BIN = examples/lifecycle_demo ENC_DEMO = examples/encrypt0_demo +HPKE_DEMO = examples/hpke_demo MAC_DEMO = examples/mac0_demo SIGN1_DEMO = examples/sign1_demo LEANV_DEMO = examples/sign1_verify_lean @@ -103,7 +104,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 comprehensive scenarios interop-tcose c99-check experimental-check clean FORCE +.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 hpke-demo lean-verify mldsa-demo mldsa-verify comprehensive scenarios interop-tcose c99-check c99-hpke-check experimental-check clean FORCE # --- Core library --- all: $(LIB_A) @@ -386,6 +387,19 @@ demos: $(LIB_A) ./$(MAC_DEMO) ./$(SIGN1_DEMO) +# --- Experimental COSE-HPKE example --- +# The library is compiled directly because HPKE is intentionally opt-in and +# the default libwolfcose.a does not contain the experimental paths. +hpke-demo: + $(CC) $(CFLAGS) -DWOLFCOSE_EXPERIMENTAL \ + -DWOLFCOSE_ENABLE_HPKE_0_ENCRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_DECRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT \ + -o $(HPKE_DEMO) $(HPKE_DEMO).c $(SRC) $(LDFLAGS) $(LDLIBS) + @echo "=== Running experimental COSE-HPKE example ===" + ./$(HPKE_DEMO) + # --- Lean verify-only example (WOLFCOSE_LEAN_VERIFY) --- # Compiles the wolfCOSE sources directly with the lean macro instead of the full # prebuilt library, so the example exercises the minimal verify-only profile. @@ -481,7 +495,7 @@ C99_FLAGS = -std=c99 -pedantic-errors -Werror -Wall -Wextra -Wshadow -Wconversio -Wvla -DHAVE_ANONYMOUS_INLINE_AGGREGATES=1 \ -I./include $(C99_WOLFSSL_CFLAGS) $(EXTRA_CFLAGS) C99_SRC = $(SRC) $(TEST_SRC) $(TOOL_SRC) $(DEMO_SRC) \ - $(ENC_DEMO).c $(MAC_DEMO).c $(SIGN1_DEMO).c \ + $(ENC_DEMO).c $(HPKE_DEMO).c $(MAC_DEMO).c $(SIGN1_DEMO).c \ $(COMP_SIGN).c $(COMP_ENCRYPT).c $(COMP_MAC).c $(COMP_ERRORS).c \ $(SCEN_FIRMWARE).c $(SCEN_MULTIPARTY).c $(SCEN_IOTFLEET).c \ $(SCEN_SENSOR).c $(SCEN_BROADCAST).c $(EXTSIGN_DEMO).c @@ -490,6 +504,24 @@ C99_SRC = $(SRC) $(TEST_SRC) $(TOOL_SRC) $(DEMO_SRC) \ # conditionally-compiled translation unit, not just the default subset. C99_CONFIGS = "" "-DWOLFCOSE_FLOAT" "-DWOLFCOSE_ENABLE_EXT_SIGN" \ "-DWOLFCOSE_ENABLE_EXT_SIGN -DWOLFCOSE_NO_EDDSA -DWOLFCOSE_NO_ED448" +HPKE_C99_CONFIG = -DWOLFCOSE_EXPERIMENTAL -DWOLFCOSE_BUILD_TOOL \ + -DWOLFCOSE_ENABLE_HPKE_0_ENCRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_DECRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT +HPKE_C99_CONFIGS = \ + "-DWOLFCOSE_EXPERIMENTAL -DWOLFCOSE_BUILD_TOOL -DWOLFCOSE_ENABLE_HPKE_0" \ + "-DWOLFCOSE_EXPERIMENTAL -DWOLFCOSE_BUILD_TOOL -DWOLFCOSE_ENABLE_HPKE_0_KE" \ + "-DWOLFCOSE_EXPERIMENTAL -DWOLFCOSE_BUILD_TOOL -DWOLFCOSE_ENABLE_HPKE_0_ENCRYPT" \ + "-DWOLFCOSE_EXPERIMENTAL -DWOLFCOSE_BUILD_TOOL -DWOLFCOSE_ENABLE_HPKE_0_DECRYPT" \ + "-DWOLFCOSE_EXPERIMENTAL -DWOLFCOSE_BUILD_TOOL -DWOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT" \ + "-DWOLFCOSE_EXPERIMENTAL -DWOLFCOSE_BUILD_TOOL -DWOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT" \ + "$(HPKE_C99_CONFIG)" \ + "$(HPKE_C99_CONFIG) -DNO_ECC256 -DHAVE_ALL_CURVES -DHAVE_ECC_KOBLITZ" +# Prefer the explicitly selected HPKE backend over an unrelated host install. +# One-way builds leave round-trip-only test helpers unused; this gate judges +# gated wolfCOSE syntax while treating backend headers as system headers. +HPKE_C99_FLAGS = $(C99_FLAGS) -Wno-unused-function c99-check: @for cfg in $(C99_CONFIGS); do \ @@ -528,14 +560,36 @@ experimental-check: @$(EXP_TU) | \ $(CC) $(EXP_FLAGS) -DWOLFCOSE_ENABLE_EXPERIMENTAL_EXAMPLE \ -DWOLFCOSE_EXPERIMENTAL -fsyntax-only -x c - + @echo " EXP COSE-HPKE without acknowledgement (expect error)" + @if $(EXP_TU) | \ + $(CC) $(EXP_FLAGS) -DWOLFCOSE_ENABLE_HPKE_0_ENCRYPT \ + -fsyntax-only -x c - 2>experimental-check.err; then \ + echo "FAIL: COSE-HPKE compiled without WOLFCOSE_EXPERIMENTAL"; \ + rm -f experimental-check.err; exit 1; \ + fi + @grep -q WOLFCOSE_EXPERIMENTAL experimental-check.err || { \ + echo "FAIL: COSE-HPKE gate error did not mention WOLFCOSE_EXPERIMENTAL"; \ + cat experimental-check.err; rm -f experimental-check.err; exit 1; } + @rm -f experimental-check.err @echo " EXP normal build (expect pass, zero experimental code)" @$(EXP_TU) | \ $(CC) $(EXP_FLAGS) -fsyntax-only -x c - @echo "PASS: WOLFCOSE_EXPERIMENTAL gate enforced" +# Requires WOLFSSL_INC to name an HPKE-enabled wolfSSL installation. Keep this +# separate from c99-check so the normal strict gate remains backend-neutral. +c99-hpke-check: + @for cfg in $(HPKE_C99_CONFIGS); do \ + for f in $(C99_SRC); do \ + echo " C99 HPKE $$cfg $$f"; \ + $(CC) $(HPKE_C99_FLAGS) $$cfg -fsyntax-only $$f || exit 1; \ + done; \ + done + @echo "PASS: all experimental HPKE sources conform to ISO C99 (-pedantic-errors)" + # --- Cleanup --- clean: - rm -f $(OBJ) $(TEST_BIN) $(TOOL_BIN) $(DEMO_BIN) $(ENC_DEMO) $(MAC_DEMO) \ + rm -f $(OBJ) $(TEST_BIN) $(TOOL_BIN) $(DEMO_BIN) $(ENC_DEMO) $(HPKE_DEMO) $(MAC_DEMO) \ $(EXTSIGN_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) \ diff --git a/docs/Getting-Started.md b/docs/Getting-Started.md index 8c5d11e..add0376 100644 --- a/docs/Getting-Started.md +++ b/docs/Getting-Started.md @@ -58,6 +58,7 @@ You can enable only the algorithms you need: | ChaCha20-Poly1305 | `--enable-chacha --enable-poly1305` | | ECDH-ES key agreement | `--enable-ecc --enable-hkdf` | | AES Key Wrap | `--enable-aeskeywrap` (wolfSSL 5.9.0+) | +| Experimental COSE-HPKE P0 | `--enable-hpke --enable-ecc --enable-aesgcm --enable-keygen` | | RSA-PSS signing | `--enable-rsapss --enable-keygen` | | ML-DSA (post-quantum) | `--enable-mldsa` | | AES-MAC | `--enable-aescbc` | @@ -113,6 +114,8 @@ paths without a wolfSSL installation. | `make tool-test` | Round-trip self-test for all 17 algorithms | | `make demo` | Build and run lifecycle demo (11 algorithms) | | `make demos` | Build and run all basic demos | +| `make hpke-demo` | Build and run the opt-in experimental COSE-HPKE P0 demo | +| `make c99-hpke-check` | Strict C99 syntax check for all experimental HPKE paths (HPKE-enabled wolfSSL required) | | `make comprehensive` | Build and run comprehensive algorithm tests (~240 tests) | | `make scenarios` | Build and run real-world scenario examples | | `make coverage` | Run tests with gcov coverage | @@ -241,6 +244,64 @@ int main(void) } ``` +## Experimental COSE-HPKE P0 + +COSE-HPKE tracks an active Internet-Draft, so it is disabled in every build, +including a normal non-lean build. It currently implements the P0 subset: +HPKE base mode with DHKEM(P-256, HKDF-SHA256), HKDF-SHA256, and AES-128-GCM. +See [Configuration Macros](Macros.md#cose-hpke-experimental) for the complete +operation and compile-out gates. + +Build wolfSSL with HPKE support, then enable the exact send and receive paths +your application needs. The standalone example enables all four paths and +demonstrates both one-recipient `COSE_Encrypt0` and two-recipient +`COSE_Encrypt` key encryption: + +```bash +cd wolfssl +./configure --enable-cryptonly --enable-hpke --enable-ecc --enable-aesgcm \ + --enable-keygen +make + +cd ../wolfCOSE +make hpke-demo \ + CFLAGS="-std=c99 -I./include -I/path/to/wolfssl" \ + LDFLAGS="-L/path/to/wolfssl -lwolfssl" +``` + +The command-line tool is compiled with the same operation gates. `keygen -p` +exports a public-only COSE_Key; keep the corresponding `-o` private key on the +recipient. The direct commands are for HPKE-0, while the `hpke-ke-*` commands +use one independently HPKE-protected CEK for every recipient: + +```bash +# Build the tool with the four WOLFCOSE_ENABLE_HPKE_0_* operation macros. +./tools/wolfcose_tool keygen -a HPKE-0 \ + -o recipient.private.cbor -p recipient.public.cbor +./tools/wolfcose_tool hpke0-enc -k recipient.public.cbor \ + -i config.bin -o config.hpke.cbor +./tools/wolfcose_tool hpke0-dec -k recipient.private.cbor \ + -i config.hpke.cbor -o config.out + +./tools/wolfcose_tool keygen -a HPKE-0-KE \ + -o recipient-a.private.cbor -p recipient-a.public.cbor +./tools/wolfcose_tool keygen -a HPKE-0-KE \ + -o recipient-b.private.cbor -p recipient-b.public.cbor +./tools/wolfcose_tool hpke-ke-enc -a A128GCM \ + -k recipient-a.public.cbor -k recipient-b.public.cbor \ + -i config.bin -o config.multi.cbor +./tools/wolfcose_tool hpke-ke-dec -k recipient-b.private.cbor -r 1 \ + -i config.multi.cbor -o config.out +``` + +`-r` is a zero-based recipient index. The tool limits its HPKE-0-KE command +paths to `WOLFCOSE_TOOL_MAX_HPKE_RECIPIENTS` recipient keys and indices, four +by default, which can be reduced for constrained integrations. Its output +buffers reserve framing for the fixed P-256 HPKE envelope, so an input at the +configured `WOLFCOSE_TOOL_MAX_MSG` limit remains usable. HPKE base mode +authenticates the recipient, not the sender. Sign or MAC the resulting message +when sender authentication is required. + ## Quick Start: Post-Quantum Signing (ML-DSA) ```c diff --git a/docs/Testing.md b/docs/Testing.md index 07790c9..d7bc686 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -24,6 +24,34 @@ make tool-test Round-trip self-tests for all 17 supported CLI algorithms. Each algorithm is tested with key generation, operation, and verification. +### Experimental COSE-HPKE Tests + +COSE-HPKE P0 is off by default and therefore has a dedicated opt-in test path. +Build against a wolfSSL configured with `--enable-hpke --enable-ecc +--enable-aesgcm --enable-keygen`, then enable the four operation macros: + +```bash +HPKE_FLAGS="-DWOLFCOSE_ENABLE_HPKE_0_ENCRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_DECRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT" + +make test EXTRA_CFLAGS="$HPKE_FLAGS" +make hpke-demo EXTRA_CFLAGS="$HPKE_FLAGS" +make c99-hpke-check WOLFSSL_INC=/path/to/hpke-enabled-wolfssl/include +EXPECT_HPKE=true make cmdline-test EXTRA_CFLAGS="$HPKE_FLAGS" +``` + +The command-line test performs public/private key export, HPKE-0 integrated +encryption, two-recipient HPKE-0-KE encryption and decryption at both recipient +indices, maximum-message round trips for both constructions, and a focused +`test -a` round trip for each construction. The dedicated strict C99 target +compiles every HPKE-gated library, test, tool, and example path under each +one-way operation gate, the complete P0 configuration, and wolfSSL's +`NO_ECC256` plus `HAVE_ALL_CURVES` configuration. GitHub Actions runs the same +coverage in +[Experimental COSE-HPKE](../.github/workflows/cose-hpke.yml). + ### Comprehensive Algorithm Tests ```bash @@ -139,6 +167,8 @@ wolfCOSE runs the following CI checks on every push and pull request: 3. **Comprehensive Tests**: ~240 algorithm combination tests 4. **Scenario Examples**: Real-world workflow tests 5. **Tool Tests**: CLI round-trip tests (17 algorithms) +6. **Experimental COSE-HPKE**: Opt-in P0 unit tests, example, CLI commands, + and both construction-specific self-tests ### Memory and Stack Bounds diff --git a/examples/hpke_demo.c b/examples/hpke_demo.c new file mode 100644 index 0000000..ad03d9f --- /dev/null +++ b/examples/hpke_demo.c @@ -0,0 +1,220 @@ +/* hpke_demo.c + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfCOSE. + * + * wolfCOSE 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 3 of the License, or + * (at your option) any later version. + * + * wolfCOSE 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 . + */ + +/* Experimental COSE-HPKE P0 demonstration. + * + * The draft binding is intentionally opt-in. This program demonstrates the + * two currently supported P0 constructions: + * - HPKE-0: one-recipient COSE_Encrypt0 integrated encryption + * - HPKE-0-KE: COSE_Encrypt with one HPKE-protected CEK per recipient + */ + +#include +#include + +#include +#include +#include + +#if defined(WOLFCOSE_HPKE_0_ENCRYPT) && \ + defined(WOLFCOSE_HPKE_0_DECRYPT) && \ + defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) && \ + defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + +#define HPKE_DEMO_RECIPIENTS 2u + +static int demo_hpke_encrypt0(void) +{ + static const uint8_t kid[] = "recipient-a"; + static const uint8_t payload[] = "HPKE-0 integrated encryption"; + WC_RNG rng; + ecc_key recipient; + WOLFCOSE_KEY recipientKey; + WOLFCOSE_HDR hdr; + uint8_t scratch[WOLFCOSE_MAX_SCRATCH_SZ]; + uint8_t cose[512]; + uint8_t plaintext[sizeof(payload)]; + size_t coseLen = 0u; + size_t plaintextLen = 0u; + int rngInit = 0; + int eccInit = 0; + int ret; + + printf("--- HPKE-0 COSE_Encrypt0 ---\n"); + + ret = wc_InitRng(&rng); + if (ret == 0) { + rngInit = 1; + ret = wc_ecc_init(&recipient); + } + if (ret == 0) { + eccInit = 1; + ret = wc_ecc_make_key(&rng, 32, &recipient); + } + if (ret == 0) { + wc_CoseKey_Init(&recipientKey); + ret = wc_CoseKey_SetEcc(&recipientKey, WOLFCOSE_CRV_P256, + &recipient); + recipientKey.alg = WOLFCOSE_ALG_HPKE_0; + recipientKey.hasPrivate = 0u; + } + if (ret == 0) { + ret = wc_CoseHpkeEncrypt0_Encrypt(&recipientKey, kid, + sizeof(kid) - 1u, payload, sizeof(payload) - 1u, + NULL, 0u, NULL, NULL, 0u, scratch, sizeof(scratch), cose, + sizeof(cose), &coseLen, &rng); + } + if (ret == 0) { + recipientKey.hasPrivate = 1u; + ret = wc_CoseHpkeEncrypt0_Decrypt(&recipientKey, cose, coseLen, + NULL, 0u, NULL, 0u, scratch, sizeof(scratch), &hdr, plaintext, + sizeof(plaintext), &plaintextLen); + } + if ((ret == 0) && + ((hdr.alg != WOLFCOSE_ALG_HPKE_0) || + (plaintextLen != (sizeof(payload) - 1u)) || + (memcmp(plaintext, payload, plaintextLen) != 0))) { + ret = -1; + } + + if (eccInit != 0) { + wc_ecc_free(&recipient); + } + if (rngInit != 0) { + wc_FreeRng(&rng); + } + + printf(" %s (%zu byte COSE_Encrypt0)\n", ret == 0 ? "PASS" : "FAIL", + coseLen); + return ret; +} + +static int demo_hpke_key_encryption(void) +{ + static const uint8_t kid[HPKE_DEMO_RECIPIENTS][12] = { + "recipient-a", "recipient-b" + }; + static const uint8_t payload[] = "HPKE-0-KE multi-recipient encryption"; + WC_RNG rng; + ecc_key recipientEcc[HPKE_DEMO_RECIPIENTS]; + WOLFCOSE_KEY recipientKey[HPKE_DEMO_RECIPIENTS]; + WOLFCOSE_RECIPIENT recipients[HPKE_DEMO_RECIPIENTS]; + WOLFCOSE_HDR hdr; + uint8_t iv[12]; + uint8_t scratch[WOLFCOSE_MAX_SCRATCH_SZ]; + uint8_t cose[1024]; + uint8_t plaintext[sizeof(payload)]; + size_t coseLen = 0u; + size_t plaintextLen = 0u; + size_t i; + size_t eccCount = 0u; + int rngInit = 0; + int ret; + + printf("--- HPKE-0-KE COSE_Encrypt ---\n"); + (void)memset(recipients, 0, sizeof(recipients)); + + ret = wc_InitRng(&rng); + if (ret == 0) { + rngInit = 1; + } + for (i = 0u; (ret == 0) && (i < HPKE_DEMO_RECIPIENTS); i++) { + ret = wc_ecc_init(&recipientEcc[i]); + if (ret == 0) { + eccCount++; + ret = wc_ecc_make_key(&rng, 32, &recipientEcc[i]); + } + if (ret == 0) { + wc_CoseKey_Init(&recipientKey[i]); + ret = wc_CoseKey_SetEcc(&recipientKey[i], WOLFCOSE_CRV_P256, + &recipientEcc[i]); + recipientKey[i].alg = WOLFCOSE_ALG_HPKE_0_KE; + recipientKey[i].hasPrivate = 0u; + } + if (ret == 0) { + recipients[i].algId = WOLFCOSE_ALG_HPKE_0_KE; + recipients[i].key = &recipientKey[i]; + recipients[i].kid = kid[i]; + recipients[i].kidLen = sizeof(kid[i]) - 1u; + } + } + if (ret == 0) { + ret = wc_RNG_GenerateBlock(&rng, iv, (word32)sizeof(iv)); + } + if (ret == 0) { + ret = wc_CoseEncrypt_Encrypt(recipients, HPKE_DEMO_RECIPIENTS, + WOLFCOSE_ALG_A128GCM, iv, sizeof(iv), payload, + sizeof(payload) - 1u, NULL, 0u, NULL, 0u, scratch, + sizeof(scratch), cose, sizeof(cose), &coseLen, &rng); + } + if (ret == 0) { + for (i = 0u; i < HPKE_DEMO_RECIPIENTS; i++) { + recipientKey[i].hasPrivate = 1u; + } + } + for (i = 0u; (ret == 0) && (i < HPKE_DEMO_RECIPIENTS); i++) { + plaintextLen = 0u; + ret = wc_CoseEncrypt_Decrypt(&recipients[i], i, cose, coseLen, + NULL, 0u, NULL, 0u, scratch, sizeof(scratch), &hdr, plaintext, + sizeof(plaintext), &plaintextLen); + if ((ret == 0) && + ((hdr.alg != WOLFCOSE_ALG_A128GCM) || + (plaintextLen != (sizeof(payload) - 1u)) || + (memcmp(plaintext, payload, plaintextLen) != 0))) { + ret = -1; + } + } + + while (eccCount > 0u) { + eccCount--; + wc_ecc_free(&recipientEcc[eccCount]); + } + if (rngInit != 0) { + wc_FreeRng(&rng); + } + + printf(" %s (%zu byte COSE_Encrypt, %u recipients)\n", + ret == 0 ? "PASS" : "FAIL", coseLen, + (unsigned int)HPKE_DEMO_RECIPIENTS); + return ret; +} + +int main(void) +{ + int ret; + + printf("Experimental COSE-HPKE P0 example\n\n"); + ret = demo_hpke_encrypt0(); + if (ret == 0) { + ret = demo_hpke_key_encryption(); + } + return ret == 0 ? 0 : 1; +} + +#else + +int main(void) +{ + fprintf(stderr, + "This example requires the four WOLFCOSE_ENABLE_HPKE_0_* macros.\n"); + return 1; +} + +#endif diff --git a/include/wolfcose/settings.h b/include/wolfcose/settings.h index 1102e6b..2572fff 100644 --- a/include/wolfcose/settings.h +++ b/include/wolfcose/settings.h @@ -551,7 +551,7 @@ extern "C" { defined(WOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT) #if !defined(HAVE_HPKE) || !defined(HAVE_ECC) || \ !defined(HAVE_AESGCM) || defined(NO_SHA256) || \ - defined(NO_ECC256) + (defined(NO_ECC256) && !defined(HAVE_ALL_CURVES)) #error "HPKE-0 requires wolfSSL HAVE_HPKE, HAVE_ECC, P-256, SHA-256, and HAVE_AESGCM" #endif #define WOLFCOSE_HAVE_HPKE_0 diff --git a/scripts/cmdline-test.sh b/scripts/cmdline-test.sh index 0b10637..6627be3 100755 --- a/scripts/cmdline-test.sh +++ b/scripts/cmdline-test.sh @@ -33,6 +33,12 @@ skip() { SKIP=$((SKIP+1)); printf ' SKIP %s (%s)\n' "$1" "${2:-not built}"; } # When an algorithm is expected to be built, a keygen failure is a FAIL, not a # silent skip. ML-DSA is only expected when the wolfSSL leg enabled PQC. EXPECT_PQC="${EXPECT_PQC:-false}" +# HPKE is experimental and disabled by default. The dedicated CI lane sets +# this so a missing opt-in path cannot be silently skipped. +EXPECT_HPKE="${EXPECT_HPKE:-false}" +# The default matches WOLFCOSE_TOOL_MAX_MSG. Set this when validating a tool +# compiled with a non-default message limit. +HPKE_TOOL_MAX_MSG="${HPKE_TOOL_MAX_MSG:-8192}" # keygen $alg $out $optional(0|1): on failure, skip if optional else FAIL. keygen_or() { if "$TOOL" keygen -a "$1" -o "$2" >/dev/null 2>&1; then return 0; fi @@ -40,6 +46,13 @@ keygen_or() { return 1 } +# HPKE keygen also writes the public recipient key used for encryption. +hpke_keygen_or() { + if "$TOOL" keygen -a "$1" -o "$2" -p "$3" >/dev/null 2>&1; then return 0; fi + if [ "$EXPECT_HPKE" = "true" ]; then bad "$1 keygen"; else skip "$1"; fi + return 1 +} + # Names exactly as wolfcose_tool's parser accepts them. SIGN_ALGS="ES256 EdDSA Ed448 ML-DSA-44 ML-DSA-65 ML-DSA-87" ENC_ALGS="A128GCM A192GCM A256GCM ChaCha20 AES-CCM" @@ -127,6 +140,79 @@ for A in $ENC_ALGS; do fi done +echo "== Experimental COSE-HPKE: keygen -> encrypt -> decrypt -> self-test ==" +H0_PRIV="$WORK/hpke0.private"; H0_PUB="$WORK/hpke0.public" +H0_COSE="$WORK/hpke0.cose"; H0_OUT="$WORK/hpke0.out" +if hpke_keygen_or "HPKE-0" "$H0_PRIV" "$H0_PUB"; then + if "$TOOL" hpke0-enc -k "$H0_PUB" -i "$IN" -o "$H0_COSE" >/dev/null 2>&1 && \ + "$TOOL" hpke0-dec -k "$H0_PRIV" -i "$H0_COSE" -o "$H0_OUT" >/dev/null 2>&1 && \ + cmp -s "$IN" "$H0_OUT"; then + ok "HPKE-0 enc/dec" + else + bad "HPKE-0 enc/dec" + fi + if "$TOOL" test -a "HPKE-0" >/dev/null 2>&1; then + ok "HPKE-0 self-test" + else + bad "HPKE-0 self-test" + fi + H0_MAX_IN="$WORK/hpke0-max.bin"; H0_MAX_COSE="$WORK/hpke0-max.cose" + H0_MAX_OUT="$WORK/hpke0-max.out" + if dd if=/dev/zero of="$H0_MAX_IN" bs="$HPKE_TOOL_MAX_MSG" count=1 \ + >/dev/null 2>&1 && \ + "$TOOL" hpke0-enc -k "$H0_PUB" -i "$H0_MAX_IN" -o "$H0_MAX_COSE" \ + >/dev/null 2>&1 && \ + "$TOOL" hpke0-dec -k "$H0_PRIV" -i "$H0_MAX_COSE" -o "$H0_MAX_OUT" \ + >/dev/null 2>&1 && \ + "$TOOL" info -i "$H0_MAX_COSE" >/dev/null 2>&1 && \ + cmp -s "$H0_MAX_IN" "$H0_MAX_OUT"; then + ok "HPKE-0 maximum message enc/dec" + else + bad "HPKE-0 maximum message enc/dec" + fi +else + skip "HPKE-0 command and self-test" +fi + +HKE0_PRIV="$WORK/hpke-ke-0.private"; HKE0_PUB="$WORK/hpke-ke-0.public" +HKE1_PRIV="$WORK/hpke-ke-1.private"; HKE1_PUB="$WORK/hpke-ke-1.public" +HKE_COSE="$WORK/hpke-ke.cose"; HKE0_OUT="$WORK/hpke-ke-0.out" +HKE1_OUT="$WORK/hpke-ke-1.out" +if hpke_keygen_or "HPKE-0-KE" "$HKE0_PRIV" "$HKE0_PUB" && \ + hpke_keygen_or "HPKE-0-KE" "$HKE1_PRIV" "$HKE1_PUB"; then + if "$TOOL" hpke-ke-enc -a A128GCM -k "$HKE0_PUB" -k "$HKE1_PUB" \ + -i "$IN" -o "$HKE_COSE" >/dev/null 2>&1 && \ + "$TOOL" hpke-ke-dec -k "$HKE0_PRIV" -r 0 -i "$HKE_COSE" \ + -o "$HKE0_OUT" >/dev/null 2>&1 && \ + "$TOOL" hpke-ke-dec -k "$HKE1_PRIV" -r 1 -i "$HKE_COSE" \ + -o "$HKE1_OUT" >/dev/null 2>&1 && \ + cmp -s "$IN" "$HKE0_OUT" && cmp -s "$IN" "$HKE1_OUT"; then + ok "HPKE-0-KE multi-recipient enc/dec" + else + bad "HPKE-0-KE multi-recipient enc/dec" + fi + if "$TOOL" test -a "HPKE-0-KE" >/dev/null 2>&1; then + ok "HPKE-0-KE self-test" + else + bad "HPKE-0-KE self-test" + fi + HKE_MAX_IN="$WORK/hpke-ke-max.bin"; HKE_MAX_COSE="$WORK/hpke-ke-max.cose" + HKE_MAX_OUT="$WORK/hpke-ke-max.out" + if dd if=/dev/zero of="$HKE_MAX_IN" bs="$HPKE_TOOL_MAX_MSG" count=1 \ + >/dev/null 2>&1 && \ + "$TOOL" hpke-ke-enc -a A128GCM -k "$HKE0_PUB" -k "$HKE1_PUB" \ + -i "$HKE_MAX_IN" -o "$HKE_MAX_COSE" >/dev/null 2>&1 && \ + "$TOOL" hpke-ke-dec -k "$HKE0_PRIV" -r 0 -i "$HKE_MAX_COSE" \ + -o "$HKE_MAX_OUT" >/dev/null 2>&1 && \ + cmp -s "$HKE_MAX_IN" "$HKE_MAX_OUT"; then + ok "HPKE-0-KE maximum message enc/dec" + else + bad "HPKE-0-KE maximum message enc/dec" + fi +else + skip "HPKE-0-KE command and self-test" +fi + echo "== MAC: keygen -> mac -> macverify ==" for A in $MAC_ALGS; do K="$WORK/mac.key"; C="$WORK/mac.cose" diff --git a/tools/wolfcose_tool.c b/tools/wolfcose_tool.c index f58ad81..a384dc8 100644 --- a/tools/wolfcose_tool.c +++ b/tools/wolfcose_tool.c @@ -28,6 +28,10 @@ * verify -k -i * enc -k -a -i -o <cose_file> * dec -k <keyfile> -i <cose_file> -o <plaintext> + * hpke0-enc -k <public-key> -i <plaintext> -o <cose_file> + * hpke0-dec -k <private-key> -i <cose_file> -o <plaintext> + * hpke-ke-enc -a <alg> -k <public-key> [-k <public-key> ...] -i <plaintext> -o <cose_file> + * hpke-ke-dec -k <private-key> [-r <recipient-index>] -i <cose_file> -o <plaintext> * info -i <cose_file> * * Key files: raw COSE_Key CBOR format. @@ -44,7 +48,7 @@ #include <wolfcose/wolfcose.h> #include <wolfssl/wolfcrypt/random.h> -#ifdef WOLFCOSE_HAVE_ECDSA +#if defined(WOLFCOSE_HAVE_ECDSA) || defined(WOLFCOSE_HAVE_HPKE_0) #include <wolfssl/wolfcrypt/ecc.h> #endif #ifdef WOLFCOSE_HAVE_EDDSA @@ -63,6 +67,9 @@ #include <stdio.h> #include <stdlib.h> #include <string.h> +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) +#include <errno.h> +#endif #ifndef WOLFCOSE_TOOL_MAX_MSG #define WOLFCOSE_TOOL_MAX_MSG 8192 @@ -78,6 +85,41 @@ #endif #endif +/* Bounds the command-line multi-recipient HPKE helper without limiting the + * library API. Integrators can reduce this for constrained tools. */ +#if defined(WOLFCOSE_HAVE_HPKE_0) +#ifndef WOLFCOSE_TOOL_MAX_HPKE_RECIPIENTS + #define WOLFCOSE_TOOL_MAX_HPKE_RECIPIENTS 4u +#endif +#if (WOLFCOSE_TOOL_MAX_HPKE_RECIPIENTS < 1u) + #error "WOLFCOSE_TOOL_MAX_HPKE_RECIPIENTS must be at least one" +#endif +#endif + +#if defined(WOLFCOSE_HPKE_0_ENCRYPT) || defined(WOLFCOSE_HPKE_0_DECRYPT) +/* Direct HPKE-0 adds protected and unprotected headers, a P-256 enc value, + * an AEAD tag, and CBOR framing. */ +#define WOLFCOSE_TOOL_HPKE_0_ENCODED_MAX \ + ((size_t)WOLFCOSE_TOOL_MAX_MSG + 128u) +#endif + +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) || \ + defined(WOLFCOSE_HPKE_0_KE_DECRYPT) +/* Each HPKE-0-KE recipient carries a fixed P-256 enc value and wrapped CEK. */ +#define WOLFCOSE_TOOL_HPKE_0_KE_ENCODED_MAX \ + ((size_t)WOLFCOSE_TOOL_MAX_MSG + 64u + \ + (128u * (size_t)WOLFCOSE_TOOL_MAX_HPKE_RECIPIENTS)) +#endif + +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) || \ + defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + #define WOLFCOSE_TOOL_MAX_COSE_MSG WOLFCOSE_TOOL_HPKE_0_KE_ENCODED_MAX +#elif defined(WOLFCOSE_HPKE_0_ENCRYPT) || defined(WOLFCOSE_HPKE_0_DECRYPT) + #define WOLFCOSE_TOOL_MAX_COSE_MSG WOLFCOSE_TOOL_HPKE_0_ENCODED_MAX +#else + #define WOLFCOSE_TOOL_MAX_COSE_MSG WOLFCOSE_TOOL_MAX_MSG +#endif + #define EXIT_USAGE 1 #define EXIT_CRYPTO 2 #define EXIT_IO 3 @@ -100,11 +142,29 @@ static void usage(void) "Usage: wolfcose_tool <command> [options]\n" "\n" "Commands:\n" +#if defined(WOLFCOSE_HAVE_HPKE_0) + " keygen -a <alg> -o <keyfile> [-p <public-keyfile>]\n" +#else " keygen -a <alg> -o <keyfile>\n" +#endif " sign -k <keyfile> -a <alg> -i <payload> -o <cose_file>\n" " verify -k <keyfile> -i <cose_file>\n" " enc -k <keyfile> -a <alg> -i <plaintext> -o <cose_file>\n" " dec -k <keyfile> -i <cose_file> -o <plaintext>\n" +#if defined(WOLFCOSE_HPKE_0_ENCRYPT) + " hpke0-enc -k <public-key> -i <plaintext> -o <cose_file>\n" +#endif +#if defined(WOLFCOSE_HPKE_0_DECRYPT) + " hpke0-dec -k <private-key> -i <cose_file> -o <plaintext>\n" +#endif +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) + " hpke-ke-enc -a <alg> -k <public-key> [-k <public-key> ...]" + " -i <plaintext> -o <cose_file>\n" +#endif +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + " hpke-ke-dec -k <private-key> [-r <recipient-index>]" + " -i <cose_file> -o <plaintext>\n" +#endif " mac -k <keyfile> -a <alg> -i <payload> -o <cose_file>\n" " macverify -k <keyfile> -i <cose_file>\n" " info -i <cose_file>\n" @@ -113,6 +173,9 @@ static void usage(void) "Algorithms: ES256, EdDSA, Ed448, PS256, PS384, PS512,\n" " ML-DSA-44, ML-DSA-65, ML-DSA-87,\n" " A128GCM, A192GCM, A256GCM, ChaCha20, AES-CCM,\n" +#if defined(WOLFCOSE_HAVE_HPKE_0) + " HPKE-0, HPKE-0-KE,\n" +#endif " HMAC256, HMAC384, HMAC512\n"); } @@ -183,6 +246,17 @@ static int parse_alg(const char* name, int32_t* alg) else if (strcmp(name, "HMAC512") == 0) { *alg = WOLFCOSE_ALG_HMAC512; } +#endif +#if defined(WOLFCOSE_HPKE_0_ENCRYPT) || defined(WOLFCOSE_HPKE_0_DECRYPT) + else if (strcmp(name, "HPKE-0") == 0) { + *alg = WOLFCOSE_ALG_HPKE_0; + } +#endif +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) || \ + defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + else if (strcmp(name, "HPKE-0-KE") == 0) { + *alg = WOLFCOSE_ALG_HPKE_0_KE; + } #endif else { fprintf(stderr, "Unknown algorithm: %s\n", name); @@ -233,6 +307,142 @@ static int write_file(const char* path, const uint8_t* buf, size_t len) return 0; } +#if defined(WOLFCOSE_HAVE_HPKE_0) +/* Generate a P-256 HPKE key pair and optionally export its public half. */ +static int tool_hpke_keygen(int32_t alg, const char* outPath, + const char* publicPath) +{ + WC_RNG rng; + ecc_key ecc; + WOLFCOSE_KEY coseKey; + uint8_t privateBuf[WOLFCOSE_TOOL_MAX_KEY]; + uint8_t publicBuf[WOLFCOSE_TOOL_MAX_KEY]; + size_t privateLen = 0u; + size_t publicLen = 0u; + int rngInit = 0; + int eccInit = 0; + int ret; + + ret = wc_InitRng(&rng); + if (ret == 0) { + rngInit = 1; + ret = wc_ecc_init(&ecc); + } + if (ret == 0) { + eccInit = 1; + ret = wc_ecc_make_key(&rng, 32, &ecc); + } + if (ret == 0) { + wc_CoseKey_Init(&coseKey); + ret = wc_CoseKey_SetEcc(&coseKey, WOLFCOSE_CRV_P256, &ecc); + coseKey.alg = alg; + } + if (ret == 0) { + ret = wc_CoseKey_Encode(&coseKey, privateBuf, sizeof(privateBuf), + &privateLen); + } + if ((ret == 0) && (publicPath != NULL)) { + ret = wc_CoseKey_Encode_ex(&coseKey, publicBuf, sizeof(publicBuf), + &publicLen, WOLFCOSE_KEY_PUBLIC_ONLY); + } + + if (eccInit != 0) { + wc_ecc_free(&ecc); + } + if (rngInit != 0) { + wc_FreeRng(&rng); + } + if (ret != 0) { + fprintf(stderr, "HPKE key generation failed: %d\n", ret); + tool_force_zero(privateBuf, sizeof(privateBuf)); + tool_force_zero(publicBuf, sizeof(publicBuf)); + return EXIT_CRYPTO; + } + + ret = write_file(outPath, privateBuf, privateLen); + if ((ret == 0) && (publicPath != NULL)) { + ret = write_file(publicPath, publicBuf, publicLen); + } + tool_force_zero(privateBuf, sizeof(privateBuf)); + tool_force_zero(publicBuf, sizeof(publicBuf)); + if (ret == 0) { + printf("Generated HPKE private key: %s (%zu bytes)\n", outPath, + privateLen); + if (publicPath != NULL) { + printf("Generated HPKE public key: %s (%zu bytes)\n", publicPath, + publicLen); + } + } + return ret; +} + +/* Decode an EC2 P-256 COSE_Key and enforce its HPKE algorithm binding. */ +static int tool_hpke_load_key(const char* path, int32_t alg, + WOLFCOSE_KEY* coseKey, ecc_key* ecc) +{ + uint8_t keyBuf[WOLFCOSE_TOOL_MAX_KEY]; + size_t keyLen = 0u; + int eccInit = 0; + int ret; + + ret = read_file(path, keyBuf, sizeof(keyBuf), &keyLen); + if (ret != 0) { + return ret; + } + wc_CoseKey_Init(coseKey); + ret = wc_ecc_init(ecc); + if (ret == 0) { + eccInit = 1; + ret = wc_CoseKey_SetEcc(coseKey, WOLFCOSE_CRV_P256, ecc); + } + if (ret == 0) { + ret = wc_CoseKey_Decode(coseKey, keyBuf, keyLen); + } + tool_force_zero(keyBuf, sizeof(keyBuf)); + if ((ret == 0) && (coseKey->alg != alg)) { + fprintf(stderr, "HPKE key has an unexpected algorithm binding\n"); + ret = WOLFCOSE_E_COSE_BAD_ALG; + } + if (ret != 0) { + fprintf(stderr, "HPKE key decode failed: %d\n", ret); + if (eccInit != 0) { + wc_ecc_free(ecc); + } + return EXIT_CRYPTO; + } + return 0; +} + +/* Return the nonce size required by the selected content encryption suite. */ +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) +static int tool_content_nonce_len(int32_t alg, size_t* nonceLen) +{ + if ((alg == WOLFCOSE_ALG_AES_CCM_16_64_128) || + (alg == WOLFCOSE_ALG_AES_CCM_16_64_256) || + (alg == WOLFCOSE_ALG_AES_CCM_16_128_128) || + (alg == WOLFCOSE_ALG_AES_CCM_16_128_256)) { + *nonceLen = 13u; + } + else if ((alg == WOLFCOSE_ALG_AES_CCM_64_64_128) || + (alg == WOLFCOSE_ALG_AES_CCM_64_64_256) || + (alg == WOLFCOSE_ALG_AES_CCM_64_128_128) || + (alg == WOLFCOSE_ALG_AES_CCM_64_128_256)) { + *nonceLen = 7u; + } + else if ((alg == WOLFCOSE_ALG_A128GCM) || + (alg == WOLFCOSE_ALG_A192GCM) || + (alg == WOLFCOSE_ALG_A256GCM) || + (alg == WOLFCOSE_ALG_CHACHA20_POLY1305)) { + *nonceLen = 12u; + } + else { + return EXIT_USAGE; + } + return 0; +} +#endif /* WOLFCOSE_HPKE_0_KE_ENCRYPT */ +#endif /* WOLFCOSE_HAVE_HPKE_0 */ + /* ----- keygen: generate a COSE key and write to file ----- */ static int tool_keygen(int32_t alg, const char* algStr, const char* outPath) { @@ -908,6 +1118,243 @@ static int tool_dec(const char* keyPath, const char* inPath, } #endif /* WOLFCOSE_HAVE_AESGCM || WOLFCOSE_HAVE_AESCCM || (WOLFCOSE_HAVE_CHACHA20) */ +/* ----- hpke0-enc: COSE HPKE-0 integrated encryption ----- */ +#if defined(WOLFCOSE_HPKE_0_ENCRYPT) +static int tool_hpke0_enc(const char* keyPath, const char* inPath, + const char* outPath) +{ + WOLFCOSE_KEY recipientKey; + ecc_key recipientEcc; + WC_RNG rng; + uint8_t msgBuf[WOLFCOSE_TOOL_MAX_MSG]; + uint8_t outBuf[WOLFCOSE_TOOL_HPKE_0_ENCODED_MAX]; + uint8_t scratch[WOLFCOSE_MAX_SCRATCH_SZ]; + size_t msgLen = 0u; + size_t outLen = 0u; + int eccLoaded = 0; + int rngInit = 0; + int ret; + + ret = tool_hpke_load_key(keyPath, WOLFCOSE_ALG_HPKE_0, &recipientKey, + &recipientEcc); + if (ret != 0) { + return ret; + } + eccLoaded = 1; + ret = read_file(inPath, msgBuf, sizeof(msgBuf), &msgLen); + if (ret != 0) { + goto exit; + } + ret = wc_InitRng(&rng); + if (ret == 0) { + rngInit = 1; + ret = wc_CoseHpkeEncrypt0_Encrypt(&recipientKey, NULL, 0u, msgBuf, + msgLen, NULL, 0u, NULL, NULL, 0u, scratch, sizeof(scratch), + outBuf, sizeof(outBuf), &outLen, &rng); + } + if (ret != 0) { + fprintf(stderr, "HPKE-0 encrypt failed: %d\n", ret); + ret = EXIT_CRYPTO; + goto exit; + } + ret = write_file(outPath, outBuf, outLen); + if (ret == 0) { + printf("HPKE-0 encrypted: %zu byte plaintext -> %zu byte " + "COSE_Encrypt0\n", msgLen, outLen); + } + +exit: + if (rngInit != 0) { + wc_FreeRng(&rng); + } + if (eccLoaded != 0) { + wc_ecc_free(&recipientEcc); + } + return ret; +} +#endif /* WOLFCOSE_HPKE_0_ENCRYPT */ + +/* ----- hpke0-dec: COSE HPKE-0 integrated decryption ----- */ +#if defined(WOLFCOSE_HPKE_0_DECRYPT) +static int tool_hpke0_dec(const char* keyPath, const char* inPath, + const char* outPath) +{ + WOLFCOSE_KEY recipientKey; + ecc_key recipientEcc; + WOLFCOSE_HDR hdr; + uint8_t msgBuf[WOLFCOSE_TOOL_HPKE_0_ENCODED_MAX]; + uint8_t plainBuf[WOLFCOSE_TOOL_MAX_MSG]; + uint8_t scratch[WOLFCOSE_MAX_SCRATCH_SZ]; + size_t msgLen = 0u; + size_t plainLen = 0u; + int eccLoaded = 0; + int ret; + + ret = tool_hpke_load_key(keyPath, WOLFCOSE_ALG_HPKE_0, &recipientKey, + &recipientEcc); + if (ret != 0) { + return ret; + } + eccLoaded = 1; + ret = read_file(inPath, msgBuf, sizeof(msgBuf), &msgLen); + if (ret != 0) { + goto exit; + } + ret = wc_CoseHpkeEncrypt0_Decrypt(&recipientKey, msgBuf, msgLen, NULL, + 0u, NULL, 0u, scratch, sizeof(scratch), &hdr, plainBuf, + sizeof(plainBuf), &plainLen); + if (ret != 0) { + fprintf(stderr, "HPKE-0 decrypt failed: %d\n", ret); + ret = EXIT_CRYPTO; + goto exit; + } + ret = write_file(outPath, plainBuf, plainLen); + if (ret == 0) { + printf("HPKE-0 decrypted: %zu byte COSE_Encrypt0 -> %zu byte " + "plaintext\n", msgLen, plainLen); + } + +exit: + if (eccLoaded != 0) { + wc_ecc_free(&recipientEcc); + } + return ret; +} +#endif /* WOLFCOSE_HPKE_0_DECRYPT */ + +/* ----- hpke-ke-enc: HPKE-0-KE multi-recipient COSE_Encrypt ----- */ +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) +static int tool_hpke_ke_enc(const char* const* keyPaths, size_t keyCount, + int32_t contentAlg, const char* inPath, + const char* outPath) +{ + WOLFCOSE_KEY recipientKey[WOLFCOSE_TOOL_MAX_HPKE_RECIPIENTS]; + WOLFCOSE_RECIPIENT recipients[WOLFCOSE_TOOL_MAX_HPKE_RECIPIENTS]; + ecc_key recipientEcc[WOLFCOSE_TOOL_MAX_HPKE_RECIPIENTS]; + WC_RNG rng; + uint8_t msgBuf[WOLFCOSE_TOOL_MAX_MSG]; + uint8_t outBuf[WOLFCOSE_TOOL_HPKE_0_KE_ENCODED_MAX]; + uint8_t scratch[WOLFCOSE_MAX_SCRATCH_SZ]; + uint8_t iv[13]; + size_t msgLen = 0u; + size_t outLen = 0u; + size_t ivLen = 0u; + size_t eccCount = 0u; + size_t i; + int rngInit = 0; + int ret; + + if ((keyCount == 0u) || (keyCount > WOLFCOSE_TOOL_MAX_HPKE_RECIPIENTS)) { + fprintf(stderr, "HPKE-0-KE supports 1 to %u recipient keys\n", + (unsigned int)WOLFCOSE_TOOL_MAX_HPKE_RECIPIENTS); + return EXIT_USAGE; + } + ret = tool_content_nonce_len(contentAlg, &ivLen); + if (ret != 0) { + fprintf(stderr, "Unsupported HPKE-0-KE content algorithm\n"); + return ret; + } + ret = read_file(inPath, msgBuf, sizeof(msgBuf), &msgLen); + if (ret != 0) { + return ret; + } + (void)memset(recipients, 0, sizeof(recipients)); + for (i = 0u; (ret == 0) && (i < keyCount); i++) { + ret = tool_hpke_load_key(keyPaths[i], WOLFCOSE_ALG_HPKE_0_KE, + &recipientKey[i], &recipientEcc[i]); + if (ret == 0) { + eccCount++; + recipients[i].algId = WOLFCOSE_ALG_HPKE_0_KE; + recipients[i].key = &recipientKey[i]; + } + } + if (ret == 0) { + ret = wc_InitRng(&rng); + if (ret == 0) { + rngInit = 1; + ret = wc_RNG_GenerateBlock(&rng, iv, (word32)ivLen); + } + } + if (ret == 0) { + ret = wc_CoseEncrypt_Encrypt(recipients, keyCount, contentAlg, iv, + ivLen, msgBuf, msgLen, NULL, 0u, NULL, 0u, scratch, + sizeof(scratch), outBuf, sizeof(outBuf), &outLen, &rng); + } + if (ret != 0) { + fprintf(stderr, "HPKE-0-KE encrypt failed: %d\n", ret); + ret = EXIT_CRYPTO; + goto exit; + } + ret = write_file(outPath, outBuf, outLen); + if (ret == 0) { + printf("HPKE-0-KE encrypted: %zu byte plaintext -> %zu byte " + "COSE_Encrypt (%zu recipients)\n", msgLen, outLen, keyCount); + } + +exit: + if (rngInit != 0) { + wc_FreeRng(&rng); + } + while (eccCount > 0u) { + eccCount--; + wc_ecc_free(&recipientEcc[eccCount]); + } + return ret; +} +#endif /* WOLFCOSE_HPKE_0_KE_ENCRYPT */ + +/* ----- hpke-ke-dec: HPKE-0-KE multi-recipient COSE_Encrypt decryption ----- */ +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) +static int tool_hpke_ke_dec(const char* keyPath, size_t recipientIndex, + const char* inPath, const char* outPath) +{ + WOLFCOSE_KEY recipientKey; + WOLFCOSE_RECIPIENT recipient; + WOLFCOSE_HDR hdr; + ecc_key recipientEcc; + uint8_t msgBuf[WOLFCOSE_TOOL_HPKE_0_KE_ENCODED_MAX]; + uint8_t plainBuf[WOLFCOSE_TOOL_MAX_MSG]; + uint8_t scratch[WOLFCOSE_MAX_SCRATCH_SZ]; + size_t msgLen = 0u; + size_t plainLen = 0u; + int eccLoaded = 0; + int ret; + + ret = tool_hpke_load_key(keyPath, WOLFCOSE_ALG_HPKE_0_KE, &recipientKey, + &recipientEcc); + if (ret != 0) { + return ret; + } + eccLoaded = 1; + ret = read_file(inPath, msgBuf, sizeof(msgBuf), &msgLen); + if (ret != 0) { + goto exit; + } + (void)memset(&recipient, 0, sizeof(recipient)); + recipient.algId = WOLFCOSE_ALG_HPKE_0_KE; + recipient.key = &recipientKey; + ret = wc_CoseEncrypt_Decrypt(&recipient, recipientIndex, msgBuf, msgLen, + NULL, 0u, NULL, 0u, scratch, sizeof(scratch), &hdr, plainBuf, + sizeof(plainBuf), &plainLen); + if (ret != 0) { + fprintf(stderr, "HPKE-0-KE decrypt failed: %d\n", ret); + ret = EXIT_CRYPTO; + goto exit; + } + ret = write_file(outPath, plainBuf, plainLen); + if (ret == 0) { + printf("HPKE-0-KE decrypted recipient %zu: %zu byte COSE_Encrypt -> " + "%zu byte plaintext\n", recipientIndex, msgLen, plainLen); + } + +exit: + if (eccLoaded != 0) { + wc_ecc_free(&recipientEcc); + } + return ret; +} +#endif /* WOLFCOSE_HPKE_0_KE_DECRYPT */ + /* ----- mac: COSE_Mac0 create ----- */ #if defined(WOLFCOSE_HAVE_HMAC) static int tool_mac(const char* keyPath, int32_t alg, @@ -999,7 +1446,7 @@ static int tool_macverify(const char* keyPath, const char* inPath) static int tool_info(const char* inPath) { int ret; - uint8_t msgBuf[WOLFCOSE_TOOL_MAX_MSG]; + uint8_t msgBuf[WOLFCOSE_TOOL_MAX_COSE_MSG]; size_t msgLen = 0; WOLFCOSE_CBOR_CTX ctx; WOLFCOSE_CBOR_ITEM item; @@ -1440,6 +1887,154 @@ static int test_enc_roundtrip(const char* name, int32_t alg, } #endif +/* HPKE-0 integrated-encryption round trip. */ +#if defined(WOLFCOSE_HPKE_0_ENCRYPT) && defined(WOLFCOSE_HPKE_0_DECRYPT) +static int test_hpke0_roundtrip(void) +{ + static const uint8_t payload[] = "wolfCOSE HPKE-0 roundtrip"; + WC_RNG rng; + ecc_key recipientEcc; + WOLFCOSE_KEY recipientKey; + WOLFCOSE_HDR hdr; + uint8_t scratch[WOLFCOSE_MAX_SCRATCH_SZ]; + uint8_t cose[512]; + uint8_t plaintext[sizeof(payload)]; + size_t coseLen = 0u; + size_t plaintextLen = 0u; + int rngInit = 0; + int eccInit = 0; + int ret; + + printf(" %-12s enc/dec ... ", "HPKE-0"); + ret = wc_InitRng(&rng); + if (ret == 0) { + rngInit = 1; + ret = wc_ecc_init(&recipientEcc); + } + if (ret == 0) { + eccInit = 1; + ret = wc_ecc_make_key(&rng, 32, &recipientEcc); + } + if (ret == 0) { + wc_CoseKey_Init(&recipientKey); + ret = wc_CoseKey_SetEcc(&recipientKey, WOLFCOSE_CRV_P256, + &recipientEcc); + recipientKey.alg = WOLFCOSE_ALG_HPKE_0; + recipientKey.hasPrivate = 0u; + } + if (ret == 0) { + ret = wc_CoseHpkeEncrypt0_Encrypt(&recipientKey, NULL, 0u, payload, + sizeof(payload) - 1u, NULL, 0u, NULL, NULL, 0u, scratch, + sizeof(scratch), cose, sizeof(cose), &coseLen, &rng); + } + if (ret == 0) { + recipientKey.hasPrivate = 1u; + ret = wc_CoseHpkeEncrypt0_Decrypt(&recipientKey, cose, coseLen, + NULL, 0u, NULL, 0u, scratch, sizeof(scratch), &hdr, plaintext, + sizeof(plaintext), &plaintextLen); + } + if ((ret == 0) && + ((hdr.alg != WOLFCOSE_ALG_HPKE_0) || + (plaintextLen != (sizeof(payload) - 1u)) || + (memcmp(plaintext, payload, plaintextLen) != 0))) { + ret = -1; + } + if (eccInit != 0) { + wc_ecc_free(&recipientEcc); + } + if (rngInit != 0) { + wc_FreeRng(&rng); + } + printf("%s\n", ret == 0 ? "PASS" : "FAIL"); + return ret; +} +#endif /* WOLFCOSE_HPKE_0_ENCRYPT && WOLFCOSE_HPKE_0_DECRYPT */ + +/* HPKE-0-KE uses one independently protected CEK for each recipient. */ +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) && \ + defined(WOLFCOSE_HPKE_0_KE_DECRYPT) +static int test_hpke_ke_roundtrip(void) +{ + enum { HPKE_TEST_RECIPIENTS = 2 }; + static const uint8_t payload[] = "wolfCOSE HPKE-0-KE roundtrip"; + WC_RNG rng; + ecc_key recipientEcc[HPKE_TEST_RECIPIENTS]; + WOLFCOSE_KEY recipientKey[HPKE_TEST_RECIPIENTS]; + WOLFCOSE_RECIPIENT recipients[HPKE_TEST_RECIPIENTS]; + WOLFCOSE_HDR hdr; + uint8_t iv[12]; + uint8_t scratch[WOLFCOSE_MAX_SCRATCH_SZ]; + uint8_t cose[1024]; + uint8_t plaintext[sizeof(payload)]; + size_t coseLen = 0u; + size_t plaintextLen = 0u; + size_t eccCount = 0u; + size_t i; + int rngInit = 0; + int ret; + + printf(" %-12s enc/dec ... ", "HPKE-0-KE"); + (void)memset(recipients, 0, sizeof(recipients)); + ret = wc_InitRng(&rng); + if (ret == 0) { + rngInit = 1; + } + for (i = 0u; (ret == 0) && (i < HPKE_TEST_RECIPIENTS); i++) { + ret = wc_ecc_init(&recipientEcc[i]); + if (ret == 0) { + eccCount++; + ret = wc_ecc_make_key(&rng, 32, &recipientEcc[i]); + } + if (ret == 0) { + wc_CoseKey_Init(&recipientKey[i]); + ret = wc_CoseKey_SetEcc(&recipientKey[i], WOLFCOSE_CRV_P256, + &recipientEcc[i]); + recipientKey[i].alg = WOLFCOSE_ALG_HPKE_0_KE; + recipientKey[i].hasPrivate = 0u; + } + if (ret == 0) { + recipients[i].algId = WOLFCOSE_ALG_HPKE_0_KE; + recipients[i].key = &recipientKey[i]; + } + } + if (ret == 0) { + ret = wc_RNG_GenerateBlock(&rng, iv, (word32)sizeof(iv)); + } + if (ret == 0) { + ret = wc_CoseEncrypt_Encrypt(recipients, HPKE_TEST_RECIPIENTS, + WOLFCOSE_ALG_A128GCM, iv, sizeof(iv), payload, + sizeof(payload) - 1u, NULL, 0u, NULL, 0u, scratch, + sizeof(scratch), cose, sizeof(cose), &coseLen, &rng); + } + if (ret == 0) { + for (i = 0u; i < HPKE_TEST_RECIPIENTS; i++) { + recipientKey[i].hasPrivate = 1u; + } + } + for (i = 0u; (ret == 0) && (i < HPKE_TEST_RECIPIENTS); i++) { + plaintextLen = 0u; + ret = wc_CoseEncrypt_Decrypt(&recipients[i], i, cose, coseLen, + NULL, 0u, NULL, 0u, scratch, sizeof(scratch), &hdr, plaintext, + sizeof(plaintext), &plaintextLen); + if ((ret == 0) && + ((hdr.alg != WOLFCOSE_ALG_A128GCM) || + (plaintextLen != (sizeof(payload) - 1u)) || + (memcmp(plaintext, payload, plaintextLen) != 0))) { + ret = -1; + } + } + while (eccCount > 0u) { + eccCount--; + wc_ecc_free(&recipientEcc[eccCount]); + } + if (rngInit != 0) { + wc_FreeRng(&rng); + } + printf("%s\n", ret == 0 ? "PASS" : "FAIL"); + return ret; +} +#endif /* WOLFCOSE_HPKE_0_KE_ENCRYPT && WOLFCOSE_HPKE_0_KE_DECRYPT */ + /* MAC round-trip: keygen -> mac -> macverify -> check payload */ #if defined(WOLFCOSE_HAVE_HMAC) static int test_mac_roundtrip(const char* name, int32_t alg, size_t keyLen) @@ -1585,6 +2180,25 @@ static int tool_test(const char* filter) } #endif + /* --- Experimental COSE-HPKE --- */ +#if defined(WOLFCOSE_HPKE_0_ENCRYPT) && defined(WOLFCOSE_HPKE_0_DECRYPT) + if (all || strcmp(filter, "HPKE-0") == 0) { + tests++; + if (test_hpke0_roundtrip() != 0) { + failures++; + } + } +#endif +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) && \ + defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + if (all || strcmp(filter, "HPKE-0-KE") == 0) { + tests++; + if (test_hpke_ke_roundtrip() != 0) { + failures++; + } + } +#endif + /* --- COSE_Mac0 --- */ #if defined(WOLFCOSE_HAVE_HMAC) #ifdef WOLFCOSE_HAVE_HMAC256 @@ -1629,8 +2243,18 @@ int main(int argc, char* argv[]) const char* cmd; const char* algStr = NULL; const char* keyPath = NULL; +#if defined(WOLFCOSE_HAVE_HPKE_0) + const char* publicPath = NULL; +#endif const char* inPath = NULL; const char* outPath = NULL; +#if defined(WOLFCOSE_HAVE_HPKE_0) + const char* keyPaths[WOLFCOSE_TOOL_MAX_HPKE_RECIPIENTS]; + size_t keyPathCount = 0u; +#endif +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + size_t recipientIndex = 0u; +#endif int32_t alg = 0; int i; @@ -1652,6 +2276,36 @@ int main(int argc, char* argv[]) } else if (strcmp(argv[i], "-k") == 0) { keyPath = argv[++i]; +#if defined(WOLFCOSE_HAVE_HPKE_0) + if (keyPathCount >= WOLFCOSE_TOOL_MAX_HPKE_RECIPIENTS) { + fprintf(stderr, "Too many -k recipient keys\n"); + return EXIT_USAGE; + } + keyPaths[keyPathCount++] = keyPath; +#endif + } +#if defined(WOLFCOSE_HAVE_HPKE_0) + else if (strcmp(argv[i], "-p") == 0) { + publicPath = argv[++i]; + } +#endif + else if (strcmp(argv[i], "-r") == 0) { +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + char* end = NULL; + unsigned long value; + + errno = 0; + value = strtoul(argv[++i], &end, 10); + if ((errno != 0) || (end == argv[i]) || (*end != '\0') || + (value >= WOLFCOSE_TOOL_MAX_HPKE_RECIPIENTS)) { + fprintf(stderr, "Invalid recipient index: %s\n", argv[i]); + return EXIT_USAGE; + } + recipientIndex = (size_t)value; +#else + fprintf(stderr, "HPKE recipient selection is not built in\n"); + return EXIT_USAGE; +#endif } else if (strcmp(argv[i], "-i") == 0) { inPath = argv[++i]; @@ -1687,6 +2341,16 @@ int main(int argc, char* argv[]) fprintf(stderr, "keygen requires -a <alg> -o <keyfile>\n"); return EXIT_USAGE; } +#if defined(WOLFCOSE_HAVE_HPKE_0) + if ((alg == WOLFCOSE_ALG_HPKE_0) || + (alg == WOLFCOSE_ALG_HPKE_0_KE)) { + return tool_hpke_keygen(alg, outPath, publicPath); + } + if (publicPath != NULL) { + fprintf(stderr, "-p is only valid for HPKE key generation\n"); + return EXIT_USAGE; + } +#endif return tool_keygen(alg, algStr, outPath); } else if (strcmp(cmd, "sign") == 0) { @@ -1705,6 +2369,52 @@ int main(int argc, char* argv[]) } return tool_verify(keyPath, inPath); } +#if defined(WOLFCOSE_HPKE_0_ENCRYPT) + else if (strcmp(cmd, "hpke0-enc") == 0) { + if ((keyPath == NULL) || (keyPathCount != 1u) || (inPath == NULL) || + (outPath == NULL)) { + fprintf(stderr, + "hpke0-enc requires -k <public-key> -i <input> -o <output>\n"); + return EXIT_USAGE; + } + return tool_hpke0_enc(keyPath, inPath, outPath); + } +#endif +#if defined(WOLFCOSE_HPKE_0_DECRYPT) + else if (strcmp(cmd, "hpke0-dec") == 0) { + if ((keyPath == NULL) || (keyPathCount != 1u) || (inPath == NULL) || + (outPath == NULL)) { + fprintf(stderr, + "hpke0-dec requires -k <private-key> -i <input> -o <output>\n"); + return EXIT_USAGE; + } + return tool_hpke0_dec(keyPath, inPath, outPath); + } +#endif +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) + else if (strcmp(cmd, "hpke-ke-enc") == 0) { + if ((keyPathCount == 0u) || (algStr == NULL) || (inPath == NULL) || + (outPath == NULL)) { + fprintf(stderr, + "hpke-ke-enc requires -a <alg> -k <public-key> " + "-i <input> -o <output>\n"); + return EXIT_USAGE; + } + return tool_hpke_ke_enc(keyPaths, keyPathCount, alg, inPath, outPath); + } +#endif +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + else if (strcmp(cmd, "hpke-ke-dec") == 0) { + if ((keyPath == NULL) || (keyPathCount != 1u) || (inPath == NULL) || + (outPath == NULL)) { + fprintf(stderr, + "hpke-ke-dec requires -k <private-key> -i <input> " + "-o <output>\n"); + return EXIT_USAGE; + } + return tool_hpke_ke_dec(keyPath, recipientIndex, inPath, outPath); + } +#endif #if defined(WOLFCOSE_HAVE_HMAC) else if (strcmp(cmd, "mac") == 0) { if (keyPath == NULL || algStr == NULL || inPath == NULL || From 48539a40d4686ea98438c4885314543688532115 Mon Sep 17 00:00:00 2001 From: Aidan Garske <aidan@wolfssl.com> Date: Tue, 1 Sep 2026 17:03:11 -0700 Subject: [PATCH 03/11] Fix HPKE rebase integration --- Makefile | 22 ++++++++++++++-------- src/wolfcose.c | 14 ++++++-------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/Makefile b/Makefile index 2830627..12e711d 100644 --- a/Makefile +++ b/Makefile @@ -130,14 +130,20 @@ src/%.o: src/%.c src/wolfcose_internal.h include/wolfcose/wolfcose.h $(BUILD_CON # under test. It must exercise the local ECDSA Sign1 signing path even when the # caller is testing a no-ECDSA or verify-only configuration. ECDSA_POLICY_OPTS ?= -include wolfssl/options.h -ECDSA_POLICY_BASE_FLAGS = $(CFLAGS) -x c -fsyntax-only -Wno-error \ +WOLFCOSE_POLICY_DISABLE_HPKE_FLAGS = -UWOLFCOSE_ENABLE_HPKE_0_ENCRYPT \ + -UWOLFCOSE_ENABLE_HPKE_0_DECRYPT \ + -UWOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT \ + -UWOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT +ECDSA_POLICY_BASE_FLAGS = $(CFLAGS) $(WOLFCOSE_POLICY_DISABLE_HPKE_FLAGS) \ + -x c -fsyntax-only -Wno-error \ $(ECDSA_POLICY_OPTS) -DHAVE_ECC \ -UWOLFCOSE_ENABLE_DETERMINISTIC_ECDSA \ -UWOLFCOSE_NO_ES256 -UWOLFCOSE_NO_SIGN1 \ -UWOLFCOSE_NO_SIGN1_SIGN -UWOLFCOSE_LEAN_VERIFY \ -UWOLFCOSE_LEAN_VERIFY_MLDSA \ -UWOLFCOSE_LEAN_MLDSA -ECDSA_POLICY_NO_SUPPORT_FLAGS = $(CFLAGS) -x c -fsyntax-only -Wno-error \ +ECDSA_POLICY_NO_SUPPORT_FLAGS = $(CFLAGS) $(WOLFCOSE_POLICY_DISABLE_HPKE_FLAGS) \ + -x c -fsyntax-only -Wno-error \ -DWOLFSSL_NO_OPTIONS_H -DHAVE_ECC \ -UWOLFSSL_ECDSA_DETERMINISTIC_K \ -UWOLFSSL_ECDSA_DETERMINISTIC_K_VARIANT \ @@ -250,18 +256,18 @@ ecdsa-policy-test: echo "PASS: optional ECDSA nonce policy enforced" rsapss-policy-test: - $(CC) $(CFLAGS) -Werror=unused-function -fsyntax-only \ + $(CC) $(CFLAGS) $(WOLFCOSE_POLICY_DISABLE_HPKE_FLAGS) -Werror=unused-function -fsyntax-only \ -DWOLFCOSE_NO_SIGN1 -DWOLFCOSE_NO_SIGN src/wolfcose.c - $(CC) $(CFLAGS) -x c -fsyntax-only -DWOLFSSL_NO_OPTIONS_H \ + $(CC) $(CFLAGS) $(WOLFCOSE_POLICY_DISABLE_HPKE_FLAGS) -x c -fsyntax-only -DWOLFSSL_NO_OPTIONS_H \ -DWC_RSA_PSS -DWOLFCOSE_NO_KEY_ENCODE \ -DWOLFCOSE_ENABLE_RSAPSS src/wolfcose.c - $(CC) $(CFLAGS) -x c -fsyntax-only -DWOLFSSL_NO_OPTIONS_H \ + $(CC) $(CFLAGS) $(WOLFCOSE_POLICY_DISABLE_HPKE_FLAGS) -x c -fsyntax-only -DWOLFSSL_NO_OPTIONS_H \ -DWC_RSA_PSS -DWOLFCOSE_LEAN_VERIFY \ -DWOLFCOSE_ENABLE_RSAPSS src/wolfcose.c @set -e; \ log_file=$$(mktemp "$${TMPDIR:-/tmp}/wolfcose-rsapss.XXXXXX"); \ trap 'rm -f "$$log_file"' 0 1 2 3 15; \ - if $(CC) $(CFLAGS) -x c -fsyntax-only -DWOLFSSL_NO_OPTIONS_H \ + if $(CC) $(CFLAGS) $(WOLFCOSE_POLICY_DISABLE_HPKE_FLAGS) -x c -fsyntax-only -DWOLFSSL_NO_OPTIONS_H \ -UHAVE_ECC -UWOLFSSL_EXPORT_INT \ -DWC_RSA_PSS -DWOLFCOSE_ENABLE_RSAPSS \ -DWOLFSSL_RSA_VERIFY_ONLY -DWOLFCOSE_LEAN_VERIFY \ @@ -271,7 +277,7 @@ rsapss-policy-test: fi; \ grep -q "RSA-PSS key validation requires WOLFSSL_EXPORT_INT" \ "$$log_file" - $(CC) $(CFLAGS) -x c -fsyntax-only -DWOLFSSL_NO_OPTIONS_H \ + $(CC) $(CFLAGS) $(WOLFCOSE_POLICY_DISABLE_HPKE_FLAGS) -x c -fsyntax-only -DWOLFSSL_NO_OPTIONS_H \ -UHAVE_ECC -UWOLFSSL_EXPORT_INT \ -DWC_RSA_PSS -DWOLFCOSE_ENABLE_RSAPSS \ -DWOLFSSL_RSA_VERIFY_ONLY -DWOLFSSL_EXPORT_INT \ @@ -280,7 +286,7 @@ rsapss-policy-test: log_file=$$(mktemp "$${TMPDIR:-/tmp}/wolfcose-rsapss.XXXXXX"); \ trap 'rm -f "$$log_file"' 0 1 2 3 15; \ for backend in WOLF_CRYPTO_CB WOLFSSL_MICROCHIP_TA100; do \ - if $(CC) $(CFLAGS) -x c -fsyntax-only -DWOLFSSL_NO_OPTIONS_H \ + if $(CC) $(CFLAGS) $(WOLFCOSE_POLICY_DISABLE_HPKE_FLAGS) -x c -fsyntax-only -DWOLFSSL_NO_OPTIONS_H \ -UHAVE_ECC -UWOLFSSL_EXPORT_INT \ -DWC_RSA_PSS -DWOLFCOSE_ENABLE_RSAPSS \ -DWOLFSSL_RSA_VERIFY_ONLY -D$$backend \ diff --git a/src/wolfcose.c b/src/wolfcose.c index c084ba1..232dff9 100644 --- a/src/wolfcose.c +++ b/src/wolfcose.c @@ -10562,7 +10562,8 @@ int wc_CoseEncrypt_Decrypt(const WOLFCOSE_RECIPIENT* recipient, ret = WOLFCOSE_E_COSE_BAD_HDR; } #endif - if (ret == WOLFCOSE_SUCCESS) { + if ((ret == WOLFCOSE_SUCCESS) && + (recipientAlgId != WOLFCOSE_ALG_UNSET)) { ret = wolfCose_ValidateRecipientKeyAlg(recipient->key, recipientAlgId, alg); } @@ -10598,15 +10599,12 @@ int wc_CoseEncrypt_Decrypt(const WOLFCOSE_RECIPIENT* recipient, } } - /* Enforce the caller's recipient->algId policy when set. A message in - * implicit direct mode has no recipient alg, so normalize it to direct. */ + /* Enforce the caller's recipient->algId policy when the protected header + * supplies an algorithm. */ if ((ret == WOLFCOSE_SUCCESS) && + (recipientAlgId != WOLFCOSE_ALG_UNSET) && (recipient->algId != WOLFCOSE_ALG_UNSET)) { - int32_t gotAlg = recipientAlgId; - if (gotAlg == WOLFCOSE_ALG_UNSET) { - gotAlg = WOLFCOSE_ALG_DIRECT; - } - if (recipient->algId != gotAlg) { + if (recipient->algId != recipientAlgId) { ret = WOLFCOSE_E_COSE_BAD_ALG; } } From be9e53e83e51bbda6a4ee3edfd39e64ae5955918 Mon Sep 17 00:00:00 2001 From: Aidan Garske <aidan@wolfssl.com> Date: Wed, 2 Sep 2026 02:41:31 -0700 Subject: [PATCH 04/11] Harden experimental COSE HPKE handling --- .github/workflows/cose-hpke.yml | 6 +- Makefile | 12 +- docs/Getting-Started.md | 9 +- docs/Macros.md | 12 + docs/Testing.md | 30 +- include/wolfcose/wolfcose.h | 43 +- scripts/cmdline-test.sh | 102 +++ src/wolfcose.c | 221 +++++-- tests/test_cose.c | 1046 ++++++++++++++++++++++++++++++- tools/wolfcose_tool.c | 393 +++++++++++- 10 files changed, 1767 insertions(+), 107 deletions(-) diff --git a/.github/workflows/cose-hpke.yml b/.github/workflows/cose-hpke.yml index 4825a35..5aea188 100644 --- a/.github/workflows/cose-hpke.yml +++ b/.github/workflows/cose-hpke.yml @@ -4,7 +4,7 @@ on: push: branches: [ 'main', 'release/**' ] pull_request: - branches: [ '*' ] + branches: [ '**' ] workflow_dispatch: concurrency: @@ -31,6 +31,7 @@ jobs: ./autogen.sh ./configure --enable-ecc --enable-ed25519 --enable-ed448 \ --enable-curve25519 --enable-curve448 \ + --enable-ecccustcurves=all \ --enable-aesgcm --enable-aesccm \ --enable-sha384 --enable-sha512 \ --enable-keygen --enable-hkdf --enable-aeskeywrap \ @@ -50,7 +51,8 @@ jobs: -DWOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT" WOLFCOSE_CFLAGS="-std=c99 -DHAVE_ANONYMOUS_INLINE_AGGREGATES=1 \ -Os -Wall -Wextra -Wpedantic -Wshadow -Wconversion -Wvla \ - -Werror=vla -I./include -I$WOLFSSL_DIR/include $HPKE_FLAGS" + -Werror=vla -DHAVE_ECC_KOBLITZ -I./include \ + -I$WOLFSSL_DIR/include $HPKE_FLAGS" make clean make test CFLAGS="$WOLFCOSE_CFLAGS" \ LDFLAGS="-L$WOLFSSL_DIR/lib -lwolfssl" diff --git a/Makefile b/Makefile index 12e711d..7238da7 100644 --- a/Makefile +++ b/Makefile @@ -130,7 +130,9 @@ src/%.o: src/%.c src/wolfcose_internal.h include/wolfcose/wolfcose.h $(BUILD_CON # under test. It must exercise the local ECDSA Sign1 signing path even when the # caller is testing a no-ECDSA or verify-only configuration. ECDSA_POLICY_OPTS ?= -include wolfssl/options.h -WOLFCOSE_POLICY_DISABLE_HPKE_FLAGS = -UWOLFCOSE_ENABLE_HPKE_0_ENCRYPT \ +WOLFCOSE_POLICY_DISABLE_HPKE_FLAGS = -UWOLFCOSE_ENABLE_HPKE_0 \ + -UWOLFCOSE_ENABLE_HPKE_0_KE \ + -UWOLFCOSE_ENABLE_HPKE_0_ENCRYPT \ -UWOLFCOSE_ENABLE_HPKE_0_DECRYPT \ -UWOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT \ -UWOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT @@ -525,9 +527,11 @@ HPKE_C99_CONFIGS = \ "$(HPKE_C99_CONFIG)" \ "$(HPKE_C99_CONFIG) -DNO_ECC256 -DHAVE_ALL_CURVES -DHAVE_ECC_KOBLITZ" # Prefer the explicitly selected HPKE backend over an unrelated host install. -# One-way builds leave round-trip-only test helpers unused; this gate judges -# gated wolfCOSE syntax while treating backend headers as system headers. -HPKE_C99_FLAGS = $(C99_FLAGS) -Wno-unused-function +# Treat backend headers as system headers so this gate judges gated wolfCOSE +# syntax, including when the selected wolfSSL version uses C11 extensions. +# Re-add the common local prefix late because macOS Clang otherwise searches it +# before a caller-selected -isystem directory. +HPKE_C99_FLAGS = $(C99_FLAGS) -Wno-unused-function -isystem /usr/local/include c99-check: @for cfg in $(C99_CONFIGS); do \ diff --git a/docs/Getting-Started.md b/docs/Getting-Started.md index add0376..6f22b3b 100644 --- a/docs/Getting-Started.md +++ b/docs/Getting-Started.md @@ -265,13 +265,18 @@ make cd ../wolfCOSE make hpke-demo \ - CFLAGS="-std=c99 -I./include -I/path/to/wolfssl" \ + EXTRA_CFLAGS="-I/path/to/wolfssl" \ LDFLAGS="-L/path/to/wolfssl -lwolfssl" ``` The command-line tool is compiled with the same operation gates. `keygen -p` exports a public-only COSE_Key; keep the corresponding `-o` private key on the -recipient. The direct commands are for HPKE-0, while the `hpke-ke-*` commands +recipient, and use new, distinct non-symlink destinations for `-o` and `-p`. +On POSIX builds, HPKE key generation refuses to replace an existing +destination. Normalized, case-equivalent, and symlink aliases are rejected +before either key is written. On non-POSIX builds, `-p` is rejected rather +than weakening those key-output safeguards. The direct commands are +for HPKE-0, while the `hpke-ke-*` commands use one independently HPKE-protected CEK for every recipient: ```bash diff --git a/docs/Macros.md b/docs/Macros.md index aa698f7..b42943b 100644 --- a/docs/Macros.md +++ b/docs/Macros.md @@ -286,6 +286,18 @@ once, and HPKE-wraps that CEK separately for every recipient. HPKE base mode does not authenticate the sender, so add a COSE signature or MAC when sender authentication is required. +Draft P0 permits the HPKE `alg` header parameter to be absent. The integrated +decrypt API is already pinned to HPKE-0; for key-encryption decrypt, set the +selected `WOLFCOSE_RECIPIENT.algId` to `WOLFCOSE_ALG_HPKE_0_KE` so wolfCOSE can +pin the omitted value safely. When an integrated or recipient HPKE `alg` is +present, it must be in the protected header. wolfCOSE rejects an unprotected +HPKE `alg` rather than accepting an unauthenticated key-management choice. For +HPKE-0-KE, the outer `COSE_Encrypt` content algorithm may be unprotected: the +HPKE Recipient_structure binds it as `next_layer_alg`, so a modification makes +HPKE CEK recovery fail. P0 requires the 65-byte P-256 `ek` in the unprotected +header, rejects it in the protected header, and rejects `psk_id` because PSK +mode is not implemented. + ```bash # Single-recipient integrated HPKE: send and receive. make EXTRA_CFLAGS="-DWOLFCOSE_ENABLE_HPKE_0" diff --git a/docs/Testing.md b/docs/Testing.md index d7bc686..7818e09 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -42,14 +42,28 @@ make c99-hpke-check WOLFSSL_INC=/path/to/hpke-enabled-wolfssl/include EXPECT_HPKE=true make cmdline-test EXTRA_CFLAGS="$HPKE_FLAGS" ``` -The command-line test performs public/private key export, HPKE-0 integrated -encryption, two-recipient HPKE-0-KE encryption and decryption at both recipient -indices, maximum-message round trips for both constructions, and a focused -`test -a` round trip for each construction. The dedicated strict C99 target -compiles every HPKE-gated library, test, tool, and example path under each -one-way operation gate, the complete P0 configuration, and wolfSSL's -`NO_ECC256` plus `HAVE_ALL_CURVES` configuration. GitHub Actions runs the same -coverage in +The command-line test performs public/private key export, rejects identical, +normalized-alias, case-alias (where the filesystem supports it), and +symlink-alias key destinations, preserves existing POSIX destinations, and +rejects a failed paired key export without leaving a private-key file. It +rejects a plaintext one byte above the configured maximum for both +constructions, exercises HPKE-0 integrated +encryption, two-recipient HPKE-0-KE encryption and decryption at both +recipient indices, maximum-message round trips for both constructions, and a +focused `test -a` round trip for each construction. The dedicated strict C99 +target compiles every HPKE-gated library, test, tool, and example path under +each one-way operation gate, both convenience gates, the complete P0 +configuration, and wolfSSL's `NO_ECC256` plus `HAVE_ALL_CURVES` configuration. +The unit suite validates draft messages that omit the optional HPKE `alg` +(including a second selected recipient), rejects an HPKE `alg` in an +unprotected header, accepts an unprotected HPKE-0-KE content algorithm only +when its authenticated Recipient_structure binds it, and rejects a modified +content algorithm. The CI backend enables Koblitz curves to prove that a +32-byte secp256k1 key cannot masquerade as P-256 at the HPKE API boundary. It +also covers missing, duplicate, wrong-type, wrong-length, and wrongly placed +`ek`, prohibited `psk_id`, detached ciphertext, and cleared outputs on failed +decrypts and encrypts. +GitHub Actions runs the same coverage in [Experimental COSE-HPKE](../.github/workflows/cose-hpke.yml). ### Comprehensive Algorithm Tests diff --git a/include/wolfcose/wolfcose.h b/include/wolfcose/wolfcose.h index 9565dc0..b97cde4 100644 --- a/include/wolfcose/wolfcose.h +++ b/include/wolfcose/wolfcose.h @@ -328,8 +328,6 @@ typedef struct WOLFCOSE_HDR { size_t ivLen; /**< IV length */ const uint8_t* partialIv; /**< Partial IV pointer */ size_t partialIvLen; /**< Partial IV length */ - const uint8_t* hpkeEk; /**< HPKE encapsulated key pointer */ - size_t hpkeEkLen; /**< HPKE encapsulated key length */ int32_t contentType; /**< Content type from either header bucket */ uint8_t flags; /**< Header flags (see WOLFCOSE_HDR_FLAG_*) */ } WOLFCOSE_HDR; @@ -338,10 +336,6 @@ typedef struct WOLFCOSE_HDR { #define WOLFCOSE_HDR_FLAG_DETACHED 0x01u /** \brief Flag indicating an unprotected content-type label was present */ #define WOLFCOSE_HDR_FLAG_CONTENT_TYPE_UNPROTECTED 0x02u -#if defined(WOLFCOSE_HAVE_HPKE_0) -/** \brief Flag indicating an HPKE encapsulated key is present. */ -#define WOLFCOSE_HDR_FLAG_HPKE_EK 0x04u -#endif /** * \brief Caller-supplied signature callback (RFC 9052 Section 4.4). @@ -1252,7 +1246,7 @@ WOLFCOSE_API int wc_CoseEncrypt0_Decrypt(const WOLFCOSE_KEY* key, * \param scratchSz Working buffer size. * \param out Output COSE_Encrypt0 buffer. * \param outSz Output buffer size. - * \param outLen Output: bytes written. + * \param outLen Output: bytes written; set to zero on failure. * \param rng Initialized random number generator. * \return WOLFCOSE_SUCCESS or a negative error code. */ @@ -1274,6 +1268,26 @@ WOLFCOSE_API int wc_CoseHpkeEncrypt0_Encrypt( * The recipient key must be an EC2 P-256 private key and may be pinned to * WOLFCOSE_ALG_HPKE_0. Only HPKE base mode with empty HPKE info is accepted; * PSK mode is rejected. + * + * \param recipientKey Recipient EC2 P-256 private key. + * \param in Input COSE_Encrypt0 message. + * \param inSz Input message size. + * \param detachedCt Detached ciphertext (NULL if attached). Required + * when the message's ciphertext item is CBOR null. + * \param detachedCtLen Detached ciphertext length. + * \param extAad External additional authenticated data (NULL if none). + * \param extAadLen External additional authenticated data length. + * \param scratch Working buffer for Enc_structure reconstruction. + * \param scratchSz Working buffer size. + * \param hdr Output: parsed COSE headers; only protected fields + * are authenticated. + * \param plaintext Output buffer for decrypted plaintext. + * \param plaintextSz Plaintext buffer size. + * \param plaintextLen Output: plaintext length. + * \return WOLFCOSE_SUCCESS or a negative error code, including + * WOLFCOSE_E_DETACHED_PAYLOAD when ciphertext is detached but + * detachedCt is NULL. On failure, hdr and plaintext are cleared and + * plaintextLen is set to zero when it is non-NULL. */ WOLFCOSE_API int wc_CoseHpkeEncrypt0_Decrypt( const WOLFCOSE_KEY* recipientKey, @@ -1481,13 +1495,14 @@ WOLFCOSE_API int wc_CoseEncrypt_Encrypt(const WOLFCOSE_RECIPIENT* recipients, /** * \brief Decrypt a COSE_Encrypt message. * - * Decrypts using the key from the specified recipient entry. - * Every on-wire recipient must declare its key-management algorithm, all - * top-level sibling recipients must use an RFC-compatible key-distribution - * mode, and a direct recipient's ciphertext item must be an empty bstr or - * null. Selecting a recipient that contains nested recipients is unsupported. - * An unprotected body algorithm is accepted only when recipient->key->alg - * pins the same value. + * Decrypts using the key from the specified recipient entry. Every non-HPKE + * on-wire recipient must declare its key-management algorithm, all top-level + * sibling recipients must use an RFC-compatible key-distribution mode, and a + * direct recipient's ciphertext item must be an empty bstr or null. Selecting + * a recipient that contains nested recipients is unsupported. An HPKE-0-KE + * recipient may omit its algorithm when recipient->algId pins HPKE-0-KE. An + * unprotected body algorithm is accepted when a direct key pins it or an + * HPKE-0-KE Recipient_structure cryptographically binds it. * * \param recipient WOLFCOSE_RECIPIENT with decryption key. * \param recipientIndex 0-based index of recipient to use. diff --git a/scripts/cmdline-test.sh b/scripts/cmdline-test.sh index 6627be3..3fdd221 100755 --- a/scripts/cmdline-test.sh +++ b/scripts/cmdline-test.sh @@ -144,6 +144,81 @@ echo "== Experimental COSE-HPKE: keygen -> encrypt -> decrypt -> self-test ==" H0_PRIV="$WORK/hpke0.private"; H0_PUB="$WORK/hpke0.public" H0_COSE="$WORK/hpke0.cose"; H0_OUT="$WORK/hpke0.out" if hpke_keygen_or "HPKE-0" "$H0_PRIV" "$H0_PUB"; then + H0_SAME="$WORK/hpke0.same" + if "$TOOL" keygen -a "HPKE-0" -o "$H0_SAME" -p "$H0_SAME" \ + >/dev/null 2>&1; then + bad "HPKE-0 keygen rejects identical private/public paths" + else + ok "HPKE-0 keygen rejects identical private/public paths" + fi + H0_ALIAS="$WORK/hpke0.alias" + if "$TOOL" keygen -a "HPKE-0" -o "$H0_ALIAS" -p "$WORK/./hpke0.alias" \ + >/dev/null 2>&1; then + bad "HPKE-0 keygen rejects normalized private/public paths" + else + ok "HPKE-0 keygen rejects normalized private/public paths" + fi + H0_CASE_LOWER="$WORK/hpke0.case" + H0_CASE_UPPER="$WORK/HPKE0.CASE" + if touch "$H0_CASE_LOWER"; then + if [ -e "$H0_CASE_UPPER" ]; then + rm -f "$H0_CASE_LOWER" + if "$TOOL" keygen -a "HPKE-0" -o "$H0_CASE_LOWER" \ + -p "$H0_CASE_UPPER" >/dev/null 2>&1 || \ + [ -e "$H0_CASE_LOWER" ]; then + bad "HPKE-0 keygen rejects case-alias private/public paths" + else + ok "HPKE-0 keygen rejects case-alias private/public paths" + fi + else + rm -f "$H0_CASE_LOWER" + skip "HPKE-0 keygen case-alias paths" "case-sensitive filesystem" + fi + else + bad "HPKE-0 keygen case-alias test setup" + fi + H0_SYMLINK_TARGET="$WORK/hpke0.symlink-target" + H0_SYMLINK="$WORK/hpke0.symlink" + if ln -s "$H0_SYMLINK_TARGET" "$H0_SYMLINK" && \ + "$TOOL" keygen -a "HPKE-0" -o "$H0_SYMLINK_TARGET" -p "$H0_SYMLINK" \ + >/dev/null 2>&1; then + bad "HPKE-0 keygen rejects symlink private/public paths" + elif [ -e "$H0_SYMLINK_TARGET" ]; then + bad "HPKE-0 keygen leaves symlink target untouched" + else + ok "HPKE-0 keygen rejects symlink private/public paths" + fi + H0_EXISTING_PRIV="$WORK/hpke0-existing.private" + H0_EXISTING_COPY="$WORK/hpke0-existing.copy" + H0_NEW_PUB="$WORK/hpke0-new.public" + if printf 'existing HPKE private key\n' > "$H0_EXISTING_PRIV" && \ + cp "$H0_EXISTING_PRIV" "$H0_EXISTING_COPY"; then + if "$TOOL" keygen -a "HPKE-0" -o "$H0_EXISTING_PRIV" \ + -p "$H0_NEW_PUB" >/dev/null 2>&1; then + bad "HPKE-0 keygen rejects existing output paths" + elif cmp -s "$H0_EXISTING_PRIV" "$H0_EXISTING_COPY" && \ + [ ! -e "$H0_NEW_PUB" ]; then + ok "HPKE-0 keygen preserves existing output paths" + else + bad "HPKE-0 keygen preserves existing output paths" + fi + else + bad "HPKE-0 existing output test setup" + fi + H0_FAILED_PRIV="$WORK/hpke0-failed.private" + H0_BLOCKED_PUB="$WORK/hpke0-public-directory" + if mkdir "$H0_BLOCKED_PUB"; then + if "$TOOL" keygen -a "HPKE-0" -o "$H0_FAILED_PRIV" \ + -p "$H0_BLOCKED_PUB" >/dev/null 2>&1; then + bad "HPKE-0 keygen rejects unusable public output" + elif [ -e "$H0_FAILED_PRIV" ]; then + bad "HPKE-0 keygen leaves no partial private output" + else + ok "HPKE-0 keygen leaves no partial private output" + fi + else + bad "HPKE-0 unusable public output test setup" + fi if "$TOOL" hpke0-enc -k "$H0_PUB" -i "$IN" -o "$H0_COSE" >/dev/null 2>&1 && \ "$TOOL" hpke0-dec -k "$H0_PRIV" -i "$H0_COSE" -o "$H0_OUT" >/dev/null 2>&1 && \ cmp -s "$IN" "$H0_OUT"; then @@ -170,6 +245,19 @@ if hpke_keygen_or "HPKE-0" "$H0_PRIV" "$H0_PUB"; then else bad "HPKE-0 maximum message enc/dec" fi + H0_OVERSIZE_IN="$WORK/hpke0-oversize.bin" + H0_OVERSIZE_COSE="$WORK/hpke0-oversize.cose" + if dd if=/dev/zero of="$H0_OVERSIZE_IN" bs="$HPKE_TOOL_MAX_MSG" count=1 \ + >/dev/null 2>&1 && printf '\0' >> "$H0_OVERSIZE_IN"; then + if "$TOOL" hpke0-enc -k "$H0_PUB" -i "$H0_OVERSIZE_IN" \ + -o "$H0_OVERSIZE_COSE" >/dev/null 2>&1; then + bad "HPKE-0 rejects oversized plaintext" + else + ok "HPKE-0 rejects oversized plaintext" + fi + else + bad "HPKE-0 oversized plaintext test setup" + fi else skip "HPKE-0 command and self-test" fi @@ -209,6 +297,20 @@ if hpke_keygen_or "HPKE-0-KE" "$HKE0_PRIV" "$HKE0_PUB" && \ else bad "HPKE-0-KE maximum message enc/dec" fi + HKE_OVERSIZE_IN="$WORK/hpke-ke-oversize.bin" + HKE_OVERSIZE_COSE="$WORK/hpke-ke-oversize.cose" + if dd if=/dev/zero of="$HKE_OVERSIZE_IN" bs="$HPKE_TOOL_MAX_MSG" count=1 \ + >/dev/null 2>&1 && printf '\0' >> "$HKE_OVERSIZE_IN"; then + if "$TOOL" hpke-ke-enc -a A128GCM -k "$HKE0_PUB" -k "$HKE1_PUB" \ + -i "$HKE_OVERSIZE_IN" -o "$HKE_OVERSIZE_COSE" \ + >/dev/null 2>&1; then + bad "HPKE-0-KE rejects oversized plaintext" + else + ok "HPKE-0-KE rejects oversized plaintext" + fi + else + bad "HPKE-0-KE oversized plaintext test setup" + fi else skip "HPKE-0-KE command and self-test" fi diff --git a/src/wolfcose.c b/src/wolfcose.c index 232dff9..386d51e 100644 --- a/src/wolfcose.c +++ b/src/wolfcose.c @@ -1344,19 +1344,6 @@ int wolfCose_DecodeProtectedHdr(const uint8_t* data, size_t dataLen, hdr->partialIvLen = pivBstrLen; } } -#if defined(WOLFCOSE_HAVE_HPKE_0) - else if ((ret == WOLFCOSE_SUCCESS) && - (label == WOLFCOSE_HDR_HPKE_EK)) { - const uint8_t* ekData; - size_t ekLen; - ret = wc_CBOR_DecodeBstr(&ctx, &ekData, &ekLen); - if (ret == WOLFCOSE_SUCCESS) { - hdr->hpkeEk = ekData; - hdr->hpkeEkLen = ekLen; - hdr->flags |= WOLFCOSE_HDR_FLAG_HPKE_EK; - } - } -#endif else { if (ret == WOLFCOSE_SUCCESS) { /* Skip unknown header */ @@ -1384,8 +1371,18 @@ int wolfCose_DecodeProtectedHdr(const uint8_t* data, size_t dataLen, return ret; } -int wolfCose_DecodeUnprotectedHdr(WOLFCOSE_CBOR_CTX* ctx, WOLFCOSE_HDR* hdr, - WOLFCOSE_HDR_STATE* hdrState) +/* HPKE recipient state is deliberately private. WOLFCOSE_HDR is a public + * structure, so adding decoded HPKE fields to it would silently break callers + * that allocate the previously published layout. */ +typedef struct WOLFCOSE_HPKE_HDR { + const uint8_t* ek; + size_t ekLen; + int hasEk; +} WOLFCOSE_HPKE_HDR; + +static int wolfCose_DecodeUnprotectedHdrEx(WOLFCOSE_CBOR_CTX* ctx, + WOLFCOSE_HDR* hdr, WOLFCOSE_HDR_STATE* hdrState, + WOLFCOSE_HPKE_HDR* hpkeHdr) { int ret; size_t mapCount = 0; @@ -1394,6 +1391,10 @@ int wolfCose_DecodeUnprotectedHdr(WOLFCOSE_CBOR_CTX* ctx, WOLFCOSE_HDR* hdr, size_t bstrLen; int skipped; +#if !defined(WOLFCOSE_HAVE_HPKE_0) + (void)hpkeHdr; +#endif + if ((ctx == NULL) || (hdr == NULL) || (hdrState == NULL)) { ret = WOLFCOSE_E_INVALID_ARG; } @@ -1456,11 +1457,16 @@ int wolfCose_DecodeUnprotectedHdr(WOLFCOSE_CBOR_CTX* ctx, WOLFCOSE_HDR* hdr, #if defined(WOLFCOSE_HAVE_HPKE_0) else if ((ret == WOLFCOSE_SUCCESS) && (label == WOLFCOSE_HDR_HPKE_EK)) { - ret = wc_CBOR_DecodeBstr(ctx, &bstrData, &bstrLen); - if (ret == WOLFCOSE_SUCCESS) { - hdr->hpkeEk = bstrData; - hdr->hpkeEkLen = bstrLen; - hdr->flags |= WOLFCOSE_HDR_FLAG_HPKE_EK; + if (hpkeHdr == NULL) { + ret = wc_CBOR_Skip(ctx); + } + else { + ret = wc_CBOR_DecodeBstr(ctx, &bstrData, &bstrLen); + if (ret == WOLFCOSE_SUCCESS) { + hpkeHdr->ek = bstrData; + hpkeHdr->ekLen = bstrLen; + hpkeHdr->hasEk = 1; + } } } #endif @@ -1521,12 +1527,20 @@ int wolfCose_DecodeUnprotectedHdr(WOLFCOSE_CBOR_CTX* ctx, WOLFCOSE_HDR* hdr, return ret; } +int wolfCose_DecodeUnprotectedHdr(WOLFCOSE_CBOR_CTX* ctx, WOLFCOSE_HDR* hdr, + WOLFCOSE_HDR_STATE* hdrState) +{ + return wolfCose_DecodeUnprotectedHdrEx(ctx, hdr, hdrState, NULL); +} + #if defined(WOLFCOSE_SIGN_VERIFY) || defined(WOLFCOSE_ENCRYPT_DECRYPT) || \ defined(WOLFCOSE_MAC_VERIFY) -/* Decode only the algorithm from an unselected header map. Other labels and - * values are intentionally left to the application that selected the entry. */ +/* Decode the algorithm from an unselected header map. When HPKE key + * encryption is compiled in, callers can also identify its unprotected `ek` + * label so that an omitted optional recipient algorithm remains classifiable. + * Other labels and values are intentionally left to the selected entry. */ static int wolfCose_DecodeSkippedHdrAlg(WOLFCOSE_CBOR_CTX* ctx, - int32_t* alg, int* algFound) + int32_t* alg, int* algFound, int* hpkeEkFound) { int ret; size_t mapCount = 0u; @@ -1536,6 +1550,9 @@ static int wolfCose_DecodeSkippedHdrAlg(WOLFCOSE_CBOR_CTX* ctx, ret = WOLFCOSE_E_INVALID_ARG; } else { + if (hpkeEkFound != NULL) { + *hpkeEkFound = 0; + } ret = wc_CBOR_DecodeMapStart(ctx, &mapCount); } if ((ret == WOLFCOSE_SUCCESS) && (mapCount > ctx->bufSz)) { @@ -1571,6 +1588,20 @@ static int wolfCose_DecodeSkippedHdrAlg(WOLFCOSE_CBOR_CTX* ctx, } } } +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + else if ((ret == WOLFCOSE_SUCCESS) && + (wc_CBOR_LabelIsInt(&label, WOLFCOSE_HDR_HPKE_EK) != 0)) { + if ((hpkeEkFound != NULL) && (*hpkeEkFound != 0)) { + ret = WOLFCOSE_E_CBOR_MALFORMED; + } + else { + if (hpkeEkFound != NULL) { + *hpkeEkFound = 1; + } + ret = wc_CBOR_Skip(ctx); + } + } +#endif else if (ret == WOLFCOSE_SUCCESS) { ret = wc_CBOR_Skip(ctx); } @@ -1584,7 +1615,8 @@ static int wolfCose_DecodeSkippedHdrAlg(WOLFCOSE_CBOR_CTX* ctx, /* Decode the three fields shared by COSE_Signature and COSE_recipient. */ static int wolfCose_DecodeSkippedHeaderEntry(WOLFCOSE_CBOR_CTX* ctx, - size_t maxArrayCount, size_t* arrayCount, int32_t* alg) + size_t maxArrayCount, size_t* arrayCount, int32_t* alg, + int* algFoundOut, int* hpkeEkFound) { int ret; const uint8_t* protectedData = NULL; @@ -1599,6 +1631,12 @@ static int wolfCose_DecodeSkippedHeaderEntry(WOLFCOSE_CBOR_CTX* ctx, if (alg != NULL) { *alg = WOLFCOSE_ALG_UNSET; } + if (algFoundOut != NULL) { + *algFoundOut = 0; + } + if (hpkeEkFound != NULL) { + *hpkeEkFound = 0; + } ret = wc_CBOR_DecodeArrayStart(ctx, arrayCount); } if ((ret == WOLFCOSE_SUCCESS) && @@ -1622,18 +1660,22 @@ static int wolfCose_DecodeSkippedHeaderEntry(WOLFCOSE_CBOR_CTX* ctx, (void)XMEMSET(&protectedCtx, 0, sizeof(protectedCtx)); protectedCtx.cbuf = protectedData; protectedCtx.bufSz = protectedLen; - ret = wolfCose_DecodeSkippedHdrAlg(&protectedCtx, alg, &algFound); + ret = wolfCose_DecodeSkippedHdrAlg(&protectedCtx, alg, &algFound, + NULL); if ((ret == WOLFCOSE_SUCCESS) && (protectedCtx.idx != protectedCtx.bufSz)) { ret = WOLFCOSE_E_CBOR_MALFORMED; } } if ((ret == WOLFCOSE_SUCCESS) && (alg != NULL)) { - ret = wolfCose_DecodeSkippedHdrAlg(ctx, alg, &algFound); + ret = wolfCose_DecodeSkippedHdrAlg(ctx, alg, &algFound, hpkeEkFound); } if ((ret == WOLFCOSE_SUCCESS) && (alg != NULL)) { ret = wc_CBOR_Skip(ctx); } + if ((ret == WOLFCOSE_SUCCESS) && (algFoundOut != NULL)) { + *algFoundOut = algFound; + } return ret; } @@ -1644,7 +1686,8 @@ static int wolfCose_DecodeSkippedSignature(WOLFCOSE_CBOR_CTX* ctx) { size_t arrayCount = 0u; - return wolfCose_DecodeSkippedHeaderEntry(ctx, 3u, &arrayCount, NULL); + return wolfCose_DecodeSkippedHeaderEntry(ctx, 3u, &arrayCount, NULL, + NULL, NULL); } #endif @@ -1671,10 +1714,25 @@ static int wolfCose_DecodeSkippedRecipient(WOLFCOSE_CBOR_CTX* ctx, while ((ret == WOLFCOSE_SUCCESS) && (remaining > 0u)) { size_t arrayCount = 0u; int32_t decodedAlg = WOLFCOSE_ALG_UNSET; +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + int algFound = 0; + int hpkeEkFound = 0; +#endif ret = wolfCose_DecodeSkippedHeaderEntry(ctx, 4u, &arrayCount, - &decodedAlg); + &decodedAlg, +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + &algFound, &hpkeEkFound); +#else + NULL, NULL); +#endif remaining--; +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + if ((ret == WOLFCOSE_SUCCESS) && + (algFound == 0) && (hpkeEkFound != 0)) { + decodedAlg = WOLFCOSE_ALG_HPKE_0_KE; + } +#endif if ((ret == WOLFCOSE_SUCCESS) && (firstRecipient != 0)) { *recipientAlg = decodedAlg; firstRecipient = 0; @@ -5290,6 +5348,10 @@ static int wolfCose_Hpke0ValidateKey(const WOLFCOSE_KEY* key, (wc_ecc_size(key->key.ecc) != 32)) { ret = WOLFCOSE_E_COSE_KEY_TYPE; } + else if (wolfCose_EccKeyCheckCurve(WOLFCOSE_CRV_P256, + key->key.ecc) != WOLFCOSE_SUCCESS) { + ret = WOLFCOSE_E_COSE_KEY_TYPE; + } else if ((key->alg != WOLFCOSE_ALG_UNSET) && (key->alg != alg)) { ret = WOLFCOSE_E_COSE_BAD_ALG; } @@ -8681,6 +8743,9 @@ int wc_CoseHpkeEncrypt0_Encrypt(const WOLFCOSE_KEY* recipientKey, if (out != NULL) { (void)wolfCose_ForceZero(out, outSz); } + if (outLen != NULL) { + *outLen = 0u; + } if ((isDetached != 0) && (detachedPayload != NULL)) { (void)wolfCose_ForceZero(detachedPayload, detachedSz); } @@ -8714,6 +8779,8 @@ int wc_CoseHpkeEncrypt0_Decrypt(const WOLFCOSE_KEY* recipientKey, size_t encStructLen = 0u; size_t payloadLen = 0u; WOLFCOSE_HDR_STATE hdrState; + WOLFCOSE_HPKE_HDR hpkeHdr; + int algProtected = 0; if ((recipientKey == NULL) || (in == NULL) || (inSz == 0u) || (scratch == NULL) || (hdr == NULL) || (plaintext == NULL) || @@ -8731,6 +8798,7 @@ int wc_CoseHpkeEncrypt0_Decrypt(const WOLFCOSE_KEY* recipientKey, #endif else { (void)XMEMSET(hdr, 0, sizeof(*hdr)); + (void)XMEMSET(&hpkeHdr, 0, sizeof(hpkeHdr)); ctx.cbuf = in; ctx.bufSz = inSz; ctx.idx = 0u; @@ -8756,9 +8824,12 @@ int wc_CoseHpkeEncrypt0_Decrypt(const WOLFCOSE_KEY* recipientKey, ret = wolfCose_DecodeProtectedHdr(protectedData, protectedLen, hdr, &hdrState); } + if (ret == WOLFCOSE_SUCCESS) { + algProtected = wolfCose_HdrStateContains(&hdrState, + WOLFCOSE_HDR_ALG); + } if ((ret == WOLFCOSE_SUCCESS) && - ((wolfCose_HdrStateContains(&hdrState, WOLFCOSE_HDR_ALG) == 0) || - (hdr->alg != WOLFCOSE_ALG_HPKE_0) || + (((algProtected != 0) && (hdr->alg != WOLFCOSE_ALG_HPKE_0)) || (wolfCose_HdrStateContains(&hdrState, WOLFCOSE_HDR_HPKE_EK) != 0) || (wolfCose_HdrStateContains(&hdrState, @@ -8766,12 +8837,15 @@ int wc_CoseHpkeEncrypt0_Decrypt(const WOLFCOSE_KEY* recipientKey, ret = WOLFCOSE_E_COSE_BAD_HDR; } if (ret == WOLFCOSE_SUCCESS) { - ret = wolfCose_DecodeUnprotectedHdr(&ctx, hdr, &hdrState); + ret = wolfCose_DecodeUnprotectedHdrEx(&ctx, hdr, &hdrState, + &hpkeHdr); } if ((ret == WOLFCOSE_SUCCESS) && - ((hdr->iv != NULL) || (hdr->partialIv != NULL) || - ((hdr->flags & WOLFCOSE_HDR_FLAG_HPKE_EK) == 0u) || - (hdr->hpkeEkLen != WOLFCOSE_HPKE_0_ENC_SZ) || + (((algProtected == 0) && + (wolfCose_HdrStateContains(&hdrState, WOLFCOSE_HDR_ALG) != 0)) || + (hdr->iv != NULL) || (hdr->partialIv != NULL) || + (hpkeHdr.hasEk == 0) || + (hpkeHdr.ekLen != WOLFCOSE_HPKE_0_ENC_SZ) || (wolfCose_HdrStateContains(&hdrState, WOLFCOSE_HPKE_0_PSK_ID_LABEL) != 0))) { ret = WOLFCOSE_E_COSE_BAD_HDR; @@ -8820,7 +8894,7 @@ int wc_CoseHpkeEncrypt0_Decrypt(const WOLFCOSE_KEY* recipientKey, if (ret == WOLFCOSE_SUCCESS) { ret = wolfCose_Hpke0Open(recipientKey, WOLFCOSE_ALG_HPKE_0, NULL, 0u, scratch, encStructLen, - hdr->hpkeEk, hdr->hpkeEkLen, + hpkeHdr.ek, hpkeHdr.ekLen, ciphertext, ciphertextLen, plaintext, plaintextSz); } if (ret == WOLFCOSE_SUCCESS) { @@ -10360,7 +10434,7 @@ int wc_CoseEncrypt_Decrypt(const WOLFCOSE_RECIPIENT* recipient, const uint8_t* decKey = NULL; const uint8_t* recipientProtectedData = NULL; size_t recipientProtectedLen = 0; - int32_t recipientAlgId = 0; + int32_t recipientAlgId = WOLFCOSE_ALG_UNSET; int recipientMode = 0; WOLFCOSE_HDR recipientHdr; WOLFCOSE_HDR_STATE hdrState; @@ -10368,6 +10442,7 @@ int wc_CoseEncrypt_Decrypt(const WOLFCOSE_RECIPIENT* recipient, int bodyAlgProtected = 0; #if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) int recipientAlgProtected = 0; + int isHpkeRecipient = 0; #endif #if defined(WOLFCOSE_ECDH_ES_DIRECT) && defined(HAVE_ECC) && defined(HAVE_HKDF) uint8_t cek[32]; @@ -10391,14 +10466,14 @@ int wc_CoseEncrypt_Decrypt(const WOLFCOSE_RECIPIENT* recipient, const uint8_t* hpkeWrappedCekData = NULL; size_t hpkeWrappedCekLen = 0u; size_t hpkeRecipientInfoLen = 0u; - WOLFCOSE_HDR recipUnprotHdr; + WOLFCOSE_HPKE_HDR recipHpkeHdr; int useHpke = 0; #endif #if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) (void)XMEMSET(cekHpke, 0, sizeof(cekHpke)); (void)XMEMSET(&recipientHdr, 0, sizeof(recipientHdr)); - (void)XMEMSET(&recipUnprotHdr, 0, sizeof(recipUnprotHdr)); + (void)XMEMSET(&recipHpkeHdr, 0, sizeof(recipHpkeHdr)); #endif /* Parameter validation */ @@ -10552,14 +10627,20 @@ int wc_CoseEncrypt_Decrypt(const WOLFCOSE_RECIPIENT* recipient, WOLFCOSE_HDR_ALG); } if ((ret == WOLFCOSE_SUCCESS) && - (recipientAlgId == WOLFCOSE_ALG_HPKE_0_KE) && - ((wolfCose_HdrStateContains(&recipientHdrState, - WOLFCOSE_HDR_ALG) == 0) || - (wolfCose_HdrStateContains(&recipientHdrState, - WOLFCOSE_HDR_HPKE_EK) != 0) || - (wolfCose_HdrStateContains(&recipientHdrState, - WOLFCOSE_HPKE_0_PSK_ID_LABEL) != 0))) { - ret = WOLFCOSE_E_COSE_BAD_HDR; + (((recipientAlgProtected != 0) && + (recipientAlgId == WOLFCOSE_ALG_HPKE_0_KE)) || + ((recipientAlgProtected == 0) && + (recipient->algId == WOLFCOSE_ALG_HPKE_0_KE)))) { + if ((wolfCose_HdrStateContains(&recipientHdrState, + WOLFCOSE_HDR_HPKE_EK) != 0) || + (wolfCose_HdrStateContains(&recipientHdrState, + WOLFCOSE_HPKE_0_PSK_ID_LABEL) != 0)) { + ret = WOLFCOSE_E_COSE_BAD_HDR; + } + else { + isHpkeRecipient = 1; + recipientAlgId = WOLFCOSE_ALG_HPKE_0_KE; + } } #endif if ((ret == WOLFCOSE_SUCCESS) && @@ -10687,15 +10768,19 @@ int wc_CoseEncrypt_Decrypt(const WOLFCOSE_RECIPIENT* recipient, /* Decode the recipient unprotected map with duplicate-label tracking * (within the map and against the recipient protected bucket). */ #if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) - if (recipientAlgId == WOLFCOSE_ALG_HPKE_0_KE) { - (void)XMEMSET(&recipUnprotHdr, 0, sizeof(recipUnprotHdr)); - ret = wolfCose_DecodeUnprotectedHdr(&ctx, &recipUnprotHdr, - &recipientHdrState); + if (isHpkeRecipient != 0) { + (void)XMEMSET(&recipHpkeHdr, 0, sizeof(recipHpkeHdr)); + ret = wolfCose_DecodeUnprotectedHdrEx(&ctx, &recipientHdr, + &recipientHdrState, + &recipHpkeHdr); if ((ret == WOLFCOSE_SUCCESS) && - ((recipUnprotHdr.iv != NULL) || - (recipUnprotHdr.partialIv != NULL) || - ((recipUnprotHdr.flags & WOLFCOSE_HDR_FLAG_HPKE_EK) == 0u) || - (recipUnprotHdr.hpkeEkLen != WOLFCOSE_HPKE_0_ENC_SZ) || + (((recipientAlgProtected == 0) && + (wolfCose_HdrStateContains(&recipientHdrState, + WOLFCOSE_HDR_ALG) != 0)) || + (recipientHdr.iv != NULL) || + (recipientHdr.partialIv != NULL) || + (recipHpkeHdr.hasEk == 0) || + (recipHpkeHdr.ekLen != WOLFCOSE_HPKE_0_ENC_SZ) || (wolfCose_HdrStateContains(&recipientHdrState, WOLFCOSE_HPKE_0_PSK_ID_LABEL) != 0))) { ret = WOLFCOSE_E_COSE_BAD_HDR; @@ -10713,15 +10798,14 @@ int wc_CoseEncrypt_Decrypt(const WOLFCOSE_RECIPIENT* recipient, } if (ret == WOLFCOSE_SUCCESS) { - recipientAlgId = recipientHdr.alg; - } #if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) - if ((ret == WOLFCOSE_SUCCESS) && - (recipientAlgId == WOLFCOSE_ALG_HPKE_0_KE) && - (recipientAlgProtected == 0)) { - ret = WOLFCOSE_E_COSE_BAD_HDR; - } + if (isHpkeRecipient == 0) { + recipientAlgId = recipientHdr.alg; + } +#else + recipientAlgId = recipientHdr.alg; #endif + } if (ret == WOLFCOSE_SUCCESS) { ret = wolfCose_ValidateRecipientKeyAlg(recipient->key, recipientAlgId, alg); @@ -10781,13 +10865,22 @@ int wc_CoseEncrypt_Decrypt(const WOLFCOSE_RECIPIENT* recipient, } /* An unprotected content algorithm is safe only when direct-key policy - * independently pins the same algorithm. Other recipient modes pin their - * key-management algorithm, not the content algorithm. */ + * independently pins it, or HPKE-0-KE binds it in the authenticated + * Recipient_structure next_layer_alg field. */ +#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) + if ((ret == WOLFCOSE_SUCCESS) && (bodyAlgProtected == 0) && + (recipientAlgId != WOLFCOSE_ALG_HPKE_0_KE) && + ((recipientAlgId != WOLFCOSE_ALG_DIRECT) || + (recipient->key == NULL) || (recipient->key->alg != alg))) { + ret = WOLFCOSE_E_COSE_BAD_ALG; + } +#else if ((ret == WOLFCOSE_SUCCESS) && (bodyAlgProtected == 0) && ((recipientAlgId != WOLFCOSE_ALG_DIRECT) || (recipient->key == NULL) || (recipient->key->alg != alg))) { ret = WOLFCOSE_E_COSE_BAD_ALG; } +#endif #if defined(WOLFCOSE_ECDH_ES_DIRECT) && defined(HAVE_ECC) && defined(HAVE_HKDF) /* RFC 9052 Section 8.5.5: direct key agreement carries exactly one @@ -10893,7 +10986,7 @@ int wc_CoseEncrypt_Decrypt(const WOLFCOSE_RECIPIENT* recipient, WOLFCOSE_ALG_HPKE_0_KE, scratch, hpkeRecipientInfoLen, NULL, 0u, - recipUnprotHdr.hpkeEk, recipUnprotHdr.hpkeEkLen, + recipHpkeHdr.ek, recipHpkeHdr.ekLen, hpkeWrappedCekData, hpkeWrappedCekLen, cekHpke, sizeof(cekHpke)); } diff --git a/tests/test_cose.c b/tests/test_cose.c index 0daa0cb..fc84fd1 100644 --- a/tests/test_cose.c +++ b/tests/test_cose.c @@ -45,6 +45,9 @@ #if defined(WOLFCOSE_HAVE_ES256) || defined(WOLFCOSE_HAVE_HPKE_0) #include <wolfssl/wolfcrypt/ecc.h> #endif +#if defined(WOLFCOSE_HAVE_HPKE_0) + #include <wolfssl/wolfcrypt/hpke.h> +#endif #ifdef WOLFCOSE_HAVE_EDDSA #include <wolfssl/wolfcrypt/ed25519.h> #endif @@ -67,6 +70,7 @@ #ifdef WOLFCOSE_TEST_LOG_ENABLE #include <stdio.h> #endif +#include <stddef.h> #include <string.h> #ifdef WOLFCOSE_FORCE_FAILURE #include "force_failure.h" @@ -98,6 +102,46 @@ static int g_failures = 0; } \ } while (0) +/* This replica is the public WOLFCOSE_HDR layout before COSE-HPKE was added. + * Keep the compatibility check in the default test build so a feature-gated + * addition cannot silently grow the ABI-visible structure. */ +typedef struct WOLFCOSE_HDR_PRE_HPKE { + int32_t alg; + const uint8_t* kid; + size_t kidLen; + const uint8_t* iv; + size_t ivLen; + const uint8_t* partialIv; + size_t partialIvLen; + int32_t contentType; + uint8_t flags; +} WOLFCOSE_HDR_PRE_HPKE; + +static void test_cose_hdr_abi_layout(void) +{ + TEST_LOG(" [WOLFCOSE_HDR ABI layout]\n"); + TEST_ASSERT(sizeof(WOLFCOSE_HDR) == sizeof(WOLFCOSE_HDR_PRE_HPKE), + "hpke preserves WOLFCOSE_HDR size"); + TEST_ASSERT(offsetof(WOLFCOSE_HDR, alg) == + offsetof(WOLFCOSE_HDR_PRE_HPKE, alg), + "hpke preserves WOLFCOSE_HDR alg offset"); + TEST_ASSERT(offsetof(WOLFCOSE_HDR, kid) == + offsetof(WOLFCOSE_HDR_PRE_HPKE, kid), + "hpke preserves WOLFCOSE_HDR kid offset"); + TEST_ASSERT(offsetof(WOLFCOSE_HDR, iv) == + offsetof(WOLFCOSE_HDR_PRE_HPKE, iv), + "hpke preserves WOLFCOSE_HDR iv offset"); + TEST_ASSERT(offsetof(WOLFCOSE_HDR, partialIv) == + offsetof(WOLFCOSE_HDR_PRE_HPKE, partialIv), + "hpke preserves WOLFCOSE_HDR partial IV offset"); + TEST_ASSERT(offsetof(WOLFCOSE_HDR, contentType) == + offsetof(WOLFCOSE_HDR_PRE_HPKE, contentType), + "hpke preserves WOLFCOSE_HDR content type offset"); + TEST_ASSERT(offsetof(WOLFCOSE_HDR, flags) == + offsetof(WOLFCOSE_HDR_PRE_HPKE, flags), + "hpke preserves WOLFCOSE_HDR flags offset"); +} + #if (defined(WOLFCOSE_MAC) && defined(WOLFCOSE_HAVE_HMAC256)) || \ (defined(WOLFCOSE_ENCRYPT) && defined(WOLFCOSE_HAVE_AESGCM)) /* Locate the one-byte direct-alg value in a generated recipient header. */ @@ -9506,21 +9550,76 @@ static void test_cose_encrypt_multi_recipient(void) #endif /* WOLFCOSE_ENCRYPT && WOLFCOSE_HAVE_AESGCM && WOLFCOSE_KEY_WRAP */ +#if (defined(WOLFCOSE_HPKE_0_ENCRYPT) && \ + defined(WOLFCOSE_HPKE_0_DECRYPT)) || \ + (defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) && \ + defined(WOLFCOSE_HPKE_0_KE_DECRYPT)) +static int test_cose_hpke_ciphertext_len(const uint8_t* encoded, + size_t encodedLen, uint64_t expectedTag, size_t expectedItems, + size_t* ciphertextLen) +{ + WOLFCOSE_CBOR_CTX ctx; + uint64_t tag = 0u; + size_t items = 0u; + const uint8_t* ciphertext = NULL; + int ret; + + if ((encoded == NULL) || (ciphertextLen == NULL)) { + return WOLFCOSE_E_INVALID_ARG; + } + (void)XMEMSET(&ctx, 0, sizeof(ctx)); + ctx.cbuf = encoded; + ctx.bufSz = encodedLen; + ret = wc_CBOR_DecodeTag(&ctx, &tag); + if ((ret == WOLFCOSE_SUCCESS) && (tag != expectedTag)) { + ret = WOLFCOSE_E_COSE_BAD_HDR; + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_DecodeArrayStart(&ctx, &items); + } + if ((ret == WOLFCOSE_SUCCESS) && (items != expectedItems)) { + ret = WOLFCOSE_E_COSE_BAD_HDR; + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_Skip(&ctx); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_Skip(&ctx); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_DecodeBstr(&ctx, &ciphertext, ciphertextLen); + } + return ret; +} +#endif + #if defined(WOLFCOSE_HPKE_0_ENCRYPT) && defined(WOLFCOSE_HPKE_0_DECRYPT) static void test_cose_hpke_encrypt0(void) { WOLFCOSE_KEY recipientKey; WOLFCOSE_KEY wrongKey; +#if defined(HAVE_ECC_KOBLITZ) + WOLFCOSE_KEY nonP256Key; +#endif WOLFCOSE_HDR hdr; ecc_key recipientEcc; ecc_key wrongEcc; +#if defined(HAVE_ECC_KOBLITZ) + ecc_key nonP256Ecc; +#endif WC_RNG rng; int ret = WOLFCOSE_SUCCESS; int rngInited = 0; int recipientEccInited = 0; int wrongEccInited = 0; +#if defined(HAVE_ECC_KOBLITZ) + int nonP256EccInited = 0; +#endif int recipientKeyInited = 0; int wrongKeyInited = 0; +#if defined(HAVE_ECC_KOBLITZ) + int nonP256KeyInited = 0; +#endif uint8_t out[512]; uint8_t detached[128]; uint8_t plaintext[128]; @@ -9528,14 +9627,19 @@ static void test_cose_hpke_encrypt0(void) size_t outLen = 0u; size_t detachedLen = 0u; size_t plaintextLen = 0u; + size_t ciphertextLen = 0u; const uint8_t kid[] = "hpke-recipient"; const uint8_t payload[] = "COSE HPKE integrated encryption"; + const uint8_t emptyPayload[] = { 0u }; const uint8_t aad[] = "hpke external aad"; const uint8_t wrongAad[] = "wrong hpke external aad"; TEST_LOG(" [COSE HPKE-0 Encrypt0]\n"); (void)XMEMSET(&recipientEcc, 0, sizeof(recipientEcc)); (void)XMEMSET(&wrongEcc, 0, sizeof(wrongEcc)); +#if defined(HAVE_ECC_KOBLITZ) + (void)XMEMSET(&nonP256Ecc, 0, sizeof(nonP256Ecc)); +#endif ret = wc_InitRng(&rng); TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke encrypt0 rng init"); @@ -9581,12 +9685,71 @@ static void test_cose_hpke_encrypt0(void) } if (ret == WOLFCOSE_SUCCESS) { wrongKey.alg = WOLFCOSE_ALG_HPKE_0; + } +#if defined(HAVE_ECC_KOBLITZ) + if (rngInited != 0) { + int curveRet; + size_t curveOutLen = sizeof(out); + + curveRet = wc_ecc_init(&nonP256Ecc); + TEST_ASSERT(curveRet == WOLFCOSE_SUCCESS, + "hpke encrypt0 non-P256 ecc init"); + if (curveRet == WOLFCOSE_SUCCESS) { + nonP256EccInited = 1; + curveRet = wc_ecc_make_key_ex(&rng, 32, &nonP256Ecc, + ECC_SECP256K1); + TEST_ASSERT(curveRet == WOLFCOSE_SUCCESS, + "hpke encrypt0 non-P256 key make"); + } + if (curveRet == WOLFCOSE_SUCCESS) { + curveRet = wc_CoseKey_Init(&nonP256Key); + TEST_ASSERT(curveRet == WOLFCOSE_SUCCESS, + "hpke encrypt0 non-P256 COSE key init"); + } + if (curveRet == WOLFCOSE_SUCCESS) { + nonP256KeyInited = 1; + /* Deliberately bypass SetEcc: this verifies the HPKE boundary + * when caller metadata claims P-256 for a 32-byte Koblitz key. */ + nonP256Key.kty = WOLFCOSE_KTY_EC2; + nonP256Key.crv = WOLFCOSE_CRV_P256; + nonP256Key.key.ecc = &nonP256Ecc; + nonP256Key.hasPrivate = 0u; + nonP256Key.alg = WOLFCOSE_ALG_HPKE_0; + curveRet = wc_CoseHpkeEncrypt0_Encrypt(&nonP256Key, kid, + sizeof(kid) - 1u, payload, sizeof(payload) - 1u, + NULL, 0u, NULL, aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), out, sizeof(out), &curveOutLen, + &rng); + TEST_ASSERT(curveRet == WOLFCOSE_E_COSE_KEY_TYPE, + "hpke encrypt0 non-P256 key rejected"); + TEST_ASSERT(curveOutLen == 0u, + "hpke encrypt0 non-P256 output length cleared"); + } + } +#endif + if (ret == WOLFCOSE_SUCCESS) { ret = wc_CoseHpkeEncrypt0_Encrypt(&recipientKey, kid, sizeof(kid) - 1u, payload, sizeof(payload) - 1u, NULL, 0u, NULL, aad, sizeof(aad) - 1u, scratch, sizeof(scratch), out, sizeof(out), &outLen, &rng); TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke encrypt0 encrypt"); } + if (ret == WOLFCOSE_SUCCESS) { + uint8_t tinyOut[1]; + size_t failedOutLen = sizeof(out); + int failedRet; + + (void)XMEMSET(tinyOut, 0xa5, sizeof(tinyOut)); + failedRet = wc_CoseHpkeEncrypt0_Encrypt(&recipientKey, kid, + sizeof(kid) - 1u, payload, sizeof(payload) - 1u, + NULL, 0u, NULL, aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), tinyOut, sizeof(tinyOut), + &failedOutLen, &rng); + TEST_ASSERT(failedRet != WOLFCOSE_SUCCESS, + "hpke encrypt0 undersized output rejected"); + TEST_ASSERT((failedOutLen == 0u) && (tinyOut[0] == 0u), + "hpke encrypt0 failed out length cleared"); + } if (ret == WOLFCOSE_SUCCESS) { recipientKey.hasPrivate = 1u; (void)XMEMSET(&hdr, 0, sizeof(hdr)); @@ -9601,10 +9764,6 @@ static void test_cose_hpke_encrypt0(void) "hpke encrypt0 payload matches"); TEST_ASSERT(hdr.alg == WOLFCOSE_ALG_HPKE_0, "hpke encrypt0 protected algorithm"); - TEST_ASSERT((hdr.flags & WOLFCOSE_HDR_FLAG_HPKE_EK) != 0u, - "hpke encrypt0 encapsulated key present"); - TEST_ASSERT(hdr.hpkeEkLen == 65u, - "hpke encrypt0 P-256 encapsulated key length"); TEST_ASSERT((hdr.kidLen == (sizeof(kid) - 1u)) && (memcmp(hdr.kid, kid, hdr.kidLen) == 0), "hpke encrypt0 kid decoded"); @@ -9659,16 +9818,69 @@ static void test_cose_hpke_encrypt0(void) "hpke encrypt0 detached payload matches"); } } + if (recipientKeyInited != 0) { + recipientKey.hasPrivate = 0u; + ret = wc_CoseHpkeEncrypt0_Encrypt(&recipientKey, kid, + sizeof(kid) - 1u, emptyPayload, 0u, + NULL, 0u, NULL, aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), out, sizeof(out), &outLen, &rng); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke encrypt0 empty attached encrypt"); + if (ret == WOLFCOSE_SUCCESS) { + ret = test_cose_hpke_ciphertext_len(out, outLen, + WOLFCOSE_TAG_ENCRYPT0, 3u, &ciphertextLen); + TEST_ASSERT((ret == WOLFCOSE_SUCCESS) && (ciphertextLen == 16u), + "hpke encrypt0 empty attached tag length"); + recipientKey.hasPrivate = 1u; + (void)XMEMSET(&hdr, 0, sizeof(hdr)); + ret = wc_CoseHpkeEncrypt0_Decrypt(&recipientKey, out, outLen, + NULL, 0u, aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), &hdr, + plaintext, sizeof(plaintext), &plaintextLen); + TEST_ASSERT((ret == WOLFCOSE_SUCCESS) && (plaintextLen == 0u), + "hpke encrypt0 empty attached decrypt"); + } + } + if (recipientKeyInited != 0) { + recipientKey.hasPrivate = 0u; + ret = wc_CoseHpkeEncrypt0_Encrypt(&recipientKey, kid, + sizeof(kid) - 1u, emptyPayload, 0u, + detached, sizeof(detached), &detachedLen, + aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), out, sizeof(out), &outLen, &rng); + TEST_ASSERT((ret == WOLFCOSE_SUCCESS) && (detachedLen == 16u), + "hpke encrypt0 empty detached tag length"); + if (ret == WOLFCOSE_SUCCESS) { + recipientKey.hasPrivate = 1u; + (void)XMEMSET(&hdr, 0, sizeof(hdr)); + ret = wc_CoseHpkeEncrypt0_Decrypt(&recipientKey, out, outLen, + detached, detachedLen, aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), &hdr, + plaintext, sizeof(plaintext), &plaintextLen); + TEST_ASSERT((ret == WOLFCOSE_SUCCESS) && (plaintextLen == 0u), + "hpke encrypt0 empty detached decrypt"); + } + } if (wrongKeyInited != 0) { wc_CoseKey_Free(&wrongKey); } +#if defined(HAVE_ECC_KOBLITZ) + if (nonP256KeyInited != 0) { + wc_CoseKey_Free(&nonP256Key); + } +#endif if (recipientKeyInited != 0) { wc_CoseKey_Free(&recipientKey); } if (wrongEccInited != 0) { (void)wc_ecc_free(&wrongEcc); } +#if defined(HAVE_ECC_KOBLITZ) + if (nonP256EccInited != 0) { + (void)wc_ecc_free(&nonP256Ecc); + } +#endif if (recipientEccInited != 0) { (void)wc_ecc_free(&recipientEcc); } @@ -9678,6 +9890,794 @@ static void test_cose_hpke_encrypt0(void) } #endif /* WOLFCOSE_HPKE_0_ENCRYPT && WOLFCOSE_HPKE_0_DECRYPT */ +#if defined(WOLFCOSE_HPKE_0_ENCRYPT) && defined(WOLFCOSE_HPKE_0_DECRYPT) +/* draft-ietf-cose-hpke-26 permits an integrated HPKE `alg` to be absent. + * Build a valid base-mode message directly with wolfCrypt so this test covers + * the omitted-header wire form, not merely a parser mutation. */ +static void test_cose_hpke_encrypt0_optional_alg(void) +{ + static const uint8_t encContext[] = "Encrypt0"; + static const uint8_t payload[] = "HPKE without a protected algorithm"; + WOLFCOSE_KEY recipientKey; + WOLFCOSE_HDR hdr; + WC_RNG rng; + ecc_key recipientEcc; + ecc_key ephemeralEcc; + Hpke hpke; + WOLFCOSE_CBOR_CTX ctx; + uint8_t emptyHdr = 0u; + uint8_t scratch[256]; + uint8_t enc[65]; + uint8_t ciphertext[sizeof(payload) - 1u + 16u]; + uint8_t encoded[256]; + uint8_t plaintext[sizeof(payload)]; + size_t encStructLen = 0u; + size_t encodedLen = 0u; + word16 encLen = (word16)sizeof(enc); + int ret = WOLFCOSE_SUCCESS; + int rngInited = 0; + int recipientEccInited = 0; + int ephemeralEccInited = 0; + int keyInited = 0; + enum { + HPKE_HDR_CASE_UNPROTECTED_ALG = 0x01u, + HPKE_HDR_CASE_NO_EK = 0x02u, + HPKE_HDR_CASE_SHORT_EK = 0x04u, + HPKE_HDR_CASE_DUPLICATE_EK = 0x08u, + HPKE_HDR_CASE_NON_BSTR_EK = 0x10u, + HPKE_HDR_CASE_PSK_ID = 0x20u, + HPKE_HDR_CASE_PROTECTED_EK = 0x40u, + HPKE_HDR_CASE_DETACHED = 0x80u + }; + typedef struct WOLFCOSE_HPKE_HDR_CASE { + uint32_t flags; + int expected; + const char* name; + } WOLFCOSE_HPKE_HDR_CASE; + static const WOLFCOSE_HPKE_HDR_CASE cases[] = { + { 0u, WOLFCOSE_SUCCESS, "hpke optional protected alg accepted" }, + { HPKE_HDR_CASE_UNPROTECTED_ALG, WOLFCOSE_E_COSE_BAD_HDR, + "hpke unprotected alg rejected" }, + { HPKE_HDR_CASE_NO_EK, WOLFCOSE_E_COSE_BAD_HDR, + "hpke missing ek rejected" }, + { HPKE_HDR_CASE_SHORT_EK, WOLFCOSE_E_COSE_BAD_HDR, + "hpke short ek rejected" }, + { HPKE_HDR_CASE_DUPLICATE_EK, WOLFCOSE_E_CBOR_MALFORMED, + "hpke duplicate ek rejected" }, + { HPKE_HDR_CASE_NON_BSTR_EK, WOLFCOSE_E_CBOR_TYPE, + "hpke non-bstr ek rejected" }, + { HPKE_HDR_CASE_PSK_ID, WOLFCOSE_E_COSE_BAD_HDR, + "hpke psk id rejected" }, + { HPKE_HDR_CASE_NO_EK | HPKE_HDR_CASE_PROTECTED_EK, + WOLFCOSE_E_COSE_BAD_HDR, "hpke protected ek rejected" }, + { HPKE_HDR_CASE_DETACHED, WOLFCOSE_E_DETACHED_PAYLOAD, + "hpke detached ciphertext required" } + }; + static const uint8_t pskId[] = { 0u }; + static const uint8_t nonBstrEk[] = "not-an-enc"; + size_t caseIndex; + + TEST_LOG(" [COSE HPKE Encrypt0 optional algorithm]\n"); + (void)XMEMSET(&recipientKey, 0, sizeof(recipientKey)); + (void)XMEMSET(&recipientEcc, 0, sizeof(recipientEcc)); + (void)XMEMSET(&ephemeralEcc, 0, sizeof(ephemeralEcc)); + (void)XMEMSET(&hpke, 0, sizeof(hpke)); + + ret = wc_InitRng(&rng); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke optional alg rng init"); + if (ret == WOLFCOSE_SUCCESS) { + rngInited = 1; + ret = wc_ecc_init(&recipientEcc); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke optional alg recipient ecc init"); + } + if (ret == WOLFCOSE_SUCCESS) { + recipientEccInited = 1; + ret = wc_ecc_make_key_ex(&rng, 32, &recipientEcc, ECC_SECP256R1); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke optional alg recipient key make"); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CoseKey_Init(&recipientKey); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke optional alg cose key init"); + } + if (ret == WOLFCOSE_SUCCESS) { + keyInited = 1; + ret = wc_CoseKey_SetEcc(&recipientKey, WOLFCOSE_CRV_P256, + &recipientEcc); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke optional alg cose key set"); + recipientKey.alg = WOLFCOSE_ALG_HPKE_0; + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_HpkeInit(&hpke, DHKEM_P256_HKDF_SHA256, HKDF_SHA256, + HPKE_AES_128_GCM, NULL); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke optional alg init"); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_ecc_init(&ephemeralEcc); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke optional alg ephemeral ecc init"); + } + if (ret == WOLFCOSE_SUCCESS) { + ephemeralEccInited = 1; + ret = wc_ecc_make_key_ex(&rng, 32, &ephemeralEcc, ECC_SECP256R1); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke optional alg ephemeral key make"); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_HpkeSerializePublicKey(&hpke, &ephemeralEcc, enc, &encLen); + TEST_ASSERT((ret == WOLFCOSE_SUCCESS) && (encLen == sizeof(enc)), + "hpke optional alg encapsulated key"); + } + if (ret == WOLFCOSE_SUCCESS) { + (void)XMEMSET(&ctx, 0, sizeof(ctx)); + ctx.buf = scratch; + ctx.bufSz = sizeof(scratch); + ret = wc_CBOR_EncodeArrayStart(&ctx, 3u); + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeTstr(&ctx, encContext, + sizeof(encContext) - 1u); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeBstr(&ctx, &emptyHdr, 0u); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeBstr(&ctx, NULL, 0u); + } + if (ret == WOLFCOSE_SUCCESS) { + encStructLen = ctx.idx; + } + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke optional alg Encrypt0 structure"); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_HpkeSealBase(&hpke, &ephemeralEcc, &recipientEcc, + NULL, 0u, scratch, (word32)encStructLen, + (byte*)payload, (word32)(sizeof(payload) - 1u), ciphertext); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke optional alg seal"); + } + + for (caseIndex = 0u; + (ret == WOLFCOSE_SUCCESS) && (caseIndex < + (sizeof(cases) / sizeof(cases[0]))); + caseIndex++) { + WOLFCOSE_CBOR_CTX protectedCtx; + WOLFCOSE_HDR clearedHdr; + const uint8_t* protectedCase = &emptyHdr; + size_t protectedCaseLen = 0u; + size_t mapEntries = 0u; + uint8_t protectedHdr[96]; + uint8_t clearedPlaintext[sizeof(plaintext)]; + int decRet = WOLFCOSE_SUCCESS; + size_t plaintextLen = sizeof(plaintext); + + if ((cases[caseIndex].flags & HPKE_HDR_CASE_PROTECTED_EK) != 0u) { + (void)XMEMSET(&protectedCtx, 0, sizeof(protectedCtx)); + protectedCtx.buf = protectedHdr; + protectedCtx.bufSz = sizeof(protectedHdr); + decRet = wc_CBOR_EncodeMapStart(&protectedCtx, 1u); + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeInt(&protectedCtx, + WOLFCOSE_HDR_HPKE_EK); + } + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeBstr(&protectedCtx, enc, sizeof(enc)); + } + if (decRet == WOLFCOSE_SUCCESS) { + protectedCase = protectedHdr; + protectedCaseLen = protectedCtx.idx; + } + } + + (void)XMEMSET(&ctx, 0, sizeof(ctx)); + ctx.buf = encoded; + ctx.bufSz = sizeof(encoded); + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeTag(&ctx, WOLFCOSE_TAG_ENCRYPT0); + } + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeArrayStart(&ctx, 3u); + } + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeBstr(&ctx, protectedCase, + protectedCaseLen); + } + if ((cases[caseIndex].flags & HPKE_HDR_CASE_NO_EK) == 0u) { + mapEntries++; + } + if ((cases[caseIndex].flags & HPKE_HDR_CASE_UNPROTECTED_ALG) != 0u) { + mapEntries++; + } + if ((cases[caseIndex].flags & HPKE_HDR_CASE_DUPLICATE_EK) != 0u) { + mapEntries++; + } + if ((cases[caseIndex].flags & HPKE_HDR_CASE_PSK_ID) != 0u) { + mapEntries++; + } + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeMapStart(&ctx, mapEntries); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((cases[caseIndex].flags & HPKE_HDR_CASE_UNPROTECTED_ALG) != 0u)) { + decRet = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_HDR_ALG); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((cases[caseIndex].flags & HPKE_HDR_CASE_UNPROTECTED_ALG) != 0u)) { + decRet = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_ALG_HPKE_0); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((cases[caseIndex].flags & HPKE_HDR_CASE_NO_EK) == 0u)) { + decRet = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_HDR_HPKE_EK); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((cases[caseIndex].flags & HPKE_HDR_CASE_NO_EK) == 0u) && + ((cases[caseIndex].flags & HPKE_HDR_CASE_NON_BSTR_EK) != 0u)) { + decRet = wc_CBOR_EncodeTstr(&ctx, nonBstrEk, + sizeof(nonBstrEk) - 1u); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((cases[caseIndex].flags & HPKE_HDR_CASE_NO_EK) == 0u) && + ((cases[caseIndex].flags & HPKE_HDR_CASE_NON_BSTR_EK) == 0u)) { + size_t ekLen = sizeof(enc); + + if ((cases[caseIndex].flags & HPKE_HDR_CASE_SHORT_EK) != 0u) { + ekLen--; + } + decRet = wc_CBOR_EncodeBstr(&ctx, enc, ekLen); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((cases[caseIndex].flags & HPKE_HDR_CASE_DUPLICATE_EK) != 0u)) { + decRet = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_HDR_HPKE_EK); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((cases[caseIndex].flags & HPKE_HDR_CASE_DUPLICATE_EK) != 0u)) { + decRet = wc_CBOR_EncodeBstr(&ctx, enc, sizeof(enc)); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((cases[caseIndex].flags & HPKE_HDR_CASE_PSK_ID) != 0u)) { + decRet = wc_CBOR_EncodeInt(&ctx, -5); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((cases[caseIndex].flags & HPKE_HDR_CASE_PSK_ID) != 0u)) { + decRet = wc_CBOR_EncodeBstr(&ctx, pskId, sizeof(pskId)); + } + if (decRet == WOLFCOSE_SUCCESS) { + if ((cases[caseIndex].flags & HPKE_HDR_CASE_DETACHED) != 0u) { + decRet = wc_CBOR_EncodeNull(&ctx); + } + else { + decRet = wc_CBOR_EncodeBstr(&ctx, ciphertext, + sizeof(ciphertext)); + } + } + if (decRet == WOLFCOSE_SUCCESS) { + encodedLen = ctx.idx; + (void)XMEMSET(&hdr, 0xa5, sizeof(hdr)); + (void)XMEMSET(plaintext, 0xa5, sizeof(plaintext)); + decRet = wc_CoseHpkeEncrypt0_Decrypt(&recipientKey, + encoded, encodedLen, NULL, 0u, NULL, 0u, + scratch, sizeof(scratch), &hdr, plaintext, sizeof(plaintext), + &plaintextLen); + } + TEST_ASSERT(decRet == cases[caseIndex].expected, cases[caseIndex].name); + if (cases[caseIndex].expected == WOLFCOSE_SUCCESS) { + TEST_ASSERT(decRet == WOLFCOSE_SUCCESS, + "hpke optional protected alg accepted"); + TEST_ASSERT((hdr.alg == WOLFCOSE_ALG_UNSET) && + (plaintextLen == (sizeof(payload) - 1u)) && + (memcmp(plaintext, payload, plaintextLen) == 0), + "hpke optional protected alg payload"); + } + else { + (void)XMEMSET(&clearedHdr, 0, sizeof(clearedHdr)); + (void)XMEMSET(clearedPlaintext, 0, sizeof(clearedPlaintext)); + TEST_ASSERT((plaintextLen == 0u) && + (memcmp(&hdr, &clearedHdr, sizeof(hdr)) == 0) && + (memcmp(plaintext, clearedPlaintext, + sizeof(plaintext)) == 0), + "hpke malformed header clears outputs"); + } + } + + if (keyInited != 0) { + wc_CoseKey_Free(&recipientKey); + } + if (ephemeralEccInited != 0) { + (void)wc_ecc_free(&ephemeralEcc); + } + if (recipientEccInited != 0) { + (void)wc_ecc_free(&recipientEcc); + } + if (rngInited != 0) { + (void)wc_FreeRng(&rng); + } + (void)XMEMSET(&hpke, 0, sizeof(hpke)); +} +#endif /* WOLFCOSE_HPKE_0_ENCRYPT && WOLFCOSE_HPKE_0_DECRYPT */ + +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) && \ + defined(WOLFCOSE_HPKE_0_KE_DECRYPT) +/* The multi-recipient draft permits an omitted recipient `alg`, and its + * Recipient_structure authenticates an unprotected content algorithm. Build + * two valid empty-protected recipients so recipient index 1 also covers + * skipped-entry classification. */ +static void test_cose_hpke_key_encryption_optional_alg(void) +{ + static const uint8_t hpkeRecipientContext[] = "HPKE Recipient"; + static const uint8_t payload[] = "HPKE key encryption without alg"; + static const uint8_t keyData[16] = { + 0x00u, 0x01u, 0x02u, 0x03u, 0x04u, 0x05u, 0x06u, 0x07u, + 0x08u, 0x09u, 0x0au, 0x0bu, 0x0cu, 0x0du, 0x0eu, 0x0fu + }; + static const uint8_t iv[12] = { + 0x11u, 0x12u, 0x13u, 0x14u, 0x15u, 0x16u, + 0x17u, 0x18u, 0x19u, 0x1au, 0x1bu, 0x1cu + }; + WOLFCOSE_KEY recipientKey; + WOLFCOSE_RECIPIENT hpkeRecipient; + WOLFCOSE_HDR hdr; + WC_RNG rng; + ecc_key recipientEcc; + ecc_key ephemeralEcc; + Hpke hpke; + Aes aes; + WOLFCOSE_CBOR_CTX ctx; + uint8_t emptyHdr = 0u; + uint8_t aad[32]; + uint8_t scratch[256]; + uint8_t enc[65]; + uint8_t bodyCiphertext[sizeof(payload) - 1u + WOLFCOSE_AES_GCM_TAG_SZ]; + uint8_t wrappedCek[sizeof(keyData) + 16u + 8u]; + uint8_t encoded[1024]; + uint8_t plaintext[sizeof(payload)]; + size_t aadLen = 0u; + size_t encodedLen = 0u; + size_t recipientInfoLen = 0u; + size_t wrappedCekLen = sizeof(keyData) + 16u; + word16 encLen = (word16)sizeof(enc); + int ret = WOLFCOSE_SUCCESS; + int rngInited = 0; + int recipientEccInited = 0; + int ephemeralEccInited = 0; + int recipientKeyInited = 0; + int aesInited = 0; + enum { + HPKE_KE_HDR_CASE_UNPROTECTED_ALG = 0x01u, + HPKE_KE_HDR_CASE_NO_EK = 0x02u, + HPKE_KE_HDR_CASE_SHORT_EK = 0x04u, + HPKE_KE_HDR_CASE_DUPLICATE_EK = 0x08u, + HPKE_KE_HDR_CASE_NON_BSTR_EK = 0x10u, + HPKE_KE_HDR_CASE_PSK_ID = 0x20u, + HPKE_KE_HDR_CASE_PROTECTED_EK = 0x40u, + HPKE_KE_HDR_CASE_SKIP_ZERO_ALG = 0x80u, + HPKE_KE_HDR_CASE_SKIP_TEXT_ALG = 0x100u + }; + typedef struct WOLFCOSE_HPKE_KE_HDR_CASE { + uint32_t flags; + int expected; + const char* name; + } WOLFCOSE_HPKE_KE_HDR_CASE; + static const WOLFCOSE_HPKE_KE_HDR_CASE cases[] = { + { 0u, WOLFCOSE_SUCCESS, + "hpke ke optional recipient alg and body alg accepted" }, + { HPKE_KE_HDR_CASE_UNPROTECTED_ALG, WOLFCOSE_E_COSE_BAD_HDR, + "hpke ke unprotected alg rejected" }, + { HPKE_KE_HDR_CASE_SKIP_ZERO_ALG, WOLFCOSE_E_COSE_BAD_ALG, + "hpke ke skipped zero alg rejected" }, + { HPKE_KE_HDR_CASE_SKIP_TEXT_ALG, WOLFCOSE_E_COSE_BAD_ALG, + "hpke ke skipped text alg rejected" }, + { HPKE_KE_HDR_CASE_NO_EK, WOLFCOSE_E_COSE_BAD_HDR, + "hpke ke missing ek rejected" }, + { HPKE_KE_HDR_CASE_SHORT_EK, WOLFCOSE_E_COSE_BAD_HDR, + "hpke ke short ek rejected" }, + { HPKE_KE_HDR_CASE_DUPLICATE_EK, WOLFCOSE_E_CBOR_MALFORMED, + "hpke ke duplicate ek rejected" }, + { HPKE_KE_HDR_CASE_NON_BSTR_EK, WOLFCOSE_E_CBOR_TYPE, + "hpke ke non-bstr ek rejected" }, + { HPKE_KE_HDR_CASE_PSK_ID, WOLFCOSE_E_COSE_BAD_HDR, + "hpke ke psk id rejected" }, + { HPKE_KE_HDR_CASE_NO_EK | HPKE_KE_HDR_CASE_PROTECTED_EK, + WOLFCOSE_E_COSE_BAD_HDR, "hpke ke protected ek rejected" } + }; + static const uint8_t pskId[] = { 0u }; + static const uint8_t nonBstrEk[] = "not-an-enc"; + static const uint8_t invalidAlg[] = "invalid"; + size_t caseIndex; + size_t i; + + TEST_LOG(" [COSE HPKE key-encryption optional algorithm]\n"); + (void)XMEMSET(&recipientKey, 0, sizeof(recipientKey)); + (void)XMEMSET(&recipientEcc, 0, sizeof(recipientEcc)); + (void)XMEMSET(&ephemeralEcc, 0, sizeof(ephemeralEcc)); + (void)XMEMSET(&hpke, 0, sizeof(hpke)); + (void)XMEMSET(&aes, 0, sizeof(aes)); + + (void)XMEMSET(&ctx, 0, sizeof(ctx)); + ctx.buf = aad; + ctx.bufSz = sizeof(aad); + ret = wc_CBOR_EncodeArrayStart(&ctx, 3u); + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeTstr(&ctx, WOLFCOSE_CTX_ENCRYPT, + sizeof(WOLFCOSE_CTX_ENCRYPT)); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeBstr(&ctx, NULL, 0u); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeBstr(&ctx, NULL, 0u); + } + if (ret == WOLFCOSE_SUCCESS) { + aadLen = ctx.idx; + } + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke ke unprotected body alg Enc_structure"); + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_AesInit(&aes, NULL, INVALID_DEVID); + if (ret == 0) { + aesInited = 1; + ret = wc_AesGcmSetKey(&aes, keyData, sizeof(keyData)); + } + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_AesGcmEncrypt(&aes, bodyCiphertext, payload, + (word32)(sizeof(payload) - 1u), iv, (word32)sizeof(iv), + &bodyCiphertext[sizeof(payload) - 1u], + (word32)WOLFCOSE_AES_GCM_TAG_SZ, aad, (word32)aadLen); + } + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke ke unprotected body alg encrypt"); + if (aesInited != 0) { + (void)wc_AesFree(&aes); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_InitRng(&rng); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke optional rng init"); + } + if (ret == WOLFCOSE_SUCCESS) { + rngInited = 1; + ret = wc_ecc_init(&recipientEcc); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke ke optional recipient ecc init"); + } + if (ret == WOLFCOSE_SUCCESS) { + recipientEccInited = 1; + ret = wc_ecc_make_key_ex(&rng, 32, &recipientEcc, ECC_SECP256R1); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke ke optional recipient key make"); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CoseKey_Init(&recipientKey); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke ke optional recipient cose key init"); + } + if (ret == WOLFCOSE_SUCCESS) { + recipientKeyInited = 1; + ret = wc_CoseKey_SetEcc(&recipientKey, WOLFCOSE_CRV_P256, + &recipientEcc); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke ke optional recipient cose key set"); + recipientKey.alg = WOLFCOSE_ALG_HPKE_0_KE; + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_HpkeInit(&hpke, DHKEM_P256_HKDF_SHA256, HKDF_SHA256, + HPKE_AES_128_GCM, NULL); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke optional init"); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_ecc_init(&ephemeralEcc); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke ke optional ephemeral ecc init"); + } + if (ret == WOLFCOSE_SUCCESS) { + ephemeralEccInited = 1; + ret = wc_ecc_make_key_ex(&rng, 32, &ephemeralEcc, ECC_SECP256R1); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke ke optional ephemeral key make"); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_HpkeSerializePublicKey(&hpke, &ephemeralEcc, enc, &encLen); + TEST_ASSERT((ret == WOLFCOSE_SUCCESS) && (encLen == sizeof(enc)), + "hpke ke optional encapsulated key"); + } + if (ret == WOLFCOSE_SUCCESS) { + (void)XMEMSET(&ctx, 0, sizeof(ctx)); + ctx.buf = scratch; + ctx.bufSz = sizeof(scratch); + ret = wc_CBOR_EncodeArrayStart(&ctx, 4u); + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeTstr(&ctx, hpkeRecipientContext, + sizeof(hpkeRecipientContext) - 1u); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_ALG_A128GCM); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeBstr(&ctx, &emptyHdr, 0u); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeBstr(&ctx, NULL, 0u); + } + if (ret == WOLFCOSE_SUCCESS) { + recipientInfoLen = ctx.idx; + } + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, + "hpke ke optional Recipient structure"); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_HpkeSealBase(&hpke, &ephemeralEcc, &recipientEcc, + scratch, (word32)recipientInfoLen, NULL, 0u, + (byte*)keyData, (word32)sizeof(keyData), wrappedCek); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke optional seal"); + } + + for (caseIndex = 0u; + (ret == WOLFCOSE_SUCCESS) && (caseIndex < + (sizeof(cases) / sizeof(cases[0]))); + caseIndex++) { + WOLFCOSE_CBOR_CTX protectedCtx; + WOLFCOSE_HDR clearedHdr; + const uint8_t* protectedCase = &emptyHdr; + size_t protectedCaseLen = 0u; + uint8_t protectedHdr[96]; + uint8_t clearedPlaintext[sizeof(plaintext)]; + int decRet = WOLFCOSE_SUCCESS; + size_t bodyAlgOffset = 0u; + size_t wrappedCekLenOffset = 0u; + size_t wrappedCekDataEnd = 0u; + size_t plaintextLen = sizeof(plaintext); + + if ((cases[caseIndex].flags & HPKE_KE_HDR_CASE_PROTECTED_EK) != 0u) { + (void)XMEMSET(&protectedCtx, 0, sizeof(protectedCtx)); + protectedCtx.buf = protectedHdr; + protectedCtx.bufSz = sizeof(protectedHdr); + decRet = wc_CBOR_EncodeMapStart(&protectedCtx, 1u); + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeInt(&protectedCtx, + WOLFCOSE_HDR_HPKE_EK); + } + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeBstr(&protectedCtx, enc, sizeof(enc)); + } + if (decRet == WOLFCOSE_SUCCESS) { + protectedCase = protectedHdr; + protectedCaseLen = protectedCtx.idx; + } + } + + (void)XMEMSET(&ctx, 0, sizeof(ctx)); + ctx.buf = encoded; + ctx.bufSz = sizeof(encoded); + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeTag(&ctx, WOLFCOSE_TAG_ENCRYPT); + } + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeArrayStart(&ctx, 4u); + } + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeBstr(&ctx, &emptyHdr, 0u); + } + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeMapStart(&ctx, 2u); + } + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_HDR_ALG); + } + if (decRet == WOLFCOSE_SUCCESS) { + bodyAlgOffset = ctx.idx; + decRet = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_ALG_A128GCM); + } + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_HDR_IV); + } + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeBstr(&ctx, iv, sizeof(iv)); + } + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeBstr(&ctx, bodyCiphertext, + sizeof(bodyCiphertext)); + } + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeArrayStart(&ctx, 2u); + } + for (i = 0u; (decRet == WOLFCOSE_SUCCESS) && (i < 2u); i++) { + uint32_t flags = cases[caseIndex].flags; + const uint8_t* recipientProtected = &emptyHdr; + size_t recipientProtectedLen = 0u; + size_t mapEntries = 0u; + + if (i == 0u) { + flags &= HPKE_KE_HDR_CASE_SKIP_ZERO_ALG | + HPKE_KE_HDR_CASE_SKIP_TEXT_ALG; + } + else { + flags &= ~(uint32_t)(HPKE_KE_HDR_CASE_SKIP_ZERO_ALG | + HPKE_KE_HDR_CASE_SKIP_TEXT_ALG); + } + if (i == 1u) { + recipientProtected = protectedCase; + recipientProtectedLen = protectedCaseLen; + } + decRet = wc_CBOR_EncodeArrayStart(&ctx, 3u); + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeBstr(&ctx, recipientProtected, + recipientProtectedLen); + } + if ((flags & HPKE_KE_HDR_CASE_NO_EK) == 0u) { + mapEntries++; + } + if ((flags & HPKE_KE_HDR_CASE_UNPROTECTED_ALG) != 0u) { + mapEntries++; + } + if ((flags & (HPKE_KE_HDR_CASE_SKIP_ZERO_ALG | + HPKE_KE_HDR_CASE_SKIP_TEXT_ALG)) != 0u) { + mapEntries++; + } + if ((flags & HPKE_KE_HDR_CASE_DUPLICATE_EK) != 0u) { + mapEntries++; + } + if ((flags & HPKE_KE_HDR_CASE_PSK_ID) != 0u) { + mapEntries++; + } + if (decRet == WOLFCOSE_SUCCESS) { + decRet = wc_CBOR_EncodeMapStart(&ctx, mapEntries); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((flags & HPKE_KE_HDR_CASE_UNPROTECTED_ALG) != 0u)) { + decRet = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_HDR_ALG); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((flags & HPKE_KE_HDR_CASE_UNPROTECTED_ALG) != 0u)) { + decRet = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_ALG_HPKE_0_KE); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((flags & HPKE_KE_HDR_CASE_SKIP_ZERO_ALG) != 0u)) { + decRet = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_HDR_ALG); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((flags & HPKE_KE_HDR_CASE_SKIP_ZERO_ALG) != 0u)) { + decRet = wc_CBOR_EncodeInt(&ctx, 0); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((flags & HPKE_KE_HDR_CASE_SKIP_TEXT_ALG) != 0u)) { + decRet = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_HDR_ALG); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((flags & HPKE_KE_HDR_CASE_SKIP_TEXT_ALG) != 0u)) { + decRet = wc_CBOR_EncodeTstr(&ctx, invalidAlg, + sizeof(invalidAlg) - 1u); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((flags & HPKE_KE_HDR_CASE_NO_EK) == 0u)) { + decRet = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_HDR_HPKE_EK); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((flags & HPKE_KE_HDR_CASE_NO_EK) == 0u) && + ((flags & HPKE_KE_HDR_CASE_NON_BSTR_EK) != 0u)) { + decRet = wc_CBOR_EncodeTstr(&ctx, nonBstrEk, + sizeof(nonBstrEk) - 1u); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((flags & HPKE_KE_HDR_CASE_NO_EK) == 0u) && + ((flags & HPKE_KE_HDR_CASE_NON_BSTR_EK) == 0u)) { + size_t ekLen = sizeof(enc); + + if ((flags & HPKE_KE_HDR_CASE_SHORT_EK) != 0u) { + ekLen--; + } + decRet = wc_CBOR_EncodeBstr(&ctx, enc, ekLen); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((flags & HPKE_KE_HDR_CASE_DUPLICATE_EK) != 0u)) { + decRet = wc_CBOR_EncodeInt(&ctx, WOLFCOSE_HDR_HPKE_EK); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((flags & HPKE_KE_HDR_CASE_DUPLICATE_EK) != 0u)) { + decRet = wc_CBOR_EncodeBstr(&ctx, enc, sizeof(enc)); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((flags & HPKE_KE_HDR_CASE_PSK_ID) != 0u)) { + decRet = wc_CBOR_EncodeInt(&ctx, -5); + } + if ((decRet == WOLFCOSE_SUCCESS) && + ((flags & HPKE_KE_HDR_CASE_PSK_ID) != 0u)) { + decRet = wc_CBOR_EncodeBstr(&ctx, pskId, sizeof(pskId)); + } + if (decRet == WOLFCOSE_SUCCESS) { + if (i == 1u) { + wrappedCekLenOffset = ctx.idx; + } + decRet = wc_CBOR_EncodeBstr(&ctx, wrappedCek, + wrappedCekLen); + if ((decRet == WOLFCOSE_SUCCESS) && (i == 1u)) { + wrappedCekDataEnd = ctx.idx; + } + } + } + if (decRet == WOLFCOSE_SUCCESS) { + encodedLen = ctx.idx; + hpkeRecipient.algId = WOLFCOSE_ALG_HPKE_0_KE; + hpkeRecipient.key = &recipientKey; + hpkeRecipient.kid = NULL; + hpkeRecipient.kidLen = 0u; + (void)XMEMSET(&hdr, 0xa5, sizeof(hdr)); + (void)XMEMSET(plaintext, 0xa5, sizeof(plaintext)); + decRet = wc_CoseEncrypt_Decrypt(&hpkeRecipient, 1u, encoded, + encodedLen, NULL, 0u, NULL, 0u, scratch, sizeof(scratch), + &hdr, plaintext, sizeof(plaintext), &plaintextLen); + } + TEST_ASSERT(decRet == cases[caseIndex].expected, cases[caseIndex].name); + if (cases[caseIndex].expected == WOLFCOSE_SUCCESS) { + TEST_ASSERT(decRet == WOLFCOSE_SUCCESS, + "hpke ke optional recipient alg accepted"); + TEST_ASSERT((plaintextLen == (sizeof(payload) - 1u)) && + (memcmp(plaintext, payload, plaintextLen) == 0), + "hpke ke unprotected body alg payload"); + if (cases[caseIndex].flags == 0u) { + int tamperedRet = WOLFCOSE_SUCCESS; + size_t tamperedPlaintextLen = sizeof(plaintext); + const size_t expandedCekLen = wrappedCekLen + 8u; + + if ((bodyAlgOffset < encodedLen) && + ((wrappedCekLenOffset + 1u) < encodedLen) && + (wrappedCekDataEnd <= encodedLen) && + ((encodedLen + 8u) <= sizeof(encoded)) && + (encoded[bodyAlgOffset] == 0x01u) && + (encoded[wrappedCekLenOffset] == 0x58u) && + (encoded[wrappedCekLenOffset + 1u] == + (uint8_t)wrappedCekLen)) { + XMEMMOVE(&encoded[wrappedCekDataEnd + 8u], + &encoded[wrappedCekDataEnd], + encodedLen - wrappedCekDataEnd); + (void)XMEMSET(&encoded[wrappedCekDataEnd], 0, 8u); + encoded[bodyAlgOffset] = 0x02u; + encoded[wrappedCekLenOffset + 1u] = + (uint8_t)expandedCekLen; + encodedLen += 8u; + (void)XMEMSET(&hdr, 0xa5, sizeof(hdr)); + (void)XMEMSET(plaintext, 0xa5, sizeof(plaintext)); + tamperedRet = wc_CoseEncrypt_Decrypt(&hpkeRecipient, 1u, + encoded, encodedLen, NULL, 0u, NULL, 0u, scratch, + sizeof(scratch), &hdr, plaintext, sizeof(plaintext), + &tamperedPlaintextLen); + } + TEST_ASSERT(tamperedRet != WOLFCOSE_SUCCESS, + "hpke ke content alg tamper rejected"); + (void)XMEMSET(&clearedHdr, 0, sizeof(clearedHdr)); + (void)XMEMSET(clearedPlaintext, 0, sizeof(clearedPlaintext)); + TEST_ASSERT((tamperedPlaintextLen == 0u) && + (memcmp(&hdr, &clearedHdr, sizeof(hdr)) == 0) && + (memcmp(plaintext, clearedPlaintext, + sizeof(plaintext)) == 0), + "hpke ke content alg tamper clears outputs"); + } + } + else { + (void)XMEMSET(&clearedHdr, 0, sizeof(clearedHdr)); + (void)XMEMSET(clearedPlaintext, 0, sizeof(clearedPlaintext)); + TEST_ASSERT((plaintextLen == 0u) && + (memcmp(&hdr, &clearedHdr, sizeof(hdr)) == 0) && + (memcmp(plaintext, clearedPlaintext, + sizeof(plaintext)) == 0), + "hpke ke malformed header clears outputs"); + } + } + + if (recipientKeyInited != 0) { + wc_CoseKey_Free(&recipientKey); + } + if (ephemeralEccInited != 0) { + (void)wc_ecc_free(&ephemeralEcc); + } + if (recipientEccInited != 0) { + (void)wc_ecc_free(&recipientEcc); + } + if (rngInited != 0) { + (void)wc_FreeRng(&rng); + } + (void)XMEMSET(&hpke, 0, sizeof(hpke)); +} +#endif /* WOLFCOSE_HPKE_0_KE_ENCRYPT && WOLFCOSE_HPKE_0_KE_DECRYPT */ + #if defined(WOLFCOSE_HPKE_0_DECRYPT) /* draft-ietf-cose-hpke-26, Section 5.1, Figure 2 and Figure 4. */ static void test_cose_hpke_encrypt0_draft_vector(void) @@ -9769,7 +10769,7 @@ static void test_cose_hpke_encrypt0_draft_vector(void) (memcmp(plaintext, expected, plaintextLen) == 0), "hpke encrypt0 draft vector plaintext"); TEST_ASSERT((hdr.alg == WOLFCOSE_ALG_HPKE_0) && - (hdr.kidLen == 3u) && (hdr.hpkeEkLen == 65u), + (hdr.kidLen == 3u), "hpke encrypt0 draft vector headers"); } @@ -9809,11 +10809,13 @@ static void test_cose_hpke_key_encryption(void) uint8_t scratch[512]; size_t outLen = 0u; size_t plaintextLen = 0u; + size_t ciphertextLen = 0u; size_t i; int foundKid = 0; const uint8_t kid1[] = "hpke-recipient-1"; const uint8_t kid2[] = "hpke-recipient-2"; const uint8_t payload[] = "COSE HPKE key encryption"; + const uint8_t emptyPayload[] = { 0u }; const uint8_t iv[12] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c @@ -9961,6 +10963,37 @@ static void test_cose_hpke_key_encryption(void) out[i] ^= 0x01u; } } + if ((recipientKey1Inited != 0) && (recipientKey2Inited != 0)) { + int emptyRet; + + recipientKey1.hasPrivate = 0u; + recipientKey2.hasPrivate = 0u; + ret = wc_CoseEncrypt_Encrypt(recipients, 2, + WOLFCOSE_ALG_A128GCM, iv, sizeof(iv), + emptyPayload, 0u, NULL, 0u, aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), out, sizeof(out), &outLen, &rng); + TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "hpke ke empty encrypt"); + if (ret == WOLFCOSE_SUCCESS) { + emptyRet = test_cose_hpke_ciphertext_len(out, outLen, + WOLFCOSE_TAG_ENCRYPT, 4u, &ciphertextLen); + TEST_ASSERT((emptyRet == WOLFCOSE_SUCCESS) && + (ciphertextLen == 16u), + "hpke ke empty tag length"); + recipientKey1.hasPrivate = 1u; + recipientKey2.hasPrivate = 1u; + for (i = 0u; i < 2u; i++) { + (void)XMEMSET(&hdr, 0, sizeof(hdr)); + plaintextLen = sizeof(plaintext); + emptyRet = wc_CoseEncrypt_Decrypt(&recipients[i], i, + out, outLen, NULL, 0u, aad, sizeof(aad) - 1u, + scratch, sizeof(scratch), &hdr, + plaintext, sizeof(plaintext), &plaintextLen); + TEST_ASSERT((emptyRet == WOLFCOSE_SUCCESS) && + (plaintextLen == 0u), + "hpke ke empty recipient decrypt"); + } + } + } if (wrongKeyInited != 0) { wc_CoseKey_Free(&wrongKey); @@ -24246,6 +25279,7 @@ int test_cose(void) g_failures = 0; /* Internal helper tests */ + test_cose_hdr_abi_layout(); test_wolfcose_force_zero(); #if defined(WOLFCOSE_HAVE_ES256) && defined(WOLFCOSE_SIGN1_SIGN) test_cose_sign1_size_and_untagged(); @@ -24354,6 +25388,7 @@ int test_cose(void) #endif #if defined(WOLFCOSE_HPKE_0_ENCRYPT) && defined(WOLFCOSE_HPKE_0_DECRYPT) test_cose_hpke_encrypt0(); + test_cose_hpke_encrypt0_optional_alg(); #endif #if defined(WOLFCOSE_HPKE_0_DECRYPT) test_cose_hpke_encrypt0_draft_vector(); @@ -24496,6 +25531,7 @@ int test_cose(void) #if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) && \ defined(WOLFCOSE_HPKE_0_KE_DECRYPT) test_cose_hpke_key_encryption(); + test_cose_hpke_key_encryption_optional_alg(); #endif #if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) test_cose_hpke_key_encryption_draft_vector_rejected(); diff --git a/tools/wolfcose_tool.c b/tools/wolfcose_tool.c index a384dc8..1c606b4 100644 --- a/tools/wolfcose_tool.c +++ b/tools/wolfcose_tool.c @@ -38,6 +38,13 @@ * Exit codes: 0=success, 1=usage, 2=crypto failure, 3=I/O error. */ +/* Request the POSIX interfaces used by the optional HPKE key-output guard + * before any system header on POSIX hosts. */ +#if (defined(__unix__) || defined(__APPLE__) || defined(__MACH__) || \ + defined(__CYGWIN__)) && !defined(_XOPEN_SOURCE) + #define _XOPEN_SOURCE 700 +#endif + #ifdef HAVE_CONFIG_H #include <config.h> #endif @@ -47,6 +54,16 @@ #include <wolfssl/wolfcrypt/settings.h> #include <wolfcose/wolfcose.h> + +#if !defined(WOLFCOSE_TOOL_HAVE_POSIX_FS) + #if defined(__unix__) || defined(__APPLE__) || defined(__MACH__) || \ + defined(__CYGWIN__) + #define WOLFCOSE_TOOL_HAVE_POSIX_FS 1 + #else + #define WOLFCOSE_TOOL_HAVE_POSIX_FS 0 + #endif +#endif + #include <wolfssl/wolfcrypt/random.h> #if defined(WOLFCOSE_HAVE_ECDSA) || defined(WOLFCOSE_HAVE_HPKE_0) #include <wolfssl/wolfcrypt/ecc.h> @@ -67,8 +84,14 @@ #include <stdio.h> #include <stdlib.h> #include <string.h> -#if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) -#include <errno.h> +#if defined(WOLFCOSE_HAVE_HPKE_0) + #include <errno.h> + #if (WOLFCOSE_TOOL_HAVE_POSIX_FS == 1) + #include <fcntl.h> + #include <limits.h> + #include <sys/stat.h> + #include <unistd.h> + #endif #endif #ifndef WOLFCOSE_TOOL_MAX_MSG @@ -85,6 +108,17 @@ #endif #endif +#if defined(WOLFCOSE_HAVE_HPKE_0) && \ + (WOLFCOSE_TOOL_HAVE_POSIX_FS == 1) +/* PATH_MAX is optional in POSIX headers. This fallback matches the minimum + * practical path size supported by the command-line tool. */ + #ifndef PATH_MAX + #define WOLFCOSE_TOOL_PATH_MAX 4096u + #else + #define WOLFCOSE_TOOL_PATH_MAX PATH_MAX + #endif +#endif + /* Bounds the command-line multi-recipient HPKE helper without limiting the * library API. Integrators can reduce this for constrained tools. */ #if defined(WOLFCOSE_HAVE_HPKE_0) @@ -265,12 +299,13 @@ static int parse_alg(const char* name, int32_t* alg) return 0; } -/* Read entire file into buffer, return bytes read */ +/* Read an entire file into buffer, rejecting data beyond the caller's bound. */ static int read_file(const char* path, uint8_t* buf, size_t bufSz, size_t* outLen) { FILE* f; size_t n; + int extra = EOF; f = fopen(path, "rb"); if (f == NULL) { @@ -278,11 +313,19 @@ static int read_file(const char* path, uint8_t* buf, size_t bufSz, return EXIT_IO; } n = fread(buf, 1, bufSz, f); - if (n == 0 && ferror(f)) { + if (n == bufSz) { + extra = fgetc(f); + } + if (ferror(f)) { fclose(f); fprintf(stderr, "Read error: %s\n", path); return EXIT_IO; } + if (extra != EOF) { + fclose(f); + fprintf(stderr, "Input too large: %s\n", path); + return EXIT_IO; + } fclose(f); *outLen = n; return 0; @@ -308,6 +351,321 @@ static int write_file(const char* path, const uint8_t* buf, size_t len) } #if defined(WOLFCOSE_HAVE_HPKE_0) +#if (WOLFCOSE_TOOL_HAVE_POSIX_FS == 1) +/* Resolve an output path without allowing a final symlink. Existing paths + * resolve fully; a new leaf is made canonical by resolving its parent. This + * lets the key generator reject aliases before either destination is created. */ +static int tool_hpke_canonical_output_path(const char* path, + char* canonical, size_t canonicalSz) +{ + char parent[WOLFCOSE_TOOL_PATH_MAX]; + char resolvedParent[WOLFCOSE_TOOL_PATH_MAX]; + const char* leaf; + const char* slash; + size_t parentLen; + size_t resolvedParentLen; + size_t leafLen; + struct stat pathStat; + int ret = 0; + + if ((path == NULL) || (path[0] == '\0') || (canonical == NULL) || + (canonicalSz == 0u)) { + ret = -1; + } + else if ((lstat(path, &pathStat) == 0) && S_ISLNK(pathStat.st_mode)) { + fprintf(stderr, "Refusing HPKE key output symlink: %s\n", path); + ret = -1; + } + else if (realpath(path, canonical) != NULL) { + /* Existing non-symlink path resolved successfully. */ + } + else { + slash = strrchr(path, '/'); + if (slash == NULL) { + parent[0] = '.'; + parent[1] = '\0'; + leaf = path; + } + else if (slash == path) { + parent[0] = '/'; + parent[1] = '\0'; + leaf = slash + 1; + } + else { + parentLen = (size_t)(slash - path); + if (parentLen >= sizeof(parent)) { + ret = -1; + } + else { + (void)XMEMCPY(parent, path, parentLen); + parent[parentLen] = '\0'; + leaf = slash + 1; + } + } + + if ((ret == 0) && (leaf[0] == '\0')) { + ret = -1; + } + if ((ret == 0) && (realpath(parent, resolvedParent) == NULL)) { + ret = -1; + } + if (ret == 0) { + resolvedParentLen = strlen(resolvedParent); + leafLen = strlen(leaf); + if ((resolvedParentLen > (SIZE_MAX - leafLen - 2u)) || + ((resolvedParentLen + leafLen + 2u) > canonicalSz)) { + ret = -1; + } + else if ((resolvedParentLen == 1u) && + (resolvedParent[0] == '/')) { + (void)XMEMCPY(canonical, resolvedParent, + resolvedParentLen); + (void)XMEMCPY(&canonical[resolvedParentLen], leaf, leafLen); + canonical[resolvedParentLen + leafLen] = '\0'; + } + else { + (void)XMEMCPY(canonical, resolvedParent, + resolvedParentLen); + canonical[resolvedParentLen] = '/'; + (void)XMEMCPY(&canonical[resolvedParentLen + 1u], leaf, + leafLen); + canonical[resolvedParentLen + leafLen + 1u] = '\0'; + } + } + } + + if (ret != 0) { + fprintf(stderr, "Invalid HPKE key output path: %s\n", path); + } + return ret; +} + +/* Return one when paths name distinct, new outputs, zero when they collide, + * and negative when either path cannot be resolved safely. For two absent + * leaves, create a temporary zero-length reservation and ask the filesystem + * whether the second spelling resolves to it. */ +static int tool_hpke_key_paths_distinct(const char* privatePath, + const char* publicPath) +{ + char privateCanonical[WOLFCOSE_TOOL_PATH_MAX]; + char publicCanonical[WOLFCOSE_TOOL_PATH_MAX]; + struct stat privateStat; + struct stat publicStat; + int privateExists; + int publicExists; + int reservationFd = -1; + int ret; + + ret = tool_hpke_canonical_output_path(privatePath, privateCanonical, + sizeof(privateCanonical)); + if ((ret == 0) && (publicPath != NULL)) { + ret = tool_hpke_canonical_output_path(publicPath, publicCanonical, + sizeof(publicCanonical)); + } + if (ret != 0) { + return -1; + } + if ((publicPath != NULL) && + (strcmp(privateCanonical, publicCanonical) == 0)) { + return 0; + } + privateExists = (stat(privateCanonical, &privateStat) == 0) ? 1 : 0; + if ((privateExists == 0) && (errno != ENOENT)) { + return -1; + } + if (privateExists != 0) { + return -1; + } + if (publicPath == NULL) { + return 1; + } + publicExists = (stat(publicCanonical, &publicStat) == 0) ? 1 : 0; + if ((publicExists == 0) && (errno != ENOENT)) { + return -1; + } + if (publicExists != 0) { + return -1; + } + + reservationFd = open(privateCanonical, O_WRONLY | O_CREAT | O_EXCL, 0600); + if (reservationFd < 0) { + return -1; + } + if (close(reservationFd) != 0) { + (void)unlink(privateCanonical); + return -1; + } + if (stat(publicCanonical, &publicStat) == 0) { + ret = ((stat(privateCanonical, &privateStat) == 0) && + (privateStat.st_dev == publicStat.st_dev) && + (privateStat.st_ino == publicStat.st_ino)) ? 0 : -1; + } + else if (errno == ENOENT) { + ret = 1; + } + else { + ret = -1; + } + if (unlink(privateCanonical) != 0) { + ret = -1; + } + if (ret == 0) { + return 0; + } + if (ret < 0) { + return -1; + } + return 1; +} + +static int tool_hpke_write_all(int fd, const uint8_t* buf, size_t len) +{ + size_t offset = 0u; + + while (offset < len) { + ssize_t written = write(fd, &buf[offset], len - offset); + + if (written > 0) { + offset += (size_t)written; + } + else if ((written < 0) && (errno == EINTR)) { + continue; + } + else { + return -1; + } + } + return 0; +} + +/* Stage a key in the target directory so a link creates only a complete file. */ +static int tool_hpke_stage_key(const char* outputPath, const uint8_t* buf, + size_t len, char* temporaryPath, size_t temporaryPathSz) +{ + static const char suffix[] = ".wolfcose-tmp.XXXXXX"; + size_t outputPathLen; + int fd; + int ret; + + if ((outputPath == NULL) || (temporaryPath == NULL) || + (temporaryPathSz < sizeof(suffix))) { + return -1; + } + temporaryPath[0] = '\0'; + outputPathLen = strlen(outputPath); + if (outputPathLen > (temporaryPathSz - sizeof(suffix))) { + return -1; + } + (void)XMEMCPY(temporaryPath, outputPath, outputPathLen); + (void)XMEMCPY(&temporaryPath[outputPathLen], suffix, sizeof(suffix)); + + fd = mkstemp(temporaryPath); + if (fd < 0) { + temporaryPath[0] = '\0'; + return -1; + } + ret = tool_hpke_write_all(fd, buf, len); + if ((ret == 0) && (fsync(fd) != 0)) { + ret = -1; + } + if (close(fd) != 0) { + ret = -1; + } + if (ret != 0) { + (void)unlink(temporaryPath); + temporaryPath[0] = '\0'; + } + return ret; +} + +/* Publish complete new key files without truncating any existing destination. */ +static int tool_hpke_write_key_pair(const char* privatePath, + const uint8_t* privateBuf, size_t privateLen, const char* publicPath, + const uint8_t* publicBuf, size_t publicLen) +{ + char privateCanonical[WOLFCOSE_TOOL_PATH_MAX]; + char publicCanonical[WOLFCOSE_TOOL_PATH_MAX]; + char privateTemporary[WOLFCOSE_TOOL_PATH_MAX]; + char publicTemporary[WOLFCOSE_TOOL_PATH_MAX]; + int privateInstalled = 0; + int publicInstalled = 0; + int ret = EXIT_IO; + + privateTemporary[0] = '\0'; + publicTemporary[0] = '\0'; + if (tool_hpke_canonical_output_path(privatePath, privateCanonical, + sizeof(privateCanonical)) != 0) { + goto exit; + } + if ((publicPath != NULL) && + (tool_hpke_canonical_output_path(publicPath, publicCanonical, + sizeof(publicCanonical)) != 0)) { + goto exit; + } + if (tool_hpke_stage_key(privateCanonical, privateBuf, privateLen, + privateTemporary, sizeof(privateTemporary)) != 0) { + goto exit; + } + if ((publicPath != NULL) && + (tool_hpke_stage_key(publicCanonical, publicBuf, publicLen, + publicTemporary, sizeof(publicTemporary)) != 0)) { + goto exit; + } + if (link(privateTemporary, privateCanonical) != 0) { + goto exit; + } + privateInstalled = 1; + if ((publicPath != NULL) && + (link(publicTemporary, publicCanonical) != 0)) { + goto exit; + } + if (publicPath != NULL) { + publicInstalled = 1; + } + ret = 0; + +exit: + if ((ret != 0) && (publicInstalled != 0)) { + (void)unlink(publicCanonical); + } + if ((ret != 0) && (privateInstalled != 0)) { + (void)unlink(privateCanonical); + } + if (privateTemporary[0] != '\0') { + (void)unlink(privateTemporary); + } + if (publicTemporary[0] != '\0') { + (void)unlink(publicTemporary); + } + return ret; +} +#else +static int tool_hpke_key_paths_distinct(const char* privatePath, + const char* publicPath) +{ + if (privatePath == NULL) { + return -1; + } + if ((publicPath != NULL) && (strcmp(privatePath, publicPath) == 0)) { + return 0; + } + return 1; +} + +static int tool_hpke_write_key_pair(const char* privatePath, + const uint8_t* privateBuf, size_t privateLen, const char* publicPath, + const uint8_t* publicBuf, size_t publicLen) +{ + (void)publicBuf; + (void)publicLen; + if (publicPath != NULL) { + fprintf(stderr, "HPKE public key export requires POSIX filesystem support\n"); + return EXIT_USAGE; + } + return write_file(privatePath, privateBuf, privateLen); +} +#endif /* WOLFCOSE_TOOL_HAVE_POSIX_FS */ + /* Generate a P-256 HPKE key pair and optionally export its public half. */ static int tool_hpke_keygen(int32_t alg, const char* outPath, const char* publicPath) @@ -323,6 +681,27 @@ static int tool_hpke_keygen(int32_t alg, const char* outPath, int eccInit = 0; int ret; + if (outPath == NULL) { + fprintf(stderr, "HPKE private and public key paths must differ\n"); + return EXIT_USAGE; + } +#if (WOLFCOSE_TOOL_HAVE_POSIX_FS == 0) + if (publicPath != NULL) { + fprintf(stderr, "HPKE public key export requires POSIX filesystem support\n"); + return EXIT_USAGE; + } +#endif + + ret = tool_hpke_key_paths_distinct(outPath, publicPath); + if (ret <= 0) { + if (ret == 0) { + fprintf(stderr, "HPKE private and public key paths must differ\n"); + return EXIT_USAGE; + } + fprintf(stderr, "HPKE key output paths must be new and non-symlinked\n"); + return EXIT_IO; + } + ret = wc_InitRng(&rng); if (ret == 0) { rngInit = 1; @@ -359,10 +738,8 @@ static int tool_hpke_keygen(int32_t alg, const char* outPath, return EXIT_CRYPTO; } - ret = write_file(outPath, privateBuf, privateLen); - if ((ret == 0) && (publicPath != NULL)) { - ret = write_file(publicPath, publicBuf, publicLen); - } + ret = tool_hpke_write_key_pair(outPath, privateBuf, privateLen, + publicPath, publicBuf, publicLen); tool_force_zero(privateBuf, sizeof(privateBuf)); tool_force_zero(publicBuf, sizeof(publicBuf)); if (ret == 0) { From fe31134652c1796a58d4097e275fe458c874ac31 Mon Sep 17 00:00:00 2001 From: Aidan Garske <aidan@wolfssl.com> Date: Wed, 2 Sep 2026 10:23:34 -0700 Subject: [PATCH 05/11] Gate COSE-HPKE under experimental settings --- .github/workflows/cose-hpke.yml | 6 +- Makefile | 32 ++++++---- docs/Experimental.md | 102 ++++++++++++++++++++++++++++++++ docs/Getting-Started.md | 10 +++- docs/Home.md | 1 + docs/Macros.md | 21 ++++--- docs/Testing.md | 9 ++- docs/_Sidebar.md | 1 + examples/hpke_demo.c | 7 ++- include/wolfcose/settings.h | 10 ++++ 10 files changed, 170 insertions(+), 29 deletions(-) create mode 100644 docs/Experimental.md diff --git a/.github/workflows/cose-hpke.yml b/.github/workflows/cose-hpke.yml index 5aea188..eaa8ad7 100644 --- a/.github/workflows/cose-hpke.yml +++ b/.github/workflows/cose-hpke.yml @@ -45,7 +45,8 @@ jobs: run: | export WOLFSSL_DIR="$HOME/wolfssl-install" export LD_LIBRARY_PATH="$WOLFSSL_DIR/lib" - HPKE_FLAGS="-DWOLFCOSE_ENABLE_HPKE_0_ENCRYPT \ + HPKE_FLAGS="-DWOLFCOSE_EXPERIMENTAL \ + -DWOLFCOSE_ENABLE_HPKE_0_ENCRYPT \ -DWOLFCOSE_ENABLE_HPKE_0_DECRYPT \ -DWOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT \ -DWOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT" @@ -77,7 +78,8 @@ jobs: run: | export WOLFSSL_DIR="$HOME/wolfssl-install" export LD_LIBRARY_PATH="$WOLFSSL_DIR/lib" - HPKE_FLAGS="-DWOLFCOSE_ENABLE_HPKE_0_ENCRYPT \ + HPKE_FLAGS="-DWOLFCOSE_EXPERIMENTAL \ + -DWOLFCOSE_ENABLE_HPKE_0_ENCRYPT \ -DWOLFCOSE_ENABLE_HPKE_0_DECRYPT \ -DWOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT \ -DWOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT" diff --git a/Makefile b/Makefile index 7238da7..cab21ed 100644 --- a/Makefile +++ b/Makefile @@ -554,6 +554,14 @@ EXP_FLAGS = -std=c99 -pedantic-errors -I./include $(C99_WOLFSSL_CFLAGS) \ # Include settings.h plus one declaration so the stub is a valid C99 TU. EXP_TU = printf '\#include <wolfcose/settings.h>\nint wolfcose_experimental_gate_check;\n' +EXP_HPKE_ENABLES = \ + WOLFCOSE_ENABLE_HPKE_0 \ + WOLFCOSE_ENABLE_HPKE_0_KE \ + WOLFCOSE_ENABLE_HPKE_0_ENCRYPT \ + WOLFCOSE_ENABLE_HPKE_0_DECRYPT \ + WOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT \ + WOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT + experimental-check: @echo " EXP feature without acknowledgement (expect error)" @if $(EXP_TU) | \ @@ -570,17 +578,19 @@ experimental-check: @$(EXP_TU) | \ $(CC) $(EXP_FLAGS) -DWOLFCOSE_ENABLE_EXPERIMENTAL_EXAMPLE \ -DWOLFCOSE_EXPERIMENTAL -fsyntax-only -x c - - @echo " EXP COSE-HPKE without acknowledgement (expect error)" - @if $(EXP_TU) | \ - $(CC) $(EXP_FLAGS) -DWOLFCOSE_ENABLE_HPKE_0_ENCRYPT \ - -fsyntax-only -x c - 2>experimental-check.err; then \ - echo "FAIL: COSE-HPKE compiled without WOLFCOSE_EXPERIMENTAL"; \ - rm -f experimental-check.err; exit 1; \ - fi - @grep -q WOLFCOSE_EXPERIMENTAL experimental-check.err || { \ - echo "FAIL: COSE-HPKE gate error did not mention WOLFCOSE_EXPERIMENTAL"; \ - cat experimental-check.err; rm -f experimental-check.err; exit 1; } - @rm -f experimental-check.err + @for enable in $(EXP_HPKE_ENABLES); do \ + echo " EXP COSE-HPKE $$enable without acknowledgement (expect error)"; \ + if $(EXP_TU) | $(CC) $(EXP_FLAGS) -D$$enable \ + -fsyntax-only -x c - 2>experimental-check.err; then \ + echo "FAIL: COSE-HPKE $$enable compiled without WOLFCOSE_EXPERIMENTAL"; \ + rm -f experimental-check.err; exit 1; \ + fi; \ + if ! grep -q WOLFCOSE_EXPERIMENTAL experimental-check.err; then \ + echo "FAIL: COSE-HPKE $$enable gate error did not mention WOLFCOSE_EXPERIMENTAL"; \ + cat experimental-check.err; rm -f experimental-check.err; exit 1; \ + fi; \ + rm -f experimental-check.err; \ + done @echo " EXP normal build (expect pass, zero experimental code)" @$(EXP_TU) | \ $(CC) $(EXP_FLAGS) -fsyntax-only -x c - diff --git a/docs/Experimental.md b/docs/Experimental.md new file mode 100644 index 0000000..e5cf0e4 --- /dev/null +++ b/docs/Experimental.md @@ -0,0 +1,102 @@ +# Experimental Features + +This page tracks wolfCOSE features that intentionally remain outside the stable +default build while their standards are still in development. + +> **Status snapshot:** 2 September 2026. Update this page when the IETF draft, +> its IANA registrations, or wolfCOSE's implementation status changes. + +## Customer Summary + +Experimental code is disabled by default. Enabling a feature requires both the +master acknowledgement, `WOLFCOSE_EXPERIMENTAL`, and the feature's individual +`WOLFCOSE_ENABLE_*` macro. This makes draft adoption an explicit build-time +decision and keeps ordinary deployments free of draft wire formats and APIs. + +Experimental features may change or be removed before their associated RFC is +published. Customers who evaluate one should pin the wolfCOSE revision used for +testing and should not promise long-term wire compatibility until the feature +graduates. + +## COSE-HPKE + +### Standards Status + +| Item | Current status | +|------|----------------| +| Specification | [draft-ietf-cose-hpke-26](https://datatracker.ietf.org/doc/draft-ietf-cose-hpke/26/) | +| IETF state | Active COSE working-group Internet-Draft | +| Intended RFC status | Proposed Standard | +| Published | 4 July 2026 | +| Draft expiration | 5 January 2027 | +| Stable RFC | Not published | + +Draft expiration is not an RFC publication estimate. The IETF may revise, +replace, or advance the document at any time. Track the +[current Datatracker record](https://datatracker.ietf.org/doc/draft-ietf-cose-hpke/) +for the authoritative state. + +### wolfCOSE Status + +| Area | Status | Scope | +|------|--------|-------| +| Master acknowledgement | Complete | `WOLFCOSE_EXPERIMENTAL` is required but enables no code by itself. | +| Fine-grained build controls | Complete | Separate send and receive macros exist for integrated HPKE-0 and multi-recipient HPKE-0-KE. | +| Integrated encryption | Complete for P0 | `COSE_Encrypt0` HPKE-0 encrypt and decrypt. | +| Multi-recipient encryption | Complete for P0 | `COSE_Encrypt` HPKE-0-KE encrypt and decrypt. | +| Command-line and example coverage | Complete for P0 | HPKE commands, self-tests, and `examples/hpke_demo.c` use the same experimental gate. | +| Continuous integration | Complete for P0 | Unit, example, CLI, strict C99, malformed-input, and curve-validation coverage run in the experimental HPKE workflow. | +| Stable wire and API commitment | Pending | Deferred until the final RFC and IANA values are published. | + +### Implemented P0 Scope + +The current implementation is deliberately narrow and auditable: + +- HPKE base mode only. +- DHKEM(P-256, HKDF-SHA256), HKDF-SHA256, and AES-128-GCM only. +- One-recipient integrated `COSE_Encrypt0` HPKE-0. +- Multi-recipient `COSE_Encrypt` HPKE-0-KE with an independently protected CEK + for each recipient. +- External AAD bound through the normal COSE encryption structure. + +The following are not implemented in P0: alternate KEM, KDF, or AEAD suites; +X25519; PSK mode; authenticated HPKE mode; and an application-controlled HPKE +`info` value. These omissions are intentional, not hidden runtime options. + +### Build Selection + +Build wolfSSL with HPKE, ECC P-256, AES-GCM, SHA-256, and key generation, then +select only the wolfCOSE directions the product needs: + +```bash +# Integrated Encrypt0, send and receive. +make EXTRA_CFLAGS="-DWOLFCOSE_EXPERIMENTAL -DWOLFCOSE_ENABLE_HPKE_0" + +# Receive-only integrated provisioning target. +make EXTRA_CFLAGS="-DWOLFCOSE_EXPERIMENTAL \ + -DWOLFCOSE_ENABLE_HPKE_0_DECRYPT" + +# Multi-recipient sender in a lean build. +make EXTRA_CFLAGS="-DWOLFCOSE_LEAN -DWOLFCOSE_EXPERIMENTAL \ + -DWOLFCOSE_ENABLE_ENCRYPT -DWOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT" +``` + +The `make hpke-demo` target supplies the acknowledgement and all four P0 +operation macros itself. See [Configuration Macros](Macros.md#cose-hpke-experimental) +for every enable and compile-out macro. + +### Path to Stable Support + +| Milestone | Status | Customer planning guidance | +|-----------|--------|----------------------------| +| New draft revisions | Ongoing | wolfCOSE will assess each revision for wire, identifier, and API changes. | +| Final RFC and IANA registrations | Pending IETF publication | No public RFC publication date is available. Do not use the draft expiration as a delivery date. | +| wolfCOSE compatibility update | Pending final RFC | Planned after the final RFC and registrations are available. | +| Graduation from experimental | Pending validation | The target is a compatible wolfCOSE release after final-RFC validation. The release date cannot be committed before the IETF publication date is known. | + +When COSE-HPKE graduates, wolfCOSE will publish migration notes, update the +implemented identifiers and wire handling as needed, remove the +`WOLFCOSE_EXPERIMENTAL` requirement, and document the stable API and +compatibility policy. Until then, use a COSE signature or MAC with HPKE when +the sender must be authenticated, and treat HPKE-encoded artifacts as +experimental interoperability data. diff --git a/docs/Getting-Started.md b/docs/Getting-Started.md index 6f22b3b..a826532 100644 --- a/docs/Getting-Started.md +++ b/docs/Getting-Started.md @@ -250,10 +250,13 @@ COSE-HPKE tracks an active Internet-Draft, so it is disabled in every build, including a normal non-lean build. It currently implements the P0 subset: HPKE base mode with DHKEM(P-256, HKDF-SHA256), HKDF-SHA256, and AES-128-GCM. See [Configuration Macros](Macros.md#cose-hpke-experimental) for the complete -operation and compile-out gates. +operation and compile-out gates, and [Experimental Features](Experimental.md) +for the draft status and graduation plan. Build wolfSSL with HPKE support, then enable the exact send and receive paths -your application needs. The standalone example enables all four paths and +your application needs. `WOLFCOSE_EXPERIMENTAL` is required with every HPKE +enable macro. The standalone example supplies that acknowledgement, enables all +four paths, and demonstrates both one-recipient `COSE_Encrypt0` and two-recipient `COSE_Encrypt` key encryption: @@ -280,7 +283,8 @@ for HPKE-0, while the `hpke-ke-*` commands use one independently HPKE-protected CEK for every recipient: ```bash -# Build the tool with the four WOLFCOSE_ENABLE_HPKE_0_* operation macros. +# Build the tool with WOLFCOSE_EXPERIMENTAL and the four +# WOLFCOSE_ENABLE_HPKE_0_* operation macros. ./tools/wolfcose_tool keygen -a HPKE-0 \ -o recipient.private.cbor -p recipient.public.cbor ./tools/wolfcose_tool hpke0-enc -k recipient.public.cbor \ diff --git a/docs/Home.md b/docs/Home.md index 4a58bc8..87049ad 100644 --- a/docs/Home.md +++ b/docs/Home.md @@ -33,6 +33,7 @@ It uses [wolfSSL](https://www.wolfssl.com/) as the cryptographic backend and is | [[Algorithms]] | Complete list of supported algorithms with COSE IDs | | [[API Reference]] | Full API documentation for all functions | | [[Macros]] | Configuration macros and compile-time options | +| [[Experimental]] | Draft feature status, supported scope, and graduation plan | | [[Footprint]] | Size and speed numbers, desktop and on-device | | [[Testing]] | Unit tests, coverage, and failure injection | | [[Project Structure]] | Source code layout and file descriptions | diff --git a/docs/Macros.md b/docs/Macros.md index b42943b..74bd7f3 100644 --- a/docs/Macros.md +++ b/docs/Macros.md @@ -2,7 +2,8 @@ wolfCOSE has two configuration modes. The default is an opt-out full build: every algorithm wolfSSL provides is enabled, and you strip what you don't need with `WOLFCOSE_NO_*` defines. Alternatively, `WOLFCOSE_LEAN` switches to an opt-in core build and you add extensions with `WOLFCOSE_ENABLE_*`. See [Lean Configuration Layer](#lean-configuration-layer). -Draft, pre-RFC features are held behind a separate acknowledgement, `WOLFCOSE_EXPERIMENTAL`; see [Experimental Features](#experimental-features). +Draft, pre-RFC features are held behind a separate acknowledgement, +`WOLFCOSE_EXPERIMENTAL`; see [Experimental Features](Experimental.md). ## Experimental Features @@ -39,8 +40,9 @@ cc -DWOLFCOSE_ENABLE_EXPERIMENTAL_EXAMPLE ... **Graduation policy.** When a draft is published as an RFC, its `WOLFCOSE_EXPERIMENTAL` requirement is removed in a focused follow-up and the feature becomes an ordinary gate (default-on full build, `WOLFCOSE_ENABLE_<X>` under `WOLFCOSE_LEAN`, `WOLFCOSE_NO_<X>` to strip), following the [Algorithm Gates](#algorithm-gates) convention. COSE-HPKE is currently an experimental feature. It stays off in every build and -requires the master acknowledgement plus the relevant operation gate. Its -specific gates are documented in [COSE-HPKE (experimental)](#cose-hpke-experimental). +requires the master acknowledgement plus the relevant operation gate. See +[Experimental Features](Experimental.md) for its current draft status, scope, +and graduation plan. ## Lean Configuration Layer @@ -242,6 +244,7 @@ with a COSE signature or MAC when the sender must be authenticated. | Define | Description | Default | |--------|-------------|---------| +| `WOLFCOSE_EXPERIMENTAL` | Required acknowledgement for every COSE-HPKE enable; alone enables no HPKE code | off | | `WOLFCOSE_ENABLE_HPKE_0` | Enable both send and receive for single-recipient integrated `COSE_Encrypt0` HPKE-0 | off | | `WOLFCOSE_ENABLE_HPKE_0_ENCRYPT` | Enable only integrated Encrypt0 send | off | | `WOLFCOSE_ENABLE_HPKE_0_DECRYPT` | Enable only integrated Encrypt0 receive | off | @@ -252,8 +255,10 @@ with a COSE signature or MAC when the sender must be authenticated. | `WOLFCOSE_NO_HPKE_0_ENCRYPT` / `WOLFCOSE_NO_HPKE_0_DECRYPT` | Compile out the corresponding integrated Encrypt0 direction | off | | `WOLFCOSE_NO_HPKE_0_KE_ENCRYPT` / `WOLFCOSE_NO_HPKE_0_KE_DECRYPT` | Compile out the corresponding multi-recipient key-encryption direction | off | -The convenience gates respect their per-direction `NO_*` gates. Defining an -individual `ENABLE_*` and its matching `NO_*` is a compile error; defining +Every HPKE `ENABLE_*` macro requires `WOLFCOSE_EXPERIMENTAL`; selecting an HPKE +operation without the acknowledgement is a compile error. The convenience +gates respect their per-direction `NO_*` gates. Defining an individual +`ENABLE_*` and its matching `NO_*` is a compile error; defining `WOLFCOSE_NO_HPKE_0` with any enable is also a compile error. An enabled HPKE operation requires wolfSSL `HAVE_HPKE`, `HAVE_ECC` with P-256, @@ -300,13 +305,13 @@ mode is not implemented. ```bash # Single-recipient integrated HPKE: send and receive. -make EXTRA_CFLAGS="-DWOLFCOSE_ENABLE_HPKE_0" +make EXTRA_CFLAGS="-DWOLFCOSE_EXPERIMENTAL -DWOLFCOSE_ENABLE_HPKE_0" # Receive-only provisioning target: no HPKE sender code. -make EXTRA_CFLAGS="-DWOLFCOSE_ENABLE_HPKE_0_DECRYPT" +make EXTRA_CFLAGS="-DWOLFCOSE_EXPERIMENTAL -DWOLFCOSE_ENABLE_HPKE_0_DECRYPT" # Multi-recipient provisioning server, including the lean COSE_Encrypt gate. -make EXTRA_CFLAGS="-DWOLFCOSE_LEAN -DWOLFCOSE_ENABLE_ENCRYPT \ +make EXTRA_CFLAGS="-DWOLFCOSE_LEAN -DWOLFCOSE_EXPERIMENTAL -DWOLFCOSE_ENABLE_ENCRYPT \ -DWOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT" ``` diff --git a/docs/Testing.md b/docs/Testing.md index 7818e09..f20cb4e 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -28,10 +28,12 @@ Round-trip self-tests for all 17 supported CLI algorithms. Each algorithm is tes COSE-HPKE P0 is off by default and therefore has a dedicated opt-in test path. Build against a wolfSSL configured with `--enable-hpke --enable-ecc ---enable-aesgcm --enable-keygen`, then enable the four operation macros: +--enable-aesgcm --enable-keygen`, then acknowledge experimental draft support +and enable the four operation macros: ```bash -HPKE_FLAGS="-DWOLFCOSE_ENABLE_HPKE_0_ENCRYPT \ +HPKE_FLAGS="-DWOLFCOSE_EXPERIMENTAL \ + -DWOLFCOSE_ENABLE_HPKE_0_ENCRYPT \ -DWOLFCOSE_ENABLE_HPKE_0_DECRYPT \ -DWOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT \ -DWOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT" @@ -54,6 +56,9 @@ focused `test -a` round trip for each construction. The dedicated strict C99 target compiles every HPKE-gated library, test, tool, and example path under each one-way operation gate, both convenience gates, the complete P0 configuration, and wolfSSL's `NO_ECC256` plus `HAVE_ALL_CURVES` configuration. +Every enabled configuration also supplies `WOLFCOSE_EXPERIMENTAL`; the regular +`make experimental-check` target verifies that an HPKE operation selected +without that acknowledgement is rejected at compile time. The unit suite validates draft messages that omit the optional HPKE `alg` (including a second selected recipient), rejects an HPKE `alg` in an unprotected header, accepts an unprotected HPKE-0-KE content algorithm only diff --git a/docs/_Sidebar.md b/docs/_Sidebar.md index 6683ebe..6979fb2 100644 --- a/docs/_Sidebar.md +++ b/docs/_Sidebar.md @@ -6,6 +6,7 @@ - [[Algorithms]] - [[API Reference]] - [[Macros]] +- [[Experimental]] - [[Footprint]] - [[Testing]] - [[MISRA Compliance]] diff --git a/examples/hpke_demo.c b/examples/hpke_demo.c index ad03d9f..fcc5a3d 100644 --- a/examples/hpke_demo.c +++ b/examples/hpke_demo.c @@ -20,8 +20,8 @@ /* Experimental COSE-HPKE P0 demonstration. * - * The draft binding is intentionally opt-in. This program demonstrates the - * two currently supported P0 constructions: + * The draft binding requires WOLFCOSE_EXPERIMENTAL plus its operation gates. + * This program demonstrates the two currently supported P0 constructions: * - HPKE-0: one-recipient COSE_Encrypt0 integrated encryption * - HPKE-0-KE: COSE_Encrypt with one HPKE-protected CEK per recipient */ @@ -213,7 +213,8 @@ int main(void) int main(void) { fprintf(stderr, - "This example requires the four WOLFCOSE_ENABLE_HPKE_0_* macros.\n"); + "This example requires WOLFCOSE_EXPERIMENTAL and the four " + "WOLFCOSE_ENABLE_HPKE_0_* macros.\n"); return 1; } diff --git a/include/wolfcose/settings.h b/include/wolfcose/settings.h index 2572fff..8a6b943 100644 --- a/include/wolfcose/settings.h +++ b/include/wolfcose/settings.h @@ -502,6 +502,16 @@ extern "C" { * other direction. The two convenience switches enable both directions for * their respective COSE construction. */ +#if (defined(WOLFCOSE_ENABLE_HPKE_0) || \ + defined(WOLFCOSE_ENABLE_HPKE_0_KE) || \ + defined(WOLFCOSE_ENABLE_HPKE_0_ENCRYPT) || \ + defined(WOLFCOSE_ENABLE_HPKE_0_DECRYPT) || \ + defined(WOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT) || \ + defined(WOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT)) && \ + !defined(WOLFCOSE_EXPERIMENTAL) + #error "COSE-HPKE selects experimental draft code (spec not yet a finalized RFC); also define WOLFCOSE_EXPERIMENTAL to acknowledge" +#endif + #if defined(WOLFCOSE_ENABLE_HPKE_0) #if defined(WOLFCOSE_NO_HPKE_0) #error "WOLFCOSE_ENABLE_HPKE_0 conflicts with WOLFCOSE_NO_HPKE_0" From 50808413d0a4213911849d755b49d82b22688dcf Mon Sep 17 00:00:00 2001 From: Aidan Garske <aidan@wolfssl.com> Date: Wed, 2 Sep 2026 11:15:10 -0700 Subject: [PATCH 06/11] Fix HPKE C99 gate configuration --- tools/wolfcose_tool.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tools/wolfcose_tool.c b/tools/wolfcose_tool.c index 1c606b4..f4b8fb9 100644 --- a/tools/wolfcose_tool.c +++ b/tools/wolfcose_tool.c @@ -2625,8 +2625,10 @@ int main(int argc, char* argv[]) #endif const char* inPath = NULL; const char* outPath = NULL; -#if defined(WOLFCOSE_HAVE_HPKE_0) +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) const char* keyPaths[WOLFCOSE_TOOL_MAX_HPKE_RECIPIENTS]; +#endif +#if defined(WOLFCOSE_HAVE_HPKE_0) size_t keyPathCount = 0u; #endif #if defined(WOLFCOSE_HPKE_0_KE_DECRYPT) @@ -2658,7 +2660,10 @@ int main(int argc, char* argv[]) fprintf(stderr, "Too many -k recipient keys\n"); return EXIT_USAGE; } - keyPaths[keyPathCount++] = keyPath; +#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) + keyPaths[keyPathCount] = keyPath; +#endif + keyPathCount++; #endif } #if defined(WOLFCOSE_HAVE_HPKE_0) From 53befd7743e2f8ace2a9eec26a959ef722d709b8 Mon Sep 17 00:00:00 2001 From: Aidan Garske <aidan@wolfssl.com> Date: Wed, 2 Sep 2026 11:49:00 -0700 Subject: [PATCH 07/11] Fix HPKE key generation path diagnostic --- tools/wolfcose_tool.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/wolfcose_tool.c b/tools/wolfcose_tool.c index f4b8fb9..f17253a 100644 --- a/tools/wolfcose_tool.c +++ b/tools/wolfcose_tool.c @@ -682,7 +682,7 @@ static int tool_hpke_keygen(int32_t alg, const char* outPath, int ret; if (outPath == NULL) { - fprintf(stderr, "HPKE private and public key paths must differ\n"); + fprintf(stderr, "HPKE key generation requires a private output path\n"); return EXIT_USAGE; } #if (WOLFCOSE_TOOL_HAVE_POSIX_FS == 0) From cee03aabf7a184a8189ed11975b349d767c0727e Mon Sep 17 00:00:00 2001 From: Aidan Garske <aidan@wolfssl.com> Date: Wed, 2 Sep 2026 11:49:10 -0700 Subject: [PATCH 08/11] Compile out disabled HPKE parser state --- src/wolfcose.c | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/wolfcose.c b/src/wolfcose.c index 386d51e..d81f26d 100644 --- a/src/wolfcose.c +++ b/src/wolfcose.c @@ -1371,6 +1371,7 @@ int wolfCose_DecodeProtectedHdr(const uint8_t* data, size_t dataLen, return ret; } +#if defined(WOLFCOSE_HAVE_HPKE_0) /* HPKE recipient state is deliberately private. WOLFCOSE_HDR is a public * structure, so adding decoded HPKE fields to it would silently break callers * that allocate the previously published layout. */ @@ -1379,10 +1380,16 @@ typedef struct WOLFCOSE_HPKE_HDR { size_t ekLen; int hasEk; } WOLFCOSE_HPKE_HDR; +#endif +#if defined(WOLFCOSE_HAVE_HPKE_0) static int wolfCose_DecodeUnprotectedHdrEx(WOLFCOSE_CBOR_CTX* ctx, WOLFCOSE_HDR* hdr, WOLFCOSE_HDR_STATE* hdrState, WOLFCOSE_HPKE_HDR* hpkeHdr) +#else +static int wolfCose_DecodeUnprotectedHdrEx(WOLFCOSE_CBOR_CTX* ctx, + WOLFCOSE_HDR* hdr, WOLFCOSE_HDR_STATE* hdrState) +#endif { int ret; size_t mapCount = 0; @@ -1391,10 +1398,6 @@ static int wolfCose_DecodeUnprotectedHdrEx(WOLFCOSE_CBOR_CTX* ctx, size_t bstrLen; int skipped; -#if !defined(WOLFCOSE_HAVE_HPKE_0) - (void)hpkeHdr; -#endif - if ((ctx == NULL) || (hdr == NULL) || (hdrState == NULL)) { ret = WOLFCOSE_E_INVALID_ARG; } @@ -1530,7 +1533,11 @@ static int wolfCose_DecodeUnprotectedHdrEx(WOLFCOSE_CBOR_CTX* ctx, int wolfCose_DecodeUnprotectedHdr(WOLFCOSE_CBOR_CTX* ctx, WOLFCOSE_HDR* hdr, WOLFCOSE_HDR_STATE* hdrState) { +#if defined(WOLFCOSE_HAVE_HPKE_0) return wolfCose_DecodeUnprotectedHdrEx(ctx, hdr, hdrState, NULL); +#else + return wolfCose_DecodeUnprotectedHdrEx(ctx, hdr, hdrState); +#endif } #if defined(WOLFCOSE_SIGN_VERIFY) || defined(WOLFCOSE_ENCRYPT_DECRYPT) || \ From 367eafd65a02b52a575fb5f5935c64c5eda0c23c Mon Sep 17 00:00:00 2001 From: Aidan Garske <aidan@wolfssl.com> Date: Wed, 2 Sep 2026 14:59:52 -0700 Subject: [PATCH 09/11] Analyze experimental HPKE in MISRA CI --- .github/workflows/misra-2012.yml | 13 ++++++++++++- docs/MISRA-Compliance.md | 2 +- tests/misra_consumer.c | 11 +++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/.github/workflows/misra-2012.yml b/.github/workflows/misra-2012.yml index 9313674..3af2d9e 100644 --- a/.github/workflows/misra-2012.yml +++ b/.github/workflows/misra-2012.yml @@ -59,7 +59,7 @@ jobs: --enable-curve25519 --enable-aesgcm --enable-aesccm \ --enable-sha384 --enable-sha512 --enable-keygen \ --enable-rsapss --enable-chacha --enable-poly1305 \ - --enable-mldsa \ + --enable-mldsa --enable-hpke \ --prefix=$HOME/wolfssl-install make -j$(nproc) make install @@ -82,6 +82,11 @@ jobs: # a full production build does. Only true user inputs are supplied. cc -std=c99 -Wall -Wextra -Wpedantic -Werror \ -DWOLFCOSE_FLOAT -DWOLFCOSE_ENABLE_EXT_SIGN \ + -DWOLFCOSE_EXPERIMENTAL \ + -DWOLFCOSE_ENABLE_HPKE_0_ENCRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_DECRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT \ -I include -I "$HOME/wolfssl-install/include" \ -fsyntax-only tests/misra_consumer.c @@ -107,8 +112,14 @@ jobs: -DWOLFSSL_AES_DIRECT \ -DHAVE_HKDF \ -DHAVE_AES_CBC \ + -DHAVE_HPKE \ -DWOLFCOSE_FLOAT \ -DWOLFCOSE_ENABLE_EXT_SIGN \ + -DWOLFCOSE_EXPERIMENTAL \ + -DWOLFCOSE_ENABLE_HPKE_0_ENCRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_DECRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_KE_ENCRYPT \ + -DWOLFCOSE_ENABLE_HPKE_0_KE_DECRYPT \ -I include -I src -I $HOME/wolfssl-install/include \ src/wolfcose.c src/wolfcose_cbor.c \ tests/misra_consumer.c \ diff --git a/docs/MISRA-Compliance.md b/docs/MISRA-Compliance.md index 4851d8d..9d3a7c2 100644 --- a/docs/MISRA-Compliance.md +++ b/docs/MISRA-Compliance.md @@ -15,7 +15,7 @@ Verified via cppcheck's MISRA addon (`--addon=misra`) with all wolfCOSE algorith **Workflow**: `.github/workflows/misra-2012.yml` -All wolfCOSE and wolfSSL feature macros are explicitly defined so cppcheck checks the full code path rather than enumerating wolfSSL's hundreds of platform `#ifdef` configurations. See [[Macros]] for the complete list. +All wolfCOSE and wolfSSL feature macros are explicitly defined so cppcheck checks the full code path rather than enumerating wolfSSL's hundreds of platform `#ifdef` configurations. This includes all four default-off experimental COSE-HPKE operation gates, `WOLFCOSE_EXPERIMENTAL`, and wolfSSL HPKE support, so draft code is analyzed rather than silently excluded. See [[Macros]] for the complete list. ### MISRA C:2023 diff --git a/tests/misra_consumer.c b/tests/misra_consumer.c index ca25de1..3347a8b 100644 --- a/tests/misra_consumer.c +++ b/tests/misra_consumer.c @@ -89,6 +89,12 @@ static size_t wolfCose_MisraUsePublicApis(void) #ifdef WOLFCOSE_ENCRYPT0_DECRYPT WOLFCOSE_MISRA_USE_API(wc_CoseEncrypt0_Decrypt); #endif +#ifdef WOLFCOSE_HPKE_0_ENCRYPT + WOLFCOSE_MISRA_USE_API(wc_CoseHpkeEncrypt0_Encrypt); +#endif +#ifdef WOLFCOSE_HPKE_0_DECRYPT + WOLFCOSE_MISRA_USE_API(wc_CoseHpkeEncrypt0_Decrypt); +#endif #ifdef WOLFCOSE_MAC0_CREATE WOLFCOSE_MISRA_USE_API(wc_CoseMac0_Create); #endif @@ -134,6 +140,11 @@ int main(void) WOLFCOSE_ALG_ECDH_ES_A128KW, WOLFCOSE_ALG_ECDH_ES_A192KW, WOLFCOSE_ALG_ECDH_ES_A256KW, +#ifdef WOLFCOSE_HAVE_HPKE_0 + WOLFCOSE_HDR_HPKE_EK, + WOLFCOSE_ALG_HPKE_0, + WOLFCOSE_ALG_HPKE_0_KE, +#endif WOLFCOSE_CRV_ML_DSA_44, WOLFCOSE_CRV_ML_DSA_65, WOLFCOSE_CRV_ML_DSA_87, From 7b6bc6a4978066275026774bd1075458e029785c Mon Sep 17 00:00:00 2001 From: Aidan Garske <aidan@wolfssl.com> Date: Wed, 2 Sep 2026 15:01:06 -0700 Subject: [PATCH 10/11] Refresh MISRA HPKE dependency cache --- .github/workflows/misra-2012.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/misra-2012.yml b/.github/workflows/misra-2012.yml index 3af2d9e..389b7f4 100644 --- a/.github/workflows/misra-2012.yml +++ b/.github/workflows/misra-2012.yml @@ -43,7 +43,7 @@ jobs: uses: actions/cache@v4 with: path: ~/wolfssl-install - key: wolfssl-ubuntu-24.04-v4-${{ env.WOLFSSL_REF }} + key: wolfssl-ubuntu-24.04-v5-${{ env.WOLFSSL_REF }} - name: Build wolfSSL if: steps.cache-wolfssl.outputs.cache-hit != 'true' From c878098cae594d36edec363856f1058743b02ae1 Mon Sep 17 00:00:00 2001 From: Aidan Garske <aidan@wolfssl.com> Date: Thu, 3 Sep 2026 09:52:49 -0700 Subject: [PATCH 11/11] Fix HPKE MISRA findings --- docs/MISRA-Compliance.md | 2 + scripts/misra-deviations.json | 20 ++++++++ src/wolfcose.c | 89 +++++++++++++++++++---------------- 3 files changed, 71 insertions(+), 40 deletions(-) diff --git a/docs/MISRA-Compliance.md b/docs/MISRA-Compliance.md index 9d3a7c2..f536da6 100644 --- a/docs/MISRA-Compliance.md +++ b/docs/MISRA-Compliance.md @@ -84,6 +84,8 @@ tracked conditional preprocessing directive. |----|------|----------|--------------------------| | D-11.5-001 | 11.5 | `wolfCose_ForceZero` | Converting the caller's object pointer to a volatile character pointer is the standard C mechanism for securely erasing its object representation. Character access is alignment-safe and the function allocates no memory. `make zeroize-test`, `make zero-alloc-check`, and `make c99-check` validate the implementation. | | D-19.2-001 | 19.2 | `WOLFCOSE_KEY.key` | The public, discriminated union preserves the established ABI and embedded-memory footprint. `kty` and `attachedType` govern member access. Replacing it with a structure would break ABI and increase RAM. The full tests exercise the supported members, and CI anchors the deviation to the exact union boundaries. | +| D-11.8-002 | 11.8 | `wolfCose_Hpke0Seal` plaintext input | The pinned wolfSSL `wc_HpkeSealBase()` interface accepts a mutable plaintext pointer even though its implementation only consumes plaintext while writing to its distinct ciphertext output. The wrapper keeps the wolfCOSE public input const-qualified and avoids a payload-sized copy in constrained builds. The deviation is anchored to the one interoperability cast and covered by the HPKE unit and sanitizer CI lanes. | +| D-11.8-003 | 11.8 | `wolfCose_Hpke0Open` ciphertext input | The pinned wolfSSL `wc_HpkeOpenBase()` interface accepts a mutable ciphertext pointer even though its implementation only consumes ciphertext while writing to its distinct plaintext output. The wrapper keeps the wolfCOSE public input const-qualified and avoids a ciphertext-sized copy in constrained builds. The deviation is anchored to the one interoperability cast and covered by the HPKE unit and sanitizer CI lanes. | `scripts/misra-deviations.json` identifies only these source locations. The classifier also pins a hash of the public union declaration. It fails if an diff --git a/scripts/misra-deviations.json b/scripts/misra-deviations.json index 3a6dfc2..8a387cd 100644 --- a/scripts/misra-deviations.json +++ b/scripts/misra-deviations.json @@ -22,6 +22,26 @@ ], "expected": 2, "range_sha256": "5ea3030e1870253ae4805cb405bf516807cac046c242467fe45648727321b261" + }, + { + "id": "D-11.8-002", + "rule": "11.8", + "path": "src/wolfcose.c", + "symbol": "wolfCose_Hpke0Seal", + "anchors": [ + " (byte*)plaintext, (word32)plaintextLen, ciphertext);" + ], + "expected": 1 + }, + { + "id": "D-11.8-003", + "rule": "11.8", + "path": "src/wolfcose.c", + "symbol": "wolfCose_Hpke0Open", + "anchors": [ + " (byte*)ciphertext, (word32)ciphertextBodyLen, plaintext);" + ], + "expected": 1 } ] } diff --git a/src/wolfcose.c b/src/wolfcose.c index d81f26d..ff84077 100644 --- a/src/wolfcose.c +++ b/src/wolfcose.c @@ -394,14 +394,6 @@ WOLFCOSE_LOCAL const uint8_t WOLFCOSE_CTX_ENCRYPT0[8] = { WOLFCOSE_LOCAL const uint8_t WOLFCOSE_CTX_ENCRYPT[7] = { 0x45u, 0x6Eu, 0x63u, 0x72u, 0x79u, 0x70u, 0x74u }; -#if defined(WOLFCOSE_HPKE_0_KE_ENCRYPT) || \ - defined(WOLFCOSE_HPKE_0_KE_DECRYPT) -static const uint8_t wolfCose_HpkeRecipientContext[14] = { - 0x48u, 0x50u, 0x4Bu, 0x45u, 0x20u, 0x52u, 0x65u, - 0x63u, 0x69u, 0x70u, 0x69u, 0x65u, 0x6Eu, 0x74u -}; -#endif - /* ----- Internal helpers: algorithm dispatch ----- */ int wolfCose_AlgToHashType(int32_t alg, enum wc_HashType* hashType) @@ -5345,28 +5337,32 @@ static int wolfCose_Hpke0ValidateKey(const WOLFCOSE_KEY* key, int32_t alg, int needPrivate) { int ret = WOLFCOSE_SUCCESS; + ecc_key* eccKey = NULL; if (key == NULL) { ret = WOLFCOSE_E_INVALID_ARG; } - else if ((key->kty != WOLFCOSE_KTY_EC2) || - (key->crv != WOLFCOSE_CRV_P256) || - (key->key.ecc == NULL) || - (wc_ecc_size(key->key.ecc) != 32)) { - ret = WOLFCOSE_E_COSE_KEY_TYPE; - } - else if (wolfCose_EccKeyCheckCurve(WOLFCOSE_CRV_P256, - key->key.ecc) != WOLFCOSE_SUCCESS) { - ret = WOLFCOSE_E_COSE_KEY_TYPE; - } - else if ((key->alg != WOLFCOSE_ALG_UNSET) && (key->alg != alg)) { - ret = WOLFCOSE_E_COSE_BAD_ALG; - } - else if ((needPrivate != 0) && (key->hasPrivate != 1u)) { - ret = WOLFCOSE_E_COSE_KEY_TYPE; - } else { - /* Key is valid. */ + eccKey = key->key.ecc; + if ((key->kty != WOLFCOSE_KTY_EC2) || + (key->crv != WOLFCOSE_CRV_P256) || + (eccKey == NULL) || + (wc_ecc_size(eccKey) != 32)) { + ret = WOLFCOSE_E_COSE_KEY_TYPE; + } + else if (wolfCose_EccKeyCheckCurve(WOLFCOSE_CRV_P256, + eccKey) != WOLFCOSE_SUCCESS) { + ret = WOLFCOSE_E_COSE_KEY_TYPE; + } + else if ((key->alg != WOLFCOSE_ALG_UNSET) && (key->alg != alg)) { + ret = WOLFCOSE_E_COSE_BAD_ALG; + } + else if ((needPrivate != 0) && (key->hasPrivate != 1u)) { + ret = WOLFCOSE_E_COSE_KEY_TYPE; + } + else { + /* Key is valid. */ + } } return ret; @@ -5380,6 +5376,10 @@ static int wolfCose_Hpke0BuildRecipientInfo(int32_t nextLayerAlg, const uint8_t* recipientProtected, size_t recipientProtectedLen, uint8_t* scratch, size_t scratchSz, size_t* infoLen) { + static const uint8_t recipientContext[14] = { + 0x48u, 0x50u, 0x4Bu, 0x45u, 0x20u, 0x52u, 0x65u, + 0x63u, 0x69u, 0x70u, 0x69u, 0x65u, 0x6Eu, 0x74u + }; int ret; WOLFCOSE_CBOR_CTX ctx; @@ -5394,8 +5394,8 @@ static int wolfCose_Hpke0BuildRecipientInfo(int32_t nextLayerAlg, ret = wc_CBOR_EncodeArrayStart(&ctx, 4u); if (ret == WOLFCOSE_SUCCESS) { - ret = wc_CBOR_EncodeTstr(&ctx, wolfCose_HpkeRecipientContext, - sizeof(wolfCose_HpkeRecipientContext)); + ret = wc_CBOR_EncodeTstr(&ctx, recipientContext, + sizeof(recipientContext)); } if (ret == WOLFCOSE_SUCCESS) { ret = wc_CBOR_EncodeInt(&ctx, (int64_t)nextLayerAlg); @@ -5433,7 +5433,10 @@ static int wolfCose_Hpke0EncodeRecipientProtectedHdr(int32_t alg, ret = WOLFCOSE_E_INVALID_ARG; } else { - mapEntries = (kid != NULL) ? 2u : 1u; + mapEntries = (size_t)1u; + if (kid != NULL) { + mapEntries = (size_t)2u; + } ctx.buf = buf; ctx.bufSz = bufSz; ctx.idx = 0u; @@ -5537,13 +5540,14 @@ static int wolfCose_Hpke0SealInit(WOLFCOSE_HPKE_0_SEAL_CTX* sealCtx, /* Perform the one-shot HPKE seal with a prepared ephemeral key. */ static int wolfCose_Hpke0Seal(WOLFCOSE_HPKE_0_SEAL_CTX* sealCtx, const WOLFCOSE_KEY* recipientKey, - const uint8_t* info, size_t infoLen, - const uint8_t* aad, size_t aadLen, + uint8_t* info, size_t infoLen, + uint8_t* aad, size_t aadLen, const uint8_t* plaintext, size_t plaintextLen, uint8_t* ciphertext, size_t ciphertextSz) { int ret = WOLFCOSE_SUCCESS; int hpkeRet; + ecc_key* recipientEcc = NULL; if ((sealCtx == NULL) || (recipientKey == NULL) || (plaintext == NULL) || (ciphertext == NULL)) { @@ -5574,10 +5578,11 @@ static int wolfCose_Hpke0Seal(WOLFCOSE_HPKE_0_SEAL_CTX* sealCtx, } if (ret == WOLFCOSE_SUCCESS) { + recipientEcc = recipientKey->key.ecc; hpkeRet = wc_HpkeSealBase(&sealCtx->hpke, - &sealCtx->ephemeralKey, (void*)recipientKey->key.ecc, - (byte*)info, (word32)infoLen, - (byte*)aad, (word32)aadLen, + &sealCtx->ephemeralKey, recipientEcc, + info, (word32)infoLen, + aad, (word32)aadLen, (byte*)plaintext, (word32)plaintextLen, ciphertext); if (hpkeRet != 0) { ret = WOLFCOSE_E_CRYPTO; @@ -5603,8 +5608,8 @@ static void wolfCose_Hpke0SealFree(WOLFCOSE_HPKE_0_SEAL_CTX* sealCtx) /* Perform a single HPKE base-mode open with the fixed HPKE-0 suite. */ static int wolfCose_Hpke0Open(const WOLFCOSE_KEY* recipientKey, int32_t alg, - const uint8_t* info, size_t infoLen, - const uint8_t* aad, size_t aadLen, + uint8_t* info, size_t infoLen, + uint8_t* aad, size_t aadLen, const uint8_t* enc, size_t encLen, const uint8_t* ciphertext, size_t ciphertextLen, uint8_t* plaintext, size_t plaintextSz) @@ -5612,6 +5617,7 @@ static int wolfCose_Hpke0Open(const WOLFCOSE_KEY* recipientKey, int ret = WOLFCOSE_SUCCESS; int hpkeRet; Hpke hpke; + ecc_key* recipientEcc = NULL; size_t ciphertextBodyLen = 0u; if ((enc == NULL) || (ciphertext == NULL) || (plaintext == NULL)) { @@ -5652,10 +5658,11 @@ static int wolfCose_Hpke0Open(const WOLFCOSE_KEY* recipientKey, } } if (ret == WOLFCOSE_SUCCESS) { - hpkeRet = wc_HpkeOpenBase(&hpke, (void*)recipientKey->key.ecc, + recipientEcc = recipientKey->key.ecc; + hpkeRet = wc_HpkeOpenBase(&hpke, recipientEcc, enc, (word16)encLen, - (byte*)info, (word32)infoLen, - (byte*)aad, (word32)aadLen, + info, (word32)infoLen, + aad, (word32)aadLen, (byte*)ciphertext, (word32)ciphertextBodyLen, plaintext); if (hpkeRet != 0) { ret = WOLFCOSE_E_COSE_DECRYPT_FAIL; @@ -8626,10 +8633,12 @@ int wc_CoseHpkeEncrypt0_Encrypt(const WOLFCOSE_KEY* recipientKey, size_t ciphertextOffset = 0u; WOLFCOSE_CBOR_CTX ctx; WOLFCOSE_HPKE_0_SEAL_CTX sealCtx; - int isDetached; + int isDetached = 0; (void)XMEMSET(&sealCtx, 0, sizeof(sealCtx)); - isDetached = (detachedPayload != NULL) ? 1 : 0; + if (detachedPayload != NULL) { + isDetached = 1; + } if ((recipientKey == NULL) || (payload == NULL) || (scratch == NULL) || (out == NULL) || (outLen == NULL) || (rng == NULL)) {