Skip to content

Commit fb73126

Browse files
authored
Add OpenDICE cwt rendering (#84)
Add rendering of CBOR web token (CWT) with OpenDICE claims.
1 parent 2b763d4 commit fb73126

4 files changed

Lines changed: 950 additions & 0 deletions

File tree

CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ set(LIBNAT20_SOURCES
9595
src/core/asn1.c
9696
src/core/cbor.c
9797
src/core/cose.c
98+
src/core/cwt.c
9899
src/core/functionality.c
99100
src/core/oid.c
100101
src/core/stream.c
@@ -112,6 +113,7 @@ set(LIBNAT20_PUB_HEADERS
112113
include/nat20/cbor.h
113114
include/nat20/constants.h
114115
include/nat20/cose.h
116+
include/nat20/cwt.h
115117
include/nat20/crypto.h
116118
include/nat20/endian.h
117119
include/nat20/error.h
@@ -150,6 +152,7 @@ set(LIBNAT20_TEST_SOURCES
150152
src/core/test/asn1.cpp
151153
src/core/test/cbor.cpp
152154
src/core/test/cose.cpp
155+
src/core/test/cwt.cpp
153156
src/core/test/functionality.cpp
154157
src/core/test/oid.cpp
155158
src/core/test/stream.cpp

include/nat20/cwt.h

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/*
2+
* Copyright 2026 Aurora Operations, Inc.
3+
*
4+
* SPDX-License-Identifier: Apache-2.0 OR GPL-2.0
5+
*
6+
* This work is dual licensed.
7+
* You may use it under Apache-2.0 or GPL-2.0 at your option.
8+
*
9+
* Licensed under the Apache License, Version 2.0 (the "License");
10+
* you may not use this file except in compliance with the License.
11+
* You may obtain a copy of the License at
12+
*
13+
* http://www.apache.org/licenses/LICENSE-2.0
14+
*
15+
* Unless required by applicable law or agreed to in writing, software
16+
* distributed under the License is distributed on an "AS IS" BASIS,
17+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
* See the License for the specific language governing permissions and
19+
* limitations under the License.
20+
*
21+
* OR
22+
*
23+
* This program is free software; you can redistribute it and/or
24+
* modify it under the terms of the GNU General Public License
25+
* as published by the Free Software Foundation; either version 2
26+
* of the License, or (at your option) any later version.
27+
*
28+
* This program is distributed in the hope that it will be useful,
29+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
30+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31+
* GNU General Public License for more details.
32+
*
33+
* You should have received a copy of the GNU General Public License
34+
* along with this program; if not, see
35+
* <https://www.gnu.org/licenses/>.
36+
*/
37+
38+
#pragma once
39+
40+
#include <nat20/cose.h>
41+
#include <nat20/crypto.h>
42+
#include <nat20/open_dice.h>
43+
#include <nat20/stream.h>
44+
#include <nat20/types.h>
45+
46+
#ifdef __cplusplus
47+
extern "C" {
48+
#endif
49+
50+
/** @file */
51+
52+
/**
53+
* @defgroup open_dice_cwt_labels Open DICE CWT Label Constants
54+
*
55+
* These constants are defined in the OpenDice specification and represent
56+
* the labels used in the Open DICE CWT structure.
57+
*
58+
* [OpenDICE specification]
59+
* (https://pigweed.googlesource.com/open-dice/+/HEAD/docs/specification.md#cbor-cdi-certificates)
60+
*
61+
* @{
62+
*/
63+
/** Label for code hash. */
64+
#define N20_OPEN_DICE_CWT_LABEL_CODE_HASH (-4670545)
65+
/** Label for code descriptor. */
66+
#define N20_OPEN_DICE_CWT_LABEL_CODE_DESCRIPTOR (-4670546)
67+
/** Label for configuration hash. */
68+
#define N20_OPEN_DICE_CWT_LABEL_CONFIGURATION_HASH (-4670547)
69+
/** Label for configuration descriptor. */
70+
#define N20_OPEN_DICE_CWT_LABEL_CONFIGURATION_DESCRIPTOR (-4670548)
71+
/** Label for authority hash. */
72+
#define N20_OPEN_DICE_CWT_LABEL_AUTHORITY_HASH (-4670549)
73+
/** Label for authority descriptor. */
74+
#define N20_OPEN_DICE_CWT_LABEL_AUTHORITY_DESCRIPTOR (-4670550)
75+
/** Label for DICE mode. */
76+
#define N20_OPEN_DICE_CWT_LABEL_MODE (-4670551)
77+
/** Label for subject public key. */
78+
#define N20_OPEN_DICE_CWT_LABEL_SUBJECT_PUBLIC_KEY (-4670552)
79+
/** Label for key usage. */
80+
#define N20_OPEN_DICE_CWT_LABEL_KEY_USAGE (-4670553)
81+
/** Label for profile name. */
82+
#define N20_OPEN_DICE_CWT_LABEL_PROFILE (-4670554)
83+
/** @} */
84+
85+
/**
86+
* @defgroup cwt_labels CBOR Web Token (CWT) Label Constants
87+
*
88+
* These constants are defined in the CWT specification and represent
89+
* the labels used in the CWT structure.
90+
*
91+
* [CWT specification](https://tools.ietf.org/html/rfc8392)
92+
*
93+
* @{
94+
*/
95+
#define N20_CWT_LABEL_ISSUER (1) /**< Label for issuer. */
96+
#define N20_CWT_LABEL_SUBJECT (2) /**< Label for subject. */
97+
/** @} */
98+
99+
/**
100+
* @brief Convert Open DICE public key information to COSE key format.
101+
*
102+
* If either argument is NULL this function is a no-op.
103+
*
104+
* If the @p key_info->algorithm is not supported, @p cose_key will be left unmodified.
105+
*
106+
* Supported algorithms are Ed25519, P-256, and P-384 which are expressed
107+
* by @ref n20_crypto_key_type_ed25519_e, @ref n20_crypto_key_type_secp256r1_e,
108+
* and @ref n20_crypto_key_type_secp384r1_e respectively.
109+
*
110+
* The function sets the algorithm ID to @ref n20_cose_algorithm_id_eddsa_e,
111+
* @ref n20_cose_algorithm_id_es256_e, or @ref n20_cose_algorithm_id_es384_e
112+
* respectively. Note that these values have been deprecated according to [1]
113+
* but are still in use.
114+
*
115+
* The function also populates the public key from the the supplied buffer
116+
* in @p key_info->key.
117+
*
118+
* In the case of P-256 and P-384, the function assumes that the key buffer
119+
* is formatted as an uncompressed EC point. If the buffer size is odd it
120+
* is assumed that the first byte is the format header `0x04` which gets ignored.
121+
* The point is then split into its X and Y coordinates and stored in
122+
* @p cose_key->x and @p cose_key->y respectively. @p cose_key->d is set to an
123+
* empty slice.
124+
*
125+
* In the case of ED25519, the function expects the key buffer to be in the
126+
* standard format for EdDSA keys and stores in @p cose_key->x.
127+
* @p cose_key->y and @p cose_key->d are set to empty slices.
128+
*
129+
* Buffers are not copied. Both @p key_info and @p cose_key point to the
130+
* same underlying buffers. No ownership transfer is implied. The buffers
131+
* must remain valid for the life time of both structures.
132+
*
133+
* [1] https://www.iana.org/assignments/cose/cose.xhtml
134+
*
135+
* @param cose_key Pointer to the COSE key structure to populate.
136+
* @param key_info Pointer to the Open DICE public key information.
137+
*/
138+
extern void n20_cwt_key_info_to_cose(n20_cose_key_t *const cose_key,
139+
n20_open_dice_public_key_info_t const *const key_info);
140+
141+
/**
142+
* @brief Render a CBOR web token (CWT) structure.
143+
*
144+
* This function encodes a CWT (RFC8392) structure into a CBOR representation
145+
* including the issuer and subject claims, the subject public key and the OpenDICE
146+
* claims as described in the OpenDICE specification [1].
147+
*
148+
* The function never fails, however, the stream needs to be checked
149+
* for buffer @ref n20_stream_has_buffer_overflow (or write position overflow
150+
* @ref n20_stream_has_write_position_overflow if the to-be-rendered size is measured)
151+
* after the call to ensure that it was rendered to the buffer successfully.
152+
*
153+
* The function is not responsible for managing the lifetime of the buffers
154+
* used by the input parameters. The caller must ensure that these buffers
155+
* remain valid for the duration of the function call.
156+
*
157+
* The function does not sanitize the input and may not produce a valid CWT.
158+
* E.g. if the key info is missing, or if the key algorithm is not supported.
159+
* the public key field will be rendered as a CBOR null value
160+
* @sa n20_cwt_key_info_to_cose.
161+
*
162+
* [1] https://pigweed.googlesource.com/open-dice/+/HEAD/docs/specification.md
163+
*
164+
* @param s The stream to write the CBOR representation to.
165+
* @param cert_info The CWT structure to render.
166+
*/
167+
extern void n20_open_dice_cwt_write(n20_stream_t *const s,
168+
n20_open_dice_cert_info_t const *const cert_info);
169+
170+
#ifdef __cplusplus
171+
}
172+
#endif

src/core/cwt.c

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/*
2+
* Copyright 2026 Aurora Operations, Inc.
3+
*
4+
* SPDX-License-Identifier: Apache-2.0 OR GPL-2.0
5+
*
6+
* This work is dual licensed.
7+
* You may use it under Apache-2.0 or GPL-2.0 at your option.
8+
*
9+
* Licensed under the Apache License, Version 2.0 (the "License");
10+
* you may not use this file except in compliance with the License.
11+
* You may obtain a copy of the License at
12+
*
13+
* http://www.apache.org/licenses/LICENSE-2.0
14+
*
15+
* Unless required by applicable law or agreed to in writing, software
16+
* distributed under the License is distributed on an "AS IS" BASIS,
17+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
* See the License for the specific language governing permissions and
19+
* limitations under the License.
20+
*
21+
* OR
22+
*
23+
* This program is free software; you can redistribute it and/or
24+
* modify it under the terms of the GNU General Public License
25+
* as published by the Free Software Foundation; either version 2
26+
* of the License, or (at your option) any later version.
27+
*
28+
* This program is distributed in the hope that it will be useful,
29+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
30+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31+
* GNU General Public License for more details.
32+
*
33+
* You should have received a copy of the GNU General Public License
34+
* along with this program; if not, see
35+
* <https://www.gnu.org/licenses/>.
36+
*/
37+
38+
#include <nat20/cbor.h>
39+
#include <nat20/cose.h>
40+
#include <nat20/crypto.h>
41+
#include <nat20/cwt.h>
42+
#include <nat20/error.h>
43+
#include <nat20/stream.h>
44+
#include <nat20/types.h>
45+
46+
static void n20_open_dice_write_name_as_hex(n20_stream_t *const s, n20_slice_t const name) {
47+
if (name.size != 0 && name.buffer == NULL) {
48+
n20_cbor_write_null(s);
49+
return;
50+
}
51+
52+
for (size_t i = 0; i < name.size; ++i) {
53+
uint8_t byte = name.buffer[name.size - (i + 1)];
54+
uint8_t hex[2] = {(byte >> 4) + '0', (byte & 0x0f) + '0'};
55+
if (hex[0] > '9') {
56+
hex[0] += 39; // Convert to 'a' - 'f'
57+
}
58+
if (hex[1] > '9') {
59+
hex[1] += 39; // Convert to 'a' - 'f'
60+
}
61+
n20_stream_prepend(s, hex, sizeof(hex));
62+
}
63+
n20_cbor_write_header(s, n20_cbor_type_string_e, name.size * 2);
64+
}
65+
66+
void n20_cwt_key_info_to_cose(n20_cose_key_t *const cose_key,
67+
n20_open_dice_public_key_info_t const *const key_info) {
68+
if (key_info == NULL || cose_key == NULL) {
69+
return;
70+
}
71+
72+
switch (key_info->algorithm) {
73+
case n20_crypto_key_type_ed25519_e:
74+
cose_key->algorithm_id = n20_cose_algorithm_id_eddsa_e;
75+
cose_key->x = key_info->key;
76+
cose_key->y = N20_SLICE_NULL;
77+
cose_key->d = N20_SLICE_NULL;
78+
return;
79+
case n20_crypto_key_type_secp256r1_e:
80+
cose_key->algorithm_id = n20_cose_algorithm_id_es256_e;
81+
break;
82+
case n20_crypto_key_type_secp384r1_e:
83+
cose_key->algorithm_id = n20_cose_algorithm_id_es384_e;
84+
break;
85+
default:
86+
/* Unsupported algorithm -> don't touch the COSE key structure. */
87+
return;
88+
}
89+
90+
/* For NIST curves P-256 and P-384 the key is split into
91+
* X and Y coordinates. */
92+
size_t coordinate_size = key_info->key.size / 2;
93+
94+
/* If the key size is odd skip the first byte to swallow
95+
* the format header. */
96+
cose_key->x.buffer = key_info->key.buffer + (key_info->key.size & 1);
97+
cose_key->x.size = coordinate_size;
98+
cose_key->y.buffer = cose_key->x.buffer + coordinate_size;
99+
cose_key->y.size = coordinate_size;
100+
cose_key->d = N20_SLICE_NULL;
101+
}
102+
103+
void n20_open_dice_cwt_write(n20_stream_t *const s, n20_open_dice_cert_info_t const *const cwt) {
104+
uint32_t pairs = 0;
105+
106+
// Write Profile Name
107+
if (cwt->open_dice_input.profile_name.size > 0) {
108+
n20_cbor_write_text_string(s, cwt->open_dice_input.profile_name);
109+
n20_cbor_write_int(s, N20_OPEN_DICE_CWT_LABEL_PROFILE);
110+
pairs++;
111+
}
112+
113+
// Write Key Usage
114+
n20_cbor_write_byte_string(
115+
s,
116+
(n20_slice_t){.buffer = (uint8_t *)cwt->key_usage,
117+
.size = (cwt->key_usage[1] != 0 ? 2 : (cwt->key_usage[0] != 0))});
118+
119+
n20_cbor_write_int(s, N20_OPEN_DICE_CWT_LABEL_KEY_USAGE);
120+
++pairs; // Key Usage
121+
122+
size_t mark = n20_stream_byte_count(s);
123+
124+
n20_cose_key_t subject_public_key = {0};
125+
n20_cose_key_ops_set(&subject_public_key.key_ops, n20_cose_key_op_verify_e);
126+
127+
n20_cwt_key_info_to_cose(&subject_public_key, &cwt->subject_public_key);
128+
129+
// Subject public key
130+
n20_cose_write_key(s, &subject_public_key);
131+
n20_cbor_write_header(s, n20_cbor_type_bytes_e, n20_stream_byte_count(s) - mark);
132+
n20_cbor_write_int(s, N20_OPEN_DICE_CWT_LABEL_SUBJECT_PUBLIC_KEY);
133+
++pairs;
134+
135+
// Convert mode to uint8_t
136+
uint8_t mode = (uint8_t)cwt->open_dice_input.mode;
137+
138+
n20_cbor_write_byte_string(s, (n20_slice_t){.buffer = &mode, .size = 1});
139+
n20_cbor_write_int(s, N20_OPEN_DICE_CWT_LABEL_MODE);
140+
++pairs;
141+
142+
if (cwt->open_dice_input.authority_descriptor.size > 0) {
143+
n20_cbor_write_byte_string(s, cwt->open_dice_input.authority_descriptor);
144+
n20_cbor_write_int(s, N20_OPEN_DICE_CWT_LABEL_AUTHORITY_DESCRIPTOR);
145+
++pairs;
146+
}
147+
148+
if (cwt->open_dice_input.authority_hash.size > 0) {
149+
n20_cbor_write_byte_string(s, cwt->open_dice_input.authority_hash);
150+
n20_cbor_write_int(s, N20_OPEN_DICE_CWT_LABEL_AUTHORITY_HASH);
151+
++pairs;
152+
}
153+
154+
if (cwt->open_dice_input.configuration_descriptor.size > 0) {
155+
n20_cbor_write_byte_string(s, cwt->open_dice_input.configuration_descriptor);
156+
n20_cbor_write_int(s, N20_OPEN_DICE_CWT_LABEL_CONFIGURATION_DESCRIPTOR);
157+
++pairs;
158+
}
159+
160+
if (cwt->open_dice_input.configuration_hash.size > 0) {
161+
n20_cbor_write_byte_string(s, cwt->open_dice_input.configuration_hash);
162+
n20_cbor_write_int(s, N20_OPEN_DICE_CWT_LABEL_CONFIGURATION_HASH);
163+
++pairs;
164+
}
165+
if (cwt->open_dice_input.code_descriptor.size > 0) {
166+
n20_cbor_write_byte_string(s, cwt->open_dice_input.code_descriptor);
167+
n20_cbor_write_int(s, N20_OPEN_DICE_CWT_LABEL_CODE_DESCRIPTOR);
168+
++pairs;
169+
}
170+
if (cwt->open_dice_input.code_hash.size > 0) {
171+
n20_cbor_write_byte_string(s, cwt->open_dice_input.code_hash);
172+
n20_cbor_write_int(s, N20_OPEN_DICE_CWT_LABEL_CODE_HASH);
173+
++pairs;
174+
}
175+
176+
if (cwt->subject.size > 0) {
177+
n20_open_dice_write_name_as_hex(s, cwt->subject);
178+
n20_cbor_write_int(s, N20_CWT_LABEL_SUBJECT);
179+
++pairs;
180+
}
181+
182+
if (cwt->issuer.size > 0) {
183+
n20_open_dice_write_name_as_hex(s, cwt->issuer);
184+
n20_cbor_write_int(s, N20_CWT_LABEL_ISSUER);
185+
++pairs;
186+
}
187+
188+
// Write the map header with the number of pairs.
189+
n20_cbor_write_map_header(s, pairs);
190+
}

0 commit comments

Comments
 (0)