Skip to main content

ttk_server/attestation/
eat.rs

1//! RFC 9711 Entity Attestation Token (EAT) data model.
2//!
3//! Defines the full set of IANA-registered EAT claim keys ([`EatClaimKey`]) and a
4//! strongly-typed claims-set structure ([`EatClaimsSet`]) that can be serialised to
5//! CBOR.  Nitro-enclave-specific wrapping logic lives in [`crate::attestation::nitro_doc`].
6use ciborium::value::Value;
7
8// ---------------------------------------------------------------------------
9// IANA-registered EAT/CWT claim keys (RFC 9711)
10// https://www.iana.org/assignments/cwt
11// ---------------------------------------------------------------------------
12
13/// CWT / EAT claim key integers as registered by RFC 9711.
14///
15/// Standard JWT/CWT claims that predate EAT (e.g., `iat = 6`) are included
16/// for completeness. The RFC 9711-specific claims start at key 10 (Nonce)
17/// and run through 275 (Intended Use).
18#[allow(dead_code)]
19#[repr(i64)]
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum EatClaimKey {
22    // ---- Standard CWT claims used by EAT ----
23    /// Issued-At (seconds since epoch).  CWT key 6.
24    Iat = 6,
25
26    // ---- RFC 9711 claims ----
27    /// Nonce – `eat_nonce`.  Key 10.  Value: bstr or array.
28    Nonce = 10,
29    /// Universal Entity ID – `ueid`.  Key 256.  Value: bstr.
30    Ueid = 256,
31    /// Semipermanent UEIDs – `sueids`.  Key 257.  Value: map.
32    Sueids = 257,
33    /// Hardware OEM ID – `oemid`.  Key 258.  Value: bstr or int.
34    OemId = 258,
35    /// Hardware Model – `hwmodel`.  Key 259.  Value: bstr.
36    HwModel = 259,
37    /// Hardware Version – `hwversion`.  Key 260.  Value: array.
38    HwVersion = 260,
39    /// Uptime – `uptime`.  Key 261.  Value: uint.
40    Uptime = 261,
41    /// OEM Authorized Boot – `oemboot`.  Key 262.  Value: bool.
42    OemBoot = 262,
43    /// Debug Status – `dbgstat`.  Key 263.  Value: uint.
44    DbgStat = 263,
45    /// Location – `location`.  Key 264.  Value: map.
46    Location = 264,
47    /// EAT Profile – `eat_profile`.  Key 265.  Value: uri or oid.
48    EatProfile = 265,
49    /// Submodules Section – `submods`.  Key 266.  Value: map.
50    Submods = 266,
51    /// Boot Count – `bootcount`.  Key 267.  Value: uint.
52    BootCount = 267,
53    /// Boot Seed – `bootseed`.  Key 268.  Value: bstr.
54    BootSeed = 268,
55    /// DLOAs (Digital Letters of Approval) – `dloas`.  Key 269.  Value: array.
56    Dloas = 269,
57    /// Software Name – `swname`.  Key 270.  Value: tstr.
58    SwName = 270,
59    /// Software Version – `swversion`.  Key 271.  Value: array.
60    SwVersion = 271,
61    /// Software Manifests – `manifests`.  Key 272.  Value: array.
62    Manifests = 272,
63    /// Measurements – `measurements`.  Key 273.  Value: array.
64    Measurements = 273,
65    /// Software Measurement Results – `measres`.  Key 274.  Value: array.
66    MeasRes = 274,
67    /// Intended Use – `intuse`.  Key 275.  Value: uint.
68    IntUse = 275,
69}
70
71/// Converts a claim key into its integer CBOR map key.
72impl From<EatClaimKey> for i64 {
73    /// Returns the numeric key of `k`.
74    fn from(k: EatClaimKey) -> i64 {
75        k as i64
76    }
77}
78
79// ---------------------------------------------------------------------------
80// RFC 9711 EAT claims-set structure
81// ---------------------------------------------------------------------------
82
83/// Strongly-typed representation of an RFC 9711 EAT claims-set.
84///
85/// Every field is `Option<…>` so that only the claims relevant to a
86/// particular token need to be populated.  Fields whose CBOR value type
87/// is composite (map / array) are left as raw `ciborium::value::Value` so
88/// that callers can construct them without an additional indirection layer.
89///
90/// # Claim → CBOR key mapping
91///
92/// | Field           | JWT name      | CWT key |
93/// |-----------------|---------------|---------|
94/// | `iat`           | —             | 6       |
95/// | `nonce`         | eat_nonce     | 10      |
96/// | `ueid`          | ueid          | 256     |
97/// | `sueids`        | sueids        | 257     |
98/// | `oem_id`        | oemid         | 258     |
99/// | `hw_model`      | hwmodel       | 259     |
100/// | `hw_version`    | hwversion     | 260     |
101/// | `uptime`        | uptime        | 261     |
102/// | `oem_boot`      | oemboot       | 262     |
103/// | `dbg_stat`      | dbgstat       | 263     |
104/// | `location`      | location      | 264     |
105/// | `eat_profile`   | eat_profile   | 265     |
106/// | `submods`       | submods       | 266     |
107/// | `boot_count`    | bootcount     | 267     |
108/// | `boot_seed`     | bootseed      | 268     |
109/// | `dloas`         | dloas         | 269     |
110/// | `sw_name`       | swname        | 270     |
111/// | `sw_version`    | swversion     | 271     |
112/// | `manifests`     | manifests     | 272     |
113/// | `measurements`  | measurements  | 273     |
114/// | `meas_res`      | measres       | 274     |
115/// | `int_use`       | intuse        | 275     |
116#[derive(Clone, Debug, Default)]
117pub struct EatClaimsSet {
118    // ---- Standard CWT ----
119    /// Issued-At: seconds since POSIX epoch (CWT key 6).
120    pub iat: Option<i64>,
121
122    // ---- RFC 9711 ----
123    /// Nonce – `eat_nonce` (key 10).
124    /// CBOR value: bstr or array-of-bstr.
125    pub nonce: Option<Value>,
126
127    /// Universal Entity ID – `ueid` (key 256).
128    /// A byte string whose first byte encodes the UEID type
129    /// (0x01 = RAND, 0x02 = IEEE EUI, 0x03 = IMEI, …).
130    pub ueid: Option<Vec<u8>>,
131
132    /// Semipermanent UEIDs – `sueids` (key 257).
133    /// CBOR value: map of label → bstr.
134    pub sueids: Option<Value>,
135
136    /// Hardware OEM ID – `oemid` (key 258).
137    /// CBOR value: bstr or int.
138    pub oem_id: Option<Value>,
139
140    /// Hardware Model – `hwmodel` (key 259).
141    /// A byte string that uniquely identifies the model within an OEM.
142    pub hw_model: Option<Vec<u8>>,
143
144    /// Hardware Version – `hwversion` (key 260).
145    /// CBOR value: array \[version-string, scheme\].
146    pub hw_version: Option<Value>,
147
148    /// Uptime in seconds – `uptime` (key 261).
149    pub uptime: Option<u64>,
150
151    /// OEM Authorized Boot indicator – `oemboot` (key 262).
152    /// `true` when the booted software is OEM-authorized.
153    pub oem_boot: Option<bool>,
154
155    /// Debug Status – `dbgstat` (key 263).
156    /// Encoded as a uint; see RFC 9711 Section 4.2.8 for the registry.
157    pub dbg_stat: Option<u64>,
158
159    /// Geographic Location – `location` (key 264).
160    /// CBOR value: map with optional latitude, longitude, altitude, …
161    pub location: Option<Value>,
162
163    /// EAT Profile – `eat_profile` (key 265).
164    /// A URI or OID identifying the profile this token conforms to.
165    pub eat_profile: Option<String>,
166
167    /// Submodules Section – `submods` (key 266).
168    /// CBOR value: map of submodule-name → nested-token or claims-set.
169    pub submods: Option<Value>,
170
171    /// Boot Count – `bootcount` (key 267).
172    pub boot_count: Option<u64>,
173
174    /// Boot Seed – `bootseed` (key 268).
175    /// A byte string that changes on every boot; ties measurements to a
176    /// single boot cycle.
177    pub boot_seed: Option<Vec<u8>>,
178
179    /// DLOAs – `dloas` (key 269).
180    /// CBOR value: array of DLOA maps (Digital Letters of Approval).
181    pub dloas: Option<Value>,
182
183    /// Software Name – `swname` (key 270).
184    pub sw_name: Option<String>,
185
186    /// Software Version – `swversion` (key 271).
187    /// CBOR value: array \[version-string, scheme\].
188    pub sw_version: Option<Value>,
189
190    /// Software Manifests – `manifests` (key 272).
191    /// CBOR value: array of CoSWID or similar manifest structures.
192    pub manifests: Option<Value>,
193
194    /// Measurements – `measurements` (key 273).
195    /// CBOR value: array of measurement maps.
196    pub measurements: Option<Value>,
197
198    /// Software Measurement Results – `measres` (key 274).
199    /// CBOR value: array of measurement-result maps.
200    pub meas_res: Option<Value>,
201
202    /// Intended Use – `intuse` (key 275).
203    /// Encoded as a uint; see RFC 9711 Section 4.2.20 for the registry.
204    pub int_use: Option<u64>,
205}
206
207/// CBOR encoding and decoding of the claims-set.
208impl EatClaimsSet {
209    /// Serialise the populated fields into a CBOR map (`Value::Map`).
210    ///
211    /// Only `Some(…)` fields are emitted; absent claims are omitted from
212    /// the encoding, consistent with RFC 9711's optional-claim model.
213    pub fn to_cbor_value(&self) -> Value {
214        let mut map: Vec<(Value, Value)> = Vec::new();
215
216        macro_rules! push_int {
217            ($key:expr, $val:expr) => {
218                if let Some(v) = $val {
219                    map.push((
220                        Value::Integer(($key as i64).into()),
221                        Value::Integer((v as i64).into()),
222                    ));
223                }
224            };
225        }
226        macro_rules! push_bool {
227            ($key:expr, $val:expr) => {
228                if let Some(v) = $val {
229                    map.push((Value::Integer(($key as i64).into()), Value::Bool(v)));
230                }
231            };
232        }
233        macro_rules! push_bytes {
234            ($key:expr, $val:expr) => {
235                if let Some(ref v) = $val {
236                    map.push((
237                        Value::Integer(($key as i64).into()),
238                        Value::Bytes(v.clone()),
239                    ));
240                }
241            };
242        }
243        macro_rules! push_text {
244            ($key:expr, $val:expr) => {
245                if let Some(ref v) = $val {
246                    map.push((Value::Integer(($key as i64).into()), Value::Text(v.clone())));
247                }
248            };
249        }
250        macro_rules! push_raw {
251            ($key:expr, $val:expr) => {
252                if let Some(ref v) = $val {
253                    map.push((Value::Integer(($key as i64).into()), v.clone()));
254                }
255            };
256        }
257
258        push_int!(EatClaimKey::Iat, self.iat);
259        push_raw!(EatClaimKey::Nonce, self.nonce);
260        push_bytes!(EatClaimKey::Ueid, self.ueid);
261        push_raw!(EatClaimKey::Sueids, self.sueids);
262        push_raw!(EatClaimKey::OemId, self.oem_id);
263        push_bytes!(EatClaimKey::HwModel, self.hw_model);
264        push_raw!(EatClaimKey::HwVersion, self.hw_version);
265        push_int!(EatClaimKey::Uptime, self.uptime);
266        push_bool!(EatClaimKey::OemBoot, self.oem_boot);
267        push_int!(EatClaimKey::DbgStat, self.dbg_stat);
268        push_raw!(EatClaimKey::Location, self.location);
269        push_text!(EatClaimKey::EatProfile, self.eat_profile);
270        push_raw!(EatClaimKey::Submods, self.submods);
271        push_int!(EatClaimKey::BootCount, self.boot_count);
272        push_bytes!(EatClaimKey::BootSeed, self.boot_seed);
273        push_raw!(EatClaimKey::Dloas, self.dloas);
274        push_text!(EatClaimKey::SwName, self.sw_name);
275        push_raw!(EatClaimKey::SwVersion, self.sw_version);
276        push_raw!(EatClaimKey::Manifests, self.manifests);
277        push_raw!(EatClaimKey::Measurements, self.measurements);
278        push_raw!(EatClaimKey::MeasRes, self.meas_res);
279        push_int!(EatClaimKey::IntUse, self.int_use);
280
281        Value::Map(map)
282    }
283
284    /// Encode the claims-set to raw CBOR bytes.
285    pub fn to_cbor_bytes(&self) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
286        let mut out = Vec::new();
287        ciborium::ser::into_writer(&self.to_cbor_value(), &mut out)?;
288        Ok(out)
289    }
290
291    /// Parse from a CBOR `Value::Map` representation into [`EatClaimsSet`].
292    pub fn from_cbor_value(value: &Value) -> Result<Self, Box<dyn std::error::Error>> {
293        let map = match value {
294            Value::Map(m) => m,
295            _ => return Err("Expected CBOR Map for EatClaimsSet".into()),
296        };
297
298        let mut claims = EatClaimsSet::default();
299
300        for (k, v) in map {
301            let key_int = match k {
302                Value::Integer(i) => i128::from(*i) as i64,
303                _ => continue,
304            };
305
306            match key_int {
307                x if x == EatClaimKey::Iat as i64 => {
308                    if let Value::Integer(i) = v {
309                        claims.iat = Some(i128::from(*i) as i64);
310                    }
311                }
312                x if x == EatClaimKey::Nonce as i64 => {
313                    claims.nonce = Some(v.clone());
314                }
315                x if x == EatClaimKey::Ueid as i64 => {
316                    if let Value::Bytes(b) = v {
317                        claims.ueid = Some(b.clone());
318                    }
319                }
320                x if x == EatClaimKey::Sueids as i64 => {
321                    claims.sueids = Some(v.clone());
322                }
323                x if x == EatClaimKey::OemId as i64 => {
324                    claims.oem_id = Some(v.clone());
325                }
326                x if x == EatClaimKey::HwModel as i64 => {
327                    if let Value::Bytes(b) = v {
328                        claims.hw_model = Some(b.clone());
329                    }
330                }
331                x if x == EatClaimKey::HwVersion as i64 => {
332                    claims.hw_version = Some(v.clone());
333                }
334                x if x == EatClaimKey::Uptime as i64 => {
335                    if let Value::Integer(i) = v {
336                        claims.uptime = Some(i128::from(*i) as u64);
337                    }
338                }
339                x if x == EatClaimKey::OemBoot as i64 => {
340                    if let Value::Bool(b) = v {
341                        claims.oem_boot = Some(*b);
342                    }
343                }
344                x if x == EatClaimKey::DbgStat as i64 => {
345                    if let Value::Integer(i) = v {
346                        claims.dbg_stat = Some(i128::from(*i) as u64);
347                    }
348                }
349                x if x == EatClaimKey::Location as i64 => {
350                    claims.location = Some(v.clone());
351                }
352                x if x == EatClaimKey::EatProfile as i64 => {
353                    if let Value::Text(s) = v {
354                        claims.eat_profile = Some(s.clone());
355                    }
356                }
357                x if x == EatClaimKey::Submods as i64 => {
358                    claims.submods = Some(v.clone());
359                }
360                x if x == EatClaimKey::BootCount as i64 => {
361                    if let Value::Integer(i) = v {
362                        claims.boot_count = Some(i128::from(*i) as u64);
363                    }
364                }
365                x if x == EatClaimKey::BootSeed as i64 => {
366                    if let Value::Bytes(b) = v {
367                        claims.boot_seed = Some(b.clone());
368                    }
369                }
370                x if x == EatClaimKey::Dloas as i64 => {
371                    claims.dloas = Some(v.clone());
372                }
373                x if x == EatClaimKey::SwName as i64 => {
374                    if let Value::Text(s) = v {
375                        claims.sw_name = Some(s.clone());
376                    }
377                }
378                x if x == EatClaimKey::SwVersion as i64 => {
379                    claims.sw_version = Some(v.clone());
380                }
381                x if x == EatClaimKey::Manifests as i64 => {
382                    claims.manifests = Some(v.clone());
383                }
384                x if x == EatClaimKey::Measurements as i64 => {
385                    claims.measurements = Some(v.clone());
386                }
387                x if x == EatClaimKey::MeasRes as i64 => {
388                    claims.meas_res = Some(v.clone());
389                }
390                x if x == EatClaimKey::IntUse as i64 => {
391                    if let Value::Integer(i) = v {
392                        claims.int_use = Some(i128::from(*i) as u64);
393                    }
394                }
395                _ => {}
396            }
397        }
398
399        Ok(claims)
400    }
401
402    /// Decode the claims-set from raw CBOR bytes.
403    pub fn from_cbor_bytes(bytes: &[u8]) -> Result<Self, Box<dyn std::error::Error>> {
404        let value: Value = ciborium::de::from_reader(bytes)?;
405        Self::from_cbor_value(&value)
406    }
407
408    /// Alias for [`Self::from_cbor_bytes`].
409    pub fn from_bytes(bytes: &[u8]) -> Result<Self, Box<dyn std::error::Error>> {
410        Self::from_cbor_bytes(bytes)
411    }
412}