1use alloc::{
32 collections::BTreeMap,
33 string::{String, ToString},
34 vec::Vec,
35};
36
37use base64::{Engine, engine::general_purpose::STANDARD};
38use serde::{Deserialize, Serialize};
39use ts_capabilityversion::CapabilityVersion;
40use ts_keys::NodePublicKey;
41
42#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(rename_all = "PascalCase")]
46pub struct TkaInitBeginRequest {
47 pub version: CapabilityVersion,
49 pub node_key: NodePublicKey,
51 #[serde(rename = "GenesisAUM", with = "marshaled_bytes")]
53 pub genesis_aum: Vec<u8>,
54}
55
56#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(rename_all = "PascalCase")]
60pub struct TkaSignInfo {
61 #[serde(rename = "NodeID")]
64 pub node_id: i64,
65 pub node_public: NodePublicKey,
67 #[serde(with = "marshaled_bytes", default)]
70 pub rotation_pubkey: Vec<u8>,
71}
72
73#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
76#[serde(rename_all = "PascalCase")]
77pub struct TkaInitBeginResponse {
78 #[serde(default)]
80 pub need_signatures: Vec<TkaSignInfo>,
81}
82
83#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
86#[serde(rename_all = "PascalCase")]
87pub struct TkaInitFinishRequest {
88 pub version: CapabilityVersion,
90 pub node_key: NodePublicKey,
92 #[serde(with = "node_id_keyed_sigs")]
96 pub signatures: BTreeMap<String, Vec<u8>>,
97 #[serde(
100 rename = "SupportDisablement",
101 with = "marshaled_bytes",
102 skip_serializing_if = "Vec::is_empty",
103 default
104 )]
105 pub support_disablement: Vec<u8>,
106}
107
108#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
111#[serde(rename_all = "PascalCase")]
112pub struct TkaInitFinishResponse {}
113
114#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
117#[serde(rename_all = "PascalCase")]
118pub struct TkaSubmitSignatureRequest {
119 pub version: CapabilityVersion,
121 pub node_key: NodePublicKey,
124 #[serde(with = "marshaled_bytes")]
127 pub signature: Vec<u8>,
128}
129
130#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
133#[serde(rename_all = "PascalCase")]
134pub struct TkaSubmitSignatureResponse {}
135
136#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
139#[serde(rename_all = "PascalCase")]
140pub struct TkaDisableRequest {
141 pub version: CapabilityVersion,
143 pub node_key: NodePublicKey,
145 pub head: String,
148 #[serde(rename = "DisablementSecret", with = "marshaled_bytes")]
150 pub disablement_secret: Vec<u8>,
151}
152
153#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
156#[serde(rename_all = "PascalCase")]
157pub struct TkaDisableResponse {}
158
159mod node_id_keyed_sigs {
163 use super::*;
164
165 pub fn serialize<S>(map: &BTreeMap<String, Vec<u8>>, serializer: S) -> Result<S::Ok, S::Error>
166 where
167 S: serde::Serializer,
168 {
169 use serde::ser::SerializeMap;
170 let mut m = serializer.serialize_map(Some(map.len()))?;
171 for (k, v) in map {
172 m.serialize_entry(k, &STANDARD.encode(v))?;
173 }
174 m.end()
175 }
176
177 pub fn deserialize<'de, D>(deserializer: D) -> Result<BTreeMap<String, Vec<u8>>, D::Error>
178 where
179 D: serde::Deserializer<'de>,
180 {
181 let raw: Option<BTreeMap<String, String>> = Option::deserialize(deserializer)?;
183 let Some(raw) = raw else {
184 return Ok(BTreeMap::new());
185 };
186 raw.into_iter()
187 .map(|(k, s)| {
188 STANDARD
189 .decode(s.as_bytes())
190 .map(|bytes| (k, bytes))
191 .map_err(|e| serde::de::Error::custom(e.to_string()))
192 })
193 .collect()
194 }
195}
196
197mod marshaled_bytes {
203 use super::*;
204
205 pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
206 where
207 S: serde::Serializer,
208 {
209 serializer.serialize_str(&STANDARD.encode(bytes))
210 }
211
212 pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
213 where
214 D: serde::Deserializer<'de>,
215 {
216 let s: Option<String> = Option::deserialize(deserializer)?;
217 let Some(s) = s else {
218 return Ok(Vec::new());
219 };
220 if s.is_empty() {
221 return Ok(Vec::new());
222 }
223 STANDARD
224 .decode(s.as_bytes())
225 .map_err(|e| serde::de::Error::custom(e.to_string()))
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 #[test]
234 fn init_begin_request_wire_shape() {
235 let req = TkaInitBeginRequest {
236 version: CapabilityVersion::CURRENT,
237 node_key: NodePublicKey::from([7u8; 32]),
238 genesis_aum: alloc::vec![1u8, 2, 3],
239 };
240 let json = serde_json::to_value(&req).unwrap();
241 assert!(json.get("Version").is_some(), "Version present");
242 assert!(
243 json.get("NodeKey")
244 .and_then(|v| v.as_str())
245 .unwrap()
246 .starts_with("nodekey:"),
247 "NodeKey is nodekey:+hex"
248 );
249 assert_eq!(
250 json.get("GenesisAUM").and_then(|v| v.as_str()).unwrap(),
251 "AQID",
252 "GenesisAUM is base64(0x01 0x02 0x03)"
253 );
254 let back: TkaInitBeginRequest = serde_json::from_value(json).unwrap();
256 assert_eq!(back, req);
257 }
258
259 #[test]
260 fn init_begin_response_need_signatures() {
261 let json = r#"{"NeedSignatures":[{"NodeID":42,"NodePublic":"nodekey:0707070707070707070707070707070707070707070707070707070707070707","RotationPubkey":"AQID"}]}"#;
262 let resp: TkaInitBeginResponse = serde_json::from_str(json).unwrap();
263 assert_eq!(resp.need_signatures.len(), 1);
264 let info = &resp.need_signatures[0];
265 assert_eq!(info.node_id, 42, "NodeID is a plain number field");
266 assert_eq!(info.rotation_pubkey, alloc::vec![1u8, 2, 3]);
267 let empty: TkaInitBeginResponse = serde_json::from_str("{}").unwrap();
269 assert!(empty.need_signatures.is_empty());
270 }
271
272 #[test]
273 fn init_finish_signatures_map_has_decimal_string_keys() {
274 let mut signatures = BTreeMap::new();
275 signatures.insert("42".to_string(), alloc::vec![0xffu8]);
276 signatures.insert("7".to_string(), alloc::vec![1u8, 2]);
277 let req = TkaInitFinishRequest {
278 version: CapabilityVersion::CURRENT,
279 node_key: NodePublicKey::from([3u8; 32]),
280 signatures,
281 support_disablement: Vec::new(),
282 };
283 let json = serde_json::to_value(&req).unwrap();
284 let sigs = json.get("Signatures").and_then(|v| v.as_object()).unwrap();
285 assert_eq!(sigs.get("42").and_then(|v| v.as_str()).unwrap(), "/w==");
287 assert_eq!(sigs.get("7").and_then(|v| v.as_str()).unwrap(), "AQI=");
288 assert!(
290 json.get("SupportDisablement").is_none(),
291 "empty SupportDisablement must be omitted"
292 );
293 let back: TkaInitFinishRequest = serde_json::from_value(json).unwrap();
294 assert_eq!(back, req);
295 }
296
297 #[test]
298 fn init_finish_support_disablement_present_when_set() {
299 let req = TkaInitFinishRequest {
300 version: CapabilityVersion::CURRENT,
301 node_key: NodePublicKey::from([3u8; 32]),
302 signatures: BTreeMap::new(),
303 support_disablement: alloc::vec![9u8, 9, 9],
304 };
305 let json = serde_json::to_value(&req).unwrap();
306 assert_eq!(
307 json.get("SupportDisablement")
308 .and_then(|v| v.as_str())
309 .unwrap(),
310 "CQkJ",
311 "SupportDisablement present + base64 when non-empty"
312 );
313 let back: TkaInitFinishRequest = serde_json::from_value(json).unwrap();
315 assert_eq!(back, req);
316 }
317
318 #[test]
319 fn init_finish_response_is_empty_object() {
320 let resp: TkaInitFinishResponse = serde_json::from_str("{}").unwrap();
321 assert_eq!(resp, TkaInitFinishResponse {});
322 assert_eq!(serde_json::to_string(&resp).unwrap(), "{}");
323 }
324
325 #[test]
326 fn submit_signature_request_roundtrips() {
327 let req = TkaSubmitSignatureRequest {
328 version: CapabilityVersion::CURRENT,
329 node_key: NodePublicKey::from([5u8; 32]),
330 signature: alloc::vec![1u8, 2, 3, 4],
331 };
332 let json = serde_json::to_value(&req).unwrap();
333 assert_eq!(
334 json.get("Signature").and_then(|v| v.as_str()).unwrap(),
335 "AQIDBA==",
336 "Signature is base64(CBOR NKS bytes)"
337 );
338 let back: TkaSubmitSignatureRequest = serde_json::from_value(json).unwrap();
339 assert_eq!(back, req);
340 }
341
342 #[test]
343 fn disable_request_head_is_plain_string_secret_is_base64() {
344 let req = TkaDisableRequest {
345 version: CapabilityVersion::CURRENT,
346 node_key: NodePublicKey::from([1u8; 32]),
347 head: "AEBAGBAF".to_string(),
348 disablement_secret: alloc::vec![0xde, 0xad],
349 };
350 let json = serde_json::to_value(&req).unwrap();
351 assert_eq!(
352 json.get("Head").and_then(|v| v.as_str()).unwrap(),
353 "AEBAGBAF",
354 "Head is the base32 text carried verbatim as a plain string"
355 );
356 assert_eq!(
357 json.get("DisablementSecret")
358 .and_then(|v| v.as_str())
359 .unwrap(),
360 "3q0=",
361 "DisablementSecret is base64(0xde 0xad)"
362 );
363 let back: TkaDisableRequest = serde_json::from_value(json).unwrap();
364 assert_eq!(back, req);
365 }
366
367 #[test]
368 fn empty_responses_serialize_as_empty_objects() {
369 assert_eq!(
370 serde_json::to_string(&TkaSubmitSignatureResponse {}).unwrap(),
371 "{}"
372 );
373 assert_eq!(serde_json::to_string(&TkaDisableResponse {}).unwrap(), "{}");
374 }
375}