1use std::fmt;
10
11use sha2::{Digest, Sha384};
12
13use crate::cbor::{self, Value};
14use crate::node_key::{node_id_of, verify, KeyError, NodeKey};
15use crate::profile::Profile;
16
17const LABEL_BINDING_TLS: &str = "MACULA-PQ-BINDING-TLS-V1";
18const LABEL_BINDING_CONNECT: &str = "MACULA-PQ-BINDING-CONNECT-V1";
19const LABEL_STATUS: &str = "MACULA-PQ-STATUS-V1";
20const MAX_BINDING_MS: i64 = 7 * 24 * 60 * 60 * 1000;
21const MAX_STATUS_MS: i64 = 60 * 60 * 1000;
22const TOLERANCE_MS: i64 = 5 * 60 * 1000;
23const MAX_PROTOCOL_INT: i64 = 1 << 53;
24
25const BINDING_FIELDS: [&str; 9] = [
26 "label",
27 "node_id",
28 "use",
29 "subject_hash",
30 "binding_id",
31 "not_before",
32 "not_after",
33 "hash_alg",
34 "sig_alg",
35];
36const STATUS_FIELDS: [&str; 6] = [
37 "label",
38 "node_id",
39 "binding_hash",
40 "issued_at",
41 "expires_at",
42 "sig_alg",
43];
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum BindingUse {
48 Tls,
50 Connect,
52}
53
54impl BindingUse {
55 fn name(self) -> &'static str {
56 match self {
57 BindingUse::Tls => "tls",
58 BindingUse::Connect => "connect",
59 }
60 }
61
62 fn label(self) -> &'static str {
63 match self {
64 BindingUse::Tls => LABEL_BINDING_TLS,
65 BindingUse::Connect => LABEL_BINDING_CONNECT,
66 }
67 }
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
72pub enum BindingError {
73 Malformed,
77 BindingSignatureInvalid,
79 WrongUse,
81 KeyMismatch,
83 Expired,
85 NotYetValid,
87 NodeIdMismatch,
89 StatusSignatureInvalid,
91 StatusBindingMismatch,
93 StatusExpired,
95 StatusFutureDated,
97 ValidityWindow,
100 Key(KeyError),
102}
103
104impl fmt::Display for BindingError {
105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106 match self {
107 BindingError::Malformed => f.write_str("malformed signed structure"),
108 BindingError::BindingSignatureInvalid => {
109 f.write_str("the binding's signature does not verify")
110 }
111 BindingError::WrongUse => f.write_str("the binding is for another use"),
112 BindingError::KeyMismatch => f.write_str("the binding binds another key"),
113 BindingError::Expired => f.write_str("the binding has expired"),
114 BindingError::NotYetValid => f.write_str("the binding is not valid yet"),
115 BindingError::NodeIdMismatch => f.write_str("the structure names another node_id"),
116 BindingError::StatusSignatureInvalid => {
117 f.write_str("the status statement's signature does not verify")
118 }
119 BindingError::StatusBindingMismatch => {
120 f.write_str("the status statement is for another binding")
121 }
122 BindingError::StatusExpired => f.write_str("the status statement has expired"),
123 BindingError::StatusFutureDated => {
124 f.write_str("the status statement is dated in the future")
125 }
126 BindingError::ValidityWindow => {
127 f.write_str("a validity period outside what a verifier accepts")
128 }
129 BindingError::Key(e) => write!(f, "{e}"),
130 }
131 }
132}
133
134impl std::error::Error for BindingError {}
135
136impl From<KeyError> for BindingError {
137 fn from(e: KeyError) -> Self {
138 BindingError::Key(e)
139 }
140}
141
142#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct SignedTbs {
146 pub tbs: Vec<u8>,
147 pub signature: Vec<u8>,
148}
149
150impl SignedTbs {
151 pub fn to_value(&self) -> Value {
153 Value::Map(vec![
154 (Value::text("tbs"), Value::Bytes(self.tbs.clone())),
155 (
156 Value::text("signature"),
157 Value::Bytes(self.signature.clone()),
158 ),
159 ])
160 }
161
162 pub fn from_value(value: &Value) -> Result<SignedTbs, BindingError> {
165 match value {
166 Value::Map(pairs) if pairs.len() == 2 => {
167 match (value.get("tbs"), value.get("signature")) {
168 (Some(Value::Bytes(tbs)), Some(Value::Bytes(signature))) => Ok(SignedTbs {
169 tbs: tbs.clone(),
170 signature: signature.clone(),
171 }),
172 _ => Err(BindingError::Malformed),
173 }
174 }
175 _ => Err(BindingError::Malformed),
176 }
177 }
178}
179
180#[derive(Debug, Clone, PartialEq, Eq)]
182pub struct BindingInfo {
183 pub use_: BindingUse,
184 pub node_id: [u8; 32],
185 pub not_after: i64,
186}
187
188pub fn tls_binding(
192 identity_key: &NodeKey,
193 leaf_der: &[u8],
194 not_before: i64,
195 not_after: i64,
196) -> Result<SignedTbs, BindingError> {
197 issue_binding(
198 identity_key,
199 BindingUse::Tls,
200 &sha384(leaf_der),
201 not_before,
202 not_after,
203 )
204}
205
206pub fn connect_binding(
209 identity_key: &NodeKey,
210 connect_key: &[u8],
211 not_before: i64,
212 not_after: i64,
213) -> Result<SignedTbs, BindingError> {
214 issue_binding(
215 identity_key,
216 BindingUse::Connect,
217 &sha384(connect_key),
218 not_before,
219 not_after,
220 )
221}
222
223fn issue_binding(
224 identity_key: &NodeKey,
225 use_: BindingUse,
226 subject_hash: &[u8; 48],
227 not_before: i64,
228 not_after: i64,
229) -> Result<SignedTbs, BindingError> {
230 let node_id = identity_key.node_id()?;
231 if !within_window(not_before, not_after, MAX_BINDING_MS) {
232 return Err(BindingError::ValidityWindow);
233 }
234 let mut binding_id = [0u8; 16];
235 aws_lc_rs::rand::fill(&mut binding_id)
236 .map_err(|_| BindingError::Key(KeyError::RandomnessUnavailable))?;
237 let tbs = encode(Value::Map(vec![
238 text("label", use_.label()),
239 bytes("node_id", &node_id),
240 text("use", use_.name()),
241 bytes("subject_hash", subject_hash),
242 bytes("binding_id", &binding_id),
243 int("not_before", not_before),
244 int("not_after", not_after),
245 text("hash_alg", "SHA-384"),
246 text("sig_alg", identity_key.profile().sig_alg()),
247 ]));
248 sign_tbs(identity_key, use_.label(), tbs)
249}
250
251pub fn status_statement(
254 identity_key: &NodeKey,
255 binding: &SignedTbs,
256 issued_at: i64,
257 expires_at: i64,
258) -> Result<SignedTbs, BindingError> {
259 let node_id = identity_key.node_id()?;
260 if !within_window(issued_at, expires_at, MAX_STATUS_MS) {
261 return Err(BindingError::ValidityWindow);
262 }
263 let tbs = encode(Value::Map(vec![
264 text("label", LABEL_STATUS),
265 bytes("node_id", &node_id),
266 bytes("binding_hash", &sha384(&binding.tbs)),
267 int("issued_at", issued_at),
268 int("expires_at", expires_at),
269 text("sig_alg", identity_key.profile().sig_alg()),
270 ]));
271 sign_tbs(identity_key, LABEL_STATUS, tbs)
272}
273
274pub fn verify_tls_binding(
278 binding: &SignedTbs,
279 identity_key: &[u8],
280 profile: Profile,
281 leaf_der: &[u8],
282 now_ms: i64,
283) -> Result<BindingInfo, BindingError> {
284 verify_binding(
285 binding,
286 identity_key,
287 profile,
288 BindingUse::Tls,
289 &sha384(leaf_der),
290 now_ms,
291 )
292}
293
294pub fn verify_connect_binding(
297 binding: &SignedTbs,
298 identity_key: &[u8],
299 profile: Profile,
300 connect_key: &[u8],
301 now_ms: i64,
302) -> Result<BindingInfo, BindingError> {
303 verify_binding(
304 binding,
305 identity_key,
306 profile,
307 BindingUse::Connect,
308 &sha384(connect_key),
309 now_ms,
310 )
311}
312
313fn verify_binding(
317 binding: &SignedTbs,
318 identity_key: &[u8],
319 profile: Profile,
320 use_: BindingUse,
321 subject_hash: &[u8; 48],
322 now_ms: i64,
323) -> Result<BindingInfo, BindingError> {
324 if !verify(
325 &labelled(use_.label(), &binding.tbs),
326 &binding.signature,
327 identity_key,
328 profile,
329 ) {
330 return Err(BindingError::BindingSignatureInvalid);
331 }
332 let fields = decode_tbs(&binding.tbs, &BINDING_FIELDS).ok_or(BindingError::Malformed)?;
333 let parsed = well_formed_binding(&fields, profile).ok_or(BindingError::Malformed)?;
334 if parsed.label != use_.label() || parsed.use_ != use_.name() {
335 return Err(BindingError::WrongUse);
336 }
337 if parsed.node_id != node_id_of(identity_key, profile) {
338 return Err(BindingError::NodeIdMismatch);
339 }
340 if &parsed.subject_hash != subject_hash {
341 return Err(BindingError::KeyMismatch);
342 }
343 if now_ms + TOLERANCE_MS < parsed.not_before {
344 return Err(BindingError::NotYetValid);
345 }
346 if now_ms - TOLERANCE_MS > parsed.not_after {
347 return Err(BindingError::Expired);
348 }
349 Ok(BindingInfo {
350 use_,
351 node_id: parsed.node_id,
352 not_after: parsed.not_after,
353 })
354}
355
356struct BindingTbs<'a> {
357 label: &'a str,
358 use_: &'a str,
359 node_id: [u8; 32],
360 subject_hash: [u8; 48],
361 not_before: i64,
362 not_after: i64,
363}
364
365fn well_formed_binding<'a>(f: &'a Fields, profile: Profile) -> Option<BindingTbs<'a>> {
366 let binding_id = field_bytes(f, "binding_id")?;
367 let not_before = protocol_int(f, "not_before")?;
368 let not_after = protocol_int(f, "not_after")?;
369 let well_formed = binding_id.len() == 16
370 && within_window(not_before, not_after, MAX_BINDING_MS)
371 && field_text(f, "hash_alg")? == "SHA-384"
372 && field_text(f, "sig_alg")? == profile.sig_alg();
373 well_formed.then_some(BindingTbs {
374 label: field_text(f, "label")?,
375 use_: field_text(f, "use")?,
376 node_id: field_array(f, "node_id")?,
377 subject_hash: field_array(f, "subject_hash")?,
378 not_before,
379 not_after,
380 })
381}
382
383pub fn verify_status(
389 statement: &SignedTbs,
390 binding: &SignedTbs,
391 identity_key: &[u8],
392 profile: Profile,
393 now_ms: i64,
394) -> Result<i64, BindingError> {
395 if !verify(
396 &labelled(LABEL_STATUS, &statement.tbs),
397 &statement.signature,
398 identity_key,
399 profile,
400 ) {
401 return Err(BindingError::StatusSignatureInvalid);
402 }
403 let fields = decode_tbs(&statement.tbs, &STATUS_FIELDS).ok_or(BindingError::Malformed)?;
404 let issued_at = protocol_int(&fields, "issued_at").ok_or(BindingError::Malformed)?;
405 let expires_at = protocol_int(&fields, "expires_at").ok_or(BindingError::Malformed)?;
406 let node_id: [u8; 32] = field_array(&fields, "node_id").ok_or(BindingError::Malformed)?;
407 let binding_hash: [u8; 48] =
408 field_array(&fields, "binding_hash").ok_or(BindingError::Malformed)?;
409 let well_formed = field_text(&fields, "label") == Some(LABEL_STATUS)
410 && within_window(issued_at, expires_at, MAX_STATUS_MS)
411 && field_text(&fields, "sig_alg") == Some(profile.sig_alg());
412 if !well_formed {
413 return Err(BindingError::Malformed);
414 }
415 if node_id != node_id_of(identity_key, profile) {
416 return Err(BindingError::NodeIdMismatch);
417 }
418 if binding_hash != sha384(&binding.tbs) {
419 return Err(BindingError::StatusBindingMismatch);
420 }
421 if issued_at > now_ms + TOLERANCE_MS {
422 return Err(BindingError::StatusFutureDated);
423 }
424 if now_ms - TOLERANCE_MS > expires_at {
425 return Err(BindingError::StatusExpired);
426 }
427 Ok(expires_at)
428}
429
430type Fields = std::collections::HashMap<String, Value>;
432
433fn decode_tbs(tbs: &[u8], names: &[&str]) -> Option<Fields> {
436 let Value::Map(pairs) = cbor::decode(tbs).ok()? else {
437 return None;
438 };
439 if pairs.len() != names.len() {
440 return None;
441 }
442 let mut fields = Fields::with_capacity(pairs.len());
443 for (key, value) in pairs {
444 let Value::Text(name) = key else {
445 return None;
446 };
447 fields.insert(name, value);
448 }
449 names
450 .iter()
451 .all(|n| fields.contains_key(*n))
452 .then_some(fields)
453}
454
455fn within_window(from: i64, to: i64, max: i64) -> bool {
459 from >= 0 && from <= to && to < MAX_PROTOCOL_INT && to - from <= max
460}
461
462fn protocol_int(f: &Fields, name: &str) -> Option<i64> {
463 match f.get(name)? {
464 Value::Int(n) => i64::try_from(*n).ok(),
465 _ => None,
466 }
467}
468
469fn field_text<'a>(f: &'a Fields, name: &str) -> Option<&'a str> {
470 match f.get(name)? {
471 Value::Text(t) => Some(t),
472 _ => None,
473 }
474}
475
476fn field_bytes<'a>(f: &'a Fields, name: &str) -> Option<&'a [u8]> {
477 match f.get(name)? {
478 Value::Bytes(b) => Some(b),
479 _ => None,
480 }
481}
482
483fn field_array<const N: usize>(f: &Fields, name: &str) -> Option<[u8; N]> {
484 field_bytes(f, name)?.try_into().ok()
485}
486
487fn sign_tbs(key: &NodeKey, label: &str, tbs: Vec<u8>) -> Result<SignedTbs, BindingError> {
488 let signature = key.sign(&labelled(label, &tbs))?;
489 Ok(SignedTbs { tbs, signature })
490}
491
492fn labelled(label: &str, tbs: &[u8]) -> Vec<u8> {
494 let mut out = Vec::with_capacity(label.len() + 1 + tbs.len());
495 out.extend_from_slice(label.as_bytes());
496 out.push(0);
497 out.extend_from_slice(tbs);
498 out
499}
500
501fn sha384(bytes: &[u8]) -> [u8; 48] {
502 Sha384::digest(bytes).into()
503}
504
505fn encode(value: Value) -> Vec<u8> {
508 cbor::encode(&value).expect("text-keyed fields of i64 integers always encode")
509}
510
511fn text(name: &str, value: &str) -> (Value, Value) {
512 (Value::text(name), Value::text(value))
513}
514
515fn bytes(name: &str, value: &[u8]) -> (Value, Value) {
516 (Value::text(name), Value::Bytes(value.to_vec()))
517}
518
519fn int(name: &str, value: i64) -> (Value, Value) {
520 (Value::text(name), Value::Int(i128::from(value)))
521}