Skip to content

Commit 90db565

Browse files
authored
Add COSE_Key output options, exact size queries, and CBOR parsing helpers (#66)
2 parents 588232e + 831acf0 commit 90db565

8 files changed

Lines changed: 3006 additions & 224 deletions

File tree

docs/API-Reference.md

Lines changed: 307 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -291,11 +291,24 @@ Set symmetric key material in a COSE key structure.
291291
### wc_CoseKey_Encode
292292
293293
```c
294-
int wc_CoseKey_Encode(const WOLFCOSE_KEY* key, uint8_t* buf, size_t bufSz, size_t* outLen);
294+
int wc_CoseKey_Encode(WOLFCOSE_KEY* key, uint8_t* buf, size_t bufSz, size_t* outLen);
295295
```
296296

297297
Encode a COSE key to CBOR format.
298298

299+
> **Warning: this serialises the private key when the key has one.**
300+
> `wc_CoseKey_SetEcc()` sets `key->hasPrivate` whenever the attached `ecc_key`
301+
> is a keypair, and `wc_CoseKey_Encode()` then emits the `-4: d` entry. The
302+
> output is a perfectly valid `COSE_Key`, only longer - for P-256 with `alg`
303+
> set, 112 bytes / `map(6)` instead of 77 bytes / `map(5)` - so nothing fails
304+
> loudly. The same applies to RSA (`-3: d` plus the CRT factors), Ed25519 and
305+
> Ed448 (`-4: d`), an RFC 9964 AKP key (`-2: priv` seed), and symmetric keys
306+
> (`-1: k` is the whole key). Anything that publishes a public key derived
307+
> from a live keypair - WebAuthn/CTAP2 attestation `authData`, ECDH key
308+
> agreement, JWK-style publication - must use
309+
> [`wc_CoseKey_Encode_ex()`](#wc_cosekey_encode_ex) with
310+
> `WOLFCOSE_KEY_PUBLIC_ONLY`.
311+
299312
**Parameters:**
300313
| Name | Description |
301314
|------|-------------|
@@ -308,6 +321,107 @@ Encode a COSE key to CBOR format.
308321

309322
---
310323

324+
### wc_CoseKey_Encode_ex
325+
326+
```c
327+
int wc_CoseKey_Encode_ex(WOLFCOSE_KEY* key, uint8_t* buf, size_t bufSz,
328+
size_t* outLen, uint32_t flags);
329+
```
330+
331+
As `wc_CoseKey_Encode()`, plus output options. Passing `flags = 0` is
332+
equivalent to calling `wc_CoseKey_Encode()`.
333+
334+
| Flag | Effect |
335+
|------|--------|
336+
| `WOLFCOSE_KEY_PUBLIC_ONLY` | Emit the public half only. No `-4: d` for EC2/OKP, no `-3: d` or CRT factors for RSA, no `-2: priv` seed for an AKP key. |
337+
338+
Unknown flag bits are rejected with `WOLFCOSE_E_INVALID_ARG`.
339+
340+
A symmetric key has no public half - `-1: k` *is* the key - so
341+
`WOLFCOSE_KEY_PUBLIC_ONLY` on `WOLFCOSE_KTY_SYMMETRIC` returns
342+
`WOLFCOSE_E_COSE_KEY_TYPE` rather than emitting a key with no key material.
343+
344+
The key structure is not modified; the flag affects only this call, so the
345+
same key can still sign afterwards.
346+
347+
```c
348+
/* Publish the attestation public key without the private scalar. */
349+
ret = wc_CoseKey_Encode_ex(&key, cose, sizeof(cose), &coseLen,
350+
WOLFCOSE_KEY_PUBLIC_ONLY);
351+
```
352+
353+
**Returns:** `WOLFCOSE_SUCCESS` or error code
354+
355+
---
356+
357+
### wc_CoseKey_EncodeEccRaw
358+
359+
```c
360+
int wc_CoseKey_EncodeEccRaw(int32_t crv,
361+
const uint8_t* x, const uint8_t* y,
362+
const uint8_t* d, size_t coordLen,
363+
const uint8_t* kid, size_t kidLen, int32_t alg,
364+
uint8_t* out, size_t outSz, size_t* outLen);
365+
```
366+
367+
Encode an EC2 `COSE_Key` straight from raw affine coordinates. For callers
368+
that hold only the coordinates - re-emitting a stored credential record, or
369+
echoing a peer key - this avoids `wc_ecc_import_unsigned()`, which pays a full
370+
point import, an on-curve check, and an `ecc_key` worth of stack purely to
371+
serialise bytes that are already in hand.
372+
373+
The output is byte-identical to what `wc_CoseKey_Encode_ex()` produces for the
374+
same `crv`/`kid`/`alg` and the same coordinates.
375+
376+
**Parameters:**
377+
| Name | Description |
378+
|------|-------------|
379+
| `crv` | `WOLFCOSE_CRV_P256` / `P384` / `P521` (EC2 curves only) |
380+
| `x`, `y` | Coordinates, each exactly `coordLen` bytes, big-endian, zero-padded |
381+
| `d` | Private scalar (`coordLen` bytes), or `NULL` for a public-only key |
382+
| `coordLen` | Must equal the curve size: 32, 48, or 66 |
383+
| `kid`, `kidLen` | Optional key identifier (`NULL`, 0 to omit) |
384+
| `alg` | Algorithm for the `3: alg` entry, or `WOLFCOSE_ALG_UNSET` to omit |
385+
| `out`, `outSz`, `outLen` | Output buffer and encoded length |
386+
387+
**Returns:** `WOLFCOSE_SUCCESS` or error code
388+
389+
Nothing here checks that `(x, y)` is on the curve: the bytes are copied into
390+
the map as supplied. Encoding an unvalidated point is safe; *using* one is
391+
not, so import it through wolfCrypt before any ECDH or verify operation.
392+
393+
There is deliberately no `wc_CoseKey_SetEccRaw()`. `WOLFCOSE_KEY` holds a
394+
single pointer-sized union member for the key, which cannot carry three
395+
independent buffers (`x`, `y`, `d`), and widening the structure would change
396+
`sizeof(WOLFCOSE_KEY)` and break the ABI for already-compiled callers. The
397+
free function above is the supported raw-coordinate path.
398+
399+
---
400+
401+
### wc_CoseKey_EncodeSize / wc_CoseKey_EncodeSize_ex
402+
403+
```c
404+
int wc_CoseKey_EncodeSize(const WOLFCOSE_KEY* key, size_t* outLen);
405+
int wc_CoseKey_EncodeSize_ex(const WOLFCOSE_KEY* key, size_t* outLen,
406+
uint32_t flags);
407+
```
408+
409+
Compute the **exact** number of bytes `wc_CoseKey_Encode()` /
410+
`wc_CoseKey_Encode_ex()` would write, without encoding into a buffer. This is
411+
the `COSE_Key` counterpart of
412+
[`wc_CoseSign1_SignSize_ex()`](#wc_cosesign1_signsize_ex): nothing is written
413+
and no key material is exported, only the component lengths of the attached
414+
key are read. The result is exact, not an upper bound, so it can size a buffer
415+
or reject an oversized key before committing storage.
416+
417+
`flags` takes the same `WOLFCOSE_KEY_*` values as `wc_CoseKey_Encode_ex()`, so
418+
`WOLFCOSE_KEY_PUBLIC_ONLY` sizes the public-only encoding. `flags = 0` sizes
419+
the private encoding - see the warning on `wc_CoseKey_Encode()`.
420+
421+
**Returns:** `WOLFCOSE_SUCCESS` or error code
422+
423+
---
424+
311425
### wc_CoseKey_Decode
312426

313427
```c
@@ -331,9 +445,71 @@ Attach the wolfCrypt key with `wc_CoseKey_SetEcc()`, `wc_CoseKey_SetEd25519()`,
331445
assigning the `key.*` union directly does not, and no key material is imported.
332446
333447
The decoded `kty`/`crv` must name the attached key type or
334-
`WOLFCOSE_E_COSE_KEY_TYPE` is returned before any importer runs. Decoding into a
335-
key with nothing attached runs no importer and yields metadata only, which is
336-
how to inspect an untrusted `COSE_Key` before choosing a key object for it.
448+
`WOLFCOSE_E_COSE_KEY_TYPE` is returned before any importer runs. To learn which
449+
key type a buffer holds before attaching anything, use
450+
[`wc_CoseKey_PeekInfo()`](#wc_cosekey_peekinfo).
451+
452+
Decoding is strict: preferred CBOR only, integer labels only, no duplicate
453+
labels, and `bufSz` must be exactly the encoded length. See
454+
[Getting Started - Strict
455+
decoding](Getting-Started.md#strict-decoding-rfc-8949-preferred-serialization).
456+
457+
---
458+
459+
### wc_CoseKey_PeekInfo
460+
461+
```c
462+
typedef struct WOLFCOSE_KEY_INFO {
463+
int32_t kty; /* WOLFCOSE_KTY_*, always set on success */
464+
int32_t alg; /* WOLFCOSE_ALG_*, WOLFCOSE_ALG_UNSET if absent */
465+
int32_t crv; /* WOLFCOSE_CRV_*, 0 if absent or N/A */
466+
const uint8_t* kid; /* Key ID, zero-copy pointer, NULL if absent */
467+
size_t kidLen;
468+
} WOLFCOSE_KEY_INFO;
469+
470+
int wc_CoseKey_PeekInfo(const uint8_t* in, size_t inSz,
471+
WOLFCOSE_KEY_INFO* info);
472+
```
473+
474+
Read `kty`, `alg`, `crv`, and `kid` out of a `COSE_Key` buffer without
475+
importing any key material and without needing a wolfCrypt key object.
476+
477+
`wc_CoseKey_Decode()` requires the caller to have attached a key of the
478+
matching type up front and returns `WOLFCOSE_E_COSE_KEY_TYPE` otherwise, so a
479+
parser that accepts more than one key type would have to guess and retry.
480+
Peek first, then attach once:
481+
482+
```c
483+
WOLFCOSE_KEY_INFO info;
484+
485+
ret = wc_CoseKey_PeekInfo(in, inSz, &info);
486+
if (ret == WOLFCOSE_SUCCESS) {
487+
if (info.kty == WOLFCOSE_KTY_EC2) {
488+
(void)wc_ecc_init(&ecc);
489+
ret = wc_CoseKey_SetEcc(&key, info.crv, &ecc);
490+
}
491+
else if (info.kty == WOLFCOSE_KTY_OKP) {
492+
(void)wc_ed25519_init(&ed);
493+
ret = wc_CoseKey_SetEd25519(&key, &ed);
494+
}
495+
/* ... */
496+
if (ret == WOLFCOSE_SUCCESS) {
497+
ret = wc_CoseKey_Decode(&key, in, inSz);
498+
}
499+
}
500+
```
501+
502+
`in` is not modified and nothing is consumed, so the call is repeatable. `kid`
503+
points into `in`, so it stays valid only as long as that buffer does.
504+
505+
The same structural checks `wc_CoseKey_Decode()` applies are applied here -
506+
integer labels only, no duplicate labels, `kty` required, no trailing bytes -
507+
so a buffer that peeks successfully will not be rejected by the decoder for
508+
those reasons. Label `-1` is `crv` for EC2/OKP but `k`/`n` for symmetric/RSA
509+
keys; the value is dispatched on its CBOR type, so `crv` stays 0 for the
510+
latter. On any error every field of `info` is cleared.
511+
512+
**Returns:** `WOLFCOSE_SUCCESS` or error code
337513

338514
---
339515

@@ -788,10 +964,38 @@ Verify a COSE_Mac message as a specific recipient.
788964

789965
Basic CBOR encoding/decoding functions in `wolfcose.h`:
790966

967+
### Context Setup
968+
969+
`WOLFCOSE_CBOR_CTX` carries both a mutable `buf` (encode) and a const `cbuf`
970+
(decode). These set the right one, clear the other, set `bufSz`, and zero
971+
`idx` in a single call:
972+
973+
```c
974+
int wc_CBOR_EncoderInit(WOLFCOSE_CBOR_CTX* ctx, uint8_t* buf, size_t bufSz);
975+
int wc_CBOR_DecoderInit(WOLFCOSE_CBOR_CTX* ctx, const uint8_t* buf, size_t bufSz);
976+
```
977+
978+
Both are `static inline` in the header, so they cost nothing over assigning
979+
the fields by hand.
980+
981+
```c
982+
WOLFCOSE_CBOR_CTX ctx;
983+
984+
(void)wc_CBOR_EncoderInit(&ctx, out, sizeof(out));
985+
ret = wc_CBOR_EncodeMapStart(&ctx, 2);
986+
...
987+
(void)wc_CBOR_DecoderInit(&ctx, in, inSz);
988+
ret = wc_CBOR_DecodeMapStart(&ctx, &count);
989+
```
990+
991+
**Returns:** `WOLFCOSE_SUCCESS`, or `WOLFCOSE_E_INVALID_ARG` if `ctx` or `buf`
992+
is `NULL`.
993+
791994
### Encoding Functions
792995

793996
| Function | Description |
794997
|----------|-------------|
998+
| `wc_CBOR_EncoderInit(ctx, buf, bufSz)` | Initialize an encode context |
795999
| `wc_CBOR_EncodeUint(ctx, val)` | Encode unsigned integer |
7961000
| `wc_CBOR_EncodeInt(ctx, val)` | Encode signed integer |
7971001
| `wc_CBOR_EncodeBstr(ctx, data, len)` | Encode byte string |
@@ -806,16 +1010,115 @@ Basic CBOR encoding/decoding functions in `wolfcose.h`:
8061010

8071011
| Function | Description |
8081012
|----------|-------------|
1013+
| `wc_CBOR_DecoderInit(ctx, buf, bufSz)` | Initialize a decode context |
8091014
| `wc_CBOR_DecodeUint(ctx, val)` | Decode unsigned integer |
8101015
| `wc_CBOR_DecodeInt(ctx, val)` | Decode signed integer |
8111016
| `wc_CBOR_DecodeBstr(ctx, data, len)` | Decode byte string (zero-copy) |
8121017
| `wc_CBOR_DecodeTstr(ctx, str, len)` | Decode text string (zero-copy) |
8131018
| `wc_CBOR_DecodeArrayStart(ctx, count)` | Decode array header |
8141019
| `wc_CBOR_DecodeMapStart(ctx, count)` | Decode map header |
8151020
| `wc_CBOR_DecodeTag(ctx, tag)` | Decode CBOR tag |
1021+
| `wc_CBOR_DecodeLabel(ctx, label)` | Decode an int-or-text map label |
8161022
| `wc_CBOR_Skip(ctx)` | Skip over any CBOR item |
1023+
| `wc_CBOR_SkipItem(ctx, data, dataLen)` | Skip an item and capture its raw bytes |
8171024
| `wc_CBOR_PeekType(ctx)` | Peek at next item's major type |
8181025

1026+
> **Decoding is strict by design.** Every decode entry point requires
1027+
> RFC 8949 Section 4.2.1 preferred (shortest-form) arguments and rejects
1028+
> indefinite lengths. See [Getting Started - Strict
1029+
> decoding](Getting-Started.md#strict-decoding-rfc-8949-preferred-serialization)
1030+
> before debugging an interop failure.
1031+
1032+
### wc_CBOR_SkipItem
1033+
1034+
```c
1035+
int wc_CBOR_SkipItem(WOLFCOSE_CBOR_CTX* ctx,
1036+
const uint8_t** data, size_t* dataLen);
1037+
```
1038+
1039+
As `wc_CBOR_Skip()`, but also reports the raw encoded bytes of the item that
1040+
was skipped, zero-copy into the decoder input. This is the deferred/nested
1041+
parse primitive: capture a sub-item now, parse it later or with a different
1042+
decoder. It replaces the hand-rolled
1043+
1044+
```c
1045+
start = ctx.idx;
1046+
ret = wc_CBOR_Skip(&ctx);
1047+
ptr = ctx.cbuf + start;
1048+
len = ctx.idx - start;
1049+
```
1050+
1051+
that every integrator ends up writing - CTAP2 `allowList` entries, a
1052+
`COSE_Key` embedded in an extension map, `keyAgreement` in
1053+
`authenticatorClientPIN`.
1054+
1055+
```c
1056+
const uint8_t* sub;
1057+
size_t subLen;
1058+
1059+
ret = wc_CBOR_SkipItem(&ctx, &sub, &subLen);
1060+
if (ret == WOLFCOSE_SUCCESS) {
1061+
ret = wc_CoseKey_Decode(&peerKey, sub, subLen); /* parse it later */
1062+
}
1063+
```
1064+
1065+
`wc_CBOR_Skip()` is unchanged. On failure the outputs are untouched.
1066+
1067+
**Returns:** `WOLFCOSE_SUCCESS` or error code
1068+
1069+
---
1070+
1071+
### wc_CBOR_DecodeLabel
1072+
1073+
```c
1074+
typedef struct WOLFCOSE_CBOR_LABEL {
1075+
int64_t val; /* Integer label, valid when isText == 0 */
1076+
const uint8_t* text; /* Text label, points into the input buffer */
1077+
size_t textLen;
1078+
uint8_t isText;
1079+
} WOLFCOSE_CBOR_LABEL;
1080+
1081+
int wc_CBOR_DecodeLabel(WOLFCOSE_CBOR_CTX* ctx, WOLFCOSE_CBOR_LABEL* label);
1082+
int wc_CBOR_LabelIsInt(const WOLFCOSE_CBOR_LABEL* label, int64_t val);
1083+
int wc_CBOR_LabelIsText(const WOLFCOSE_CBOR_LABEL* label,
1084+
const uint8_t* text, size_t textLen);
1085+
```
1086+
1087+
RFC 9052 defines `label = int / tstr`, and real COSE and CTAP2 maps use both
1088+
spellings for the same field (`3` vs `"alg"`, `1` vs `"type"`, `2` vs `"id"`).
1089+
`wc_CBOR_DecodeLabel()` consumes one item and reports whichever form it found,
1090+
so a parser writes the dispatch once instead of duplicating a
1091+
`wc_CBOR_PeekType()` branch at every map.
1092+
1093+
Major types 0 and 1 fill `val` with `isText == 0`; major type 3 fills
1094+
`text`/`textLen` with `isText == 1` and no copy. Anything else returns
1095+
`WOLFCOSE_E_CBOR_TYPE`.
1096+
1097+
`wc_CBOR_LabelIsInt()` and `wc_CBOR_LabelIsText()` return 1 on match and 0
1098+
otherwise, including for a `NULL` label. Text comparison is byte-exact: no
1099+
Unicode normalization or case folding, matching how CTAP2 and COSE compare
1100+
labels.
1101+
1102+
```c
1103+
WOLFCOSE_CBOR_LABEL label;
1104+
static const uint8_t algText[] = "alg";
1105+
1106+
ret = wc_CBOR_DecodeLabel(&ctx, &label);
1107+
if (wc_CBOR_LabelIsInt(&label, 3) || wc_CBOR_LabelIsText(&label, algText, 3)) {
1108+
ret = wc_CBOR_DecodeInt(&ctx, &alg);
1109+
}
1110+
else {
1111+
ret = wc_CBOR_Skip(&ctx);
1112+
}
1113+
```
1114+
1115+
Note that `wc_CoseKey_Decode()` and the COSE header parsers accept integer
1116+
labels only, by design: silently skipping text labels would break their
1117+
duplicate-label enforcement. `wc_CBOR_DecodeLabel()` is for caller-written
1118+
parsers of protocol maps such as CTAP2.
1119+
1120+
**Returns:** `WOLFCOSE_SUCCESS` or error code
1121+
8191122
---
8201123

8211124
## Error Codes

0 commit comments

Comments
 (0)