Skip to main content

macula_rust/record/
procedure_advertisement.rs

1//! Procedure advertisements: a node's statement that it serves a procedure in
2//! a realm, through 1 to 8 stations (K > 1, WIRE_1_0.md section 5.3), signed
3//! by the node. A procedure with an org
4//! namespace carries its provider authorization inside the payload: the
5//! realm's org directory and the org's procedure delegation, as their wire
6//! forms (see `authorization`).
7
8use crate::cbor::Value;
9use crate::seal::{self, KEY_ID_SIZE};
10
11use super::payload::MAX_SERVING_STATIONS;
12use super::{entry, id_field, malformed, text_field, unsigned, Record, RecordError, RecordType};
13
14/// A procedure advertisement's provider authorization, as macula_record's
15/// read_authorization/1 reads it.
16#[derive(Debug, Clone, Default, PartialEq, Eq)]
17pub enum Authorization {
18    /// The advertisement carries none.
19    #[default]
20    None,
21    /// The realm's org directory and the org's procedure delegation, the one
22    /// form macula 12 has, as the records' wire forms.
23    Delegation {
24        org_directory: Vec<u8>,
25        procedure_delegation: Vec<u8>,
26    },
27    /// A map of any other fields, a certificate chain among them.
28    Unsupported,
29    /// Not a map, or an org directory and a delegation that are not both
30    /// byte strings.
31    Malformed,
32}
33
34/// A procedure advertisement's optional fields: its authorization, the
35/// provider's KEM key as carried, which the advertisement names with its id
36/// (macula 13, E2E design amendment A1), and `ttl_ms`, 0 for the default and
37/// maximum, 5 minutes.
38#[derive(Debug, Clone, Default, PartialEq, Eq)]
39pub struct ProcedureAdvertisementOptions {
40    pub authorization: Authorization,
41    pub kem_key: Option<Vec<u8>>,
42    pub ttl_ms: u64,
43}
44
45/// An unsigned advertisement, by `advertiser_node`, which signs it, of
46/// `procedure` in `realm_id`, served through `serving_stations`: 1 to 8
47/// stations, carried in ascending order of their node_ids, each once. It
48/// builds no authorization but an org directory and a procedure delegation.
49pub fn new_procedure_advertisement(
50    advertiser_node: &[u8; 32],
51    realm_id: &[u8; 32],
52    procedure: &str,
53    serving_stations: &[[u8; 32]],
54    opts: &ProcedureAdvertisementOptions,
55) -> Result<Record, RecordError> {
56    let mut stations = serving_stations.to_vec();
57    stations.sort_unstable();
58    stations.dedup();
59    if stations.is_empty() || stations.len() > MAX_SERVING_STATIONS {
60        return Err(malformed("an advertisement names 1 to 8 serving stations"));
61    }
62    let mut entries = vec![
63        entry("realm_id", Value::Bytes(realm_id.to_vec())),
64        entry("procedure", Value::text(procedure)),
65        entry("advertiser_node", Value::Bytes(advertiser_node.to_vec())),
66        entry(
67            "serving_stations",
68            Value::List(stations.iter().map(|s| Value::Bytes(s.to_vec())).collect()),
69        ),
70    ];
71    match &opts.authorization {
72        Authorization::None => {}
73        Authorization::Delegation {
74            org_directory,
75            procedure_delegation,
76        } => entries.push(entry(
77            "authorization",
78            Value::Map(vec![
79                entry("org_directory", Value::Bytes(org_directory.clone())),
80                entry(
81                    "procedure_delegation",
82                    Value::Bytes(procedure_delegation.clone()),
83                ),
84            ]),
85        )),
86        Authorization::Unsupported => return Err(RecordError::AuthorizationFormUnsupported),
87        Authorization::Malformed => return Err(malformed("an authorization in no form")),
88    }
89    if let Some(key) = &opts.kem_key {
90        if !seal::is_carried_key_size(key.len()) {
91            return Err(malformed("a KEM key of no profile's size"));
92        }
93        entries.push(entry("kem_key", Value::Bytes(key.clone())));
94        entries.push(entry(
95            "kem_key_id",
96            Value::Bytes(seal::key_id(key).to_vec()),
97        ));
98    }
99    Ok(unsigned(
100        RecordType::PROCEDURE_ADVERTISEMENT,
101        Value::Map(entries),
102        opts.ttl_ms,
103    ))
104}
105
106/// A procedure advertisement's payload. `kem_key` is the provider's KEM key
107/// as carried and its id, when the advertisement names one: a verified
108/// record's pair is well formed, the key's id its own.
109#[derive(Debug, Clone, Default, PartialEq, Eq)]
110pub struct ProcedureAdvertisement {
111    pub realm_id: [u8; 32],
112    pub procedure: String,
113    pub advertiser_node: [u8; 32],
114    pub serving_stations: Vec<[u8; 32]>,
115    pub authorization: Authorization,
116    pub kem_key: Option<(Vec<u8>, [u8; KEY_ID_SIZE])>,
117}
118
119/// Reads a procedure advertisement's payload.
120pub fn read_procedure_advertisement(r: &Record) -> Result<ProcedureAdvertisement, RecordError> {
121    if r.record_type != RecordType::PROCEDURE_ADVERTISEMENT {
122        return Err(malformed("not a procedure advertisement"));
123    }
124    let p = &r.payload;
125    Ok(ProcedureAdvertisement {
126        realm_id: id_field(p, "realm_id"),
127        procedure: text_field(p, "procedure"),
128        advertiser_node: id_field(p, "advertiser_node"),
129        serving_stations: match p.get("serving_stations") {
130            Some(Value::List(items)) => items
131                .iter()
132                .filter_map(|s| match s {
133                    Value::Bytes(b) => b.as_slice().try_into().ok(),
134                    _ => None,
135                })
136                .collect(),
137            _ => Vec::new(),
138        },
139        authorization: read_authorization(p),
140        kem_key: read_kem_key(p),
141    })
142}
143
144fn read_kem_key(payload: &Value) -> Option<(Vec<u8>, [u8; KEY_ID_SIZE])> {
145    match (payload.get("kem_key"), payload.get("kem_key_id")) {
146        (Some(Value::Bytes(key)), Some(Value::Bytes(id))) => {
147            Some((key.clone(), id.as_slice().try_into().ok()?))
148        }
149        _ => None,
150    }
151}
152
153fn read_authorization(payload: &Value) -> Authorization {
154    let Some(value) = payload.get("authorization") else {
155        return Authorization::None;
156    };
157    let Value::Map(pairs) = value else {
158        return Authorization::Malformed;
159    };
160    let (Some(directory), Some(delegation)) = (
161        value.get("org_directory"),
162        value.get("procedure_delegation"),
163    ) else {
164        return Authorization::Unsupported;
165    };
166    if pairs.len() != 2 {
167        return Authorization::Unsupported;
168    }
169    match (directory, delegation) {
170        (Value::Bytes(d), Value::Bytes(g)) => Authorization::Delegation {
171            org_directory: d.clone(),
172            procedure_delegation: g.clone(),
173        },
174        _ => Authorization::Malformed,
175    }
176}