1use serde::ser::SerializeMap;
35use serde::{Serialize, Serializer};
36
37use crate::acdc::{Acdc, AcdcError};
38use crate::error::KeriTranslationError;
39use crate::events::KERI_VERSION_PREFIX;
40use crate::said::{Protocol, compute_said_with_protocol, compute_section_said};
41use crate::types::{Prefix, Said};
42
43const KERI_VERSION_PLACEHOLDER: &str = "KERI10JSON000000_";
46
47const EXN_ILK: &str = "exn";
49
50fn recompute_version_string<T: Serialize>(event: &T) -> Result<String, IpexError> {
54 let bytes = serde_json::to_vec(event).map_err(KeriTranslationError::SerializationFailed)?;
55 Ok(format!("{KERI_VERSION_PREFIX}{:06x}_", bytes.len()))
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct IpexGrant {
69 pub v: String,
71 pub d: Said,
73 pub i: Prefix,
75 pub m: String,
77 pub recipient: Prefix,
79 pub dt: String,
81 pub acdc: Acdc,
83 pub embeds_said: Said,
85}
86
87impl IpexGrant {
88 pub const ROUTE: &'static str = "/ipex/grant";
90
91 pub fn new(
97 sender: Prefix,
98 recipient: Prefix,
99 acdc: Acdc,
100 message: impl Into<String>,
101 dt: impl Into<String>,
102 ) -> Result<Self, IpexError> {
103 let mut grant = Self {
104 v: KERI_VERSION_PLACEHOLDER.to_string(),
105 d: Said::default(),
106 i: sender,
107 m: message.into(),
108 recipient,
109 dt: dt.into(),
110 acdc,
111 embeds_said: Said::default(),
112 };
113 grant.saidify()?;
114 Ok(grant)
115 }
116
117 fn saidify(&mut self) -> Result<(), IpexError> {
121 let embeds = self.embeds_value()?;
124 self.embeds_said = compute_section_said(&embeds)?;
125
126 let body =
129 serde_json::to_value(&*self).map_err(KeriTranslationError::SerializationFailed)?;
130 self.d = compute_said_with_protocol(&body, Protocol::Keri)?;
131 self.v = recompute_version_string(&*self)?;
132 Ok(())
133 }
134
135 fn embeds_value(&self) -> Result<serde_json::Value, IpexError> {
138 let acdc =
139 serde_json::to_value(&self.acdc).map_err(KeriTranslationError::SerializationFailed)?;
140 let mut e = serde_json::Map::new();
141 e.insert("acdc".to_string(), acdc);
142 e.insert(
143 "d".to_string(),
144 serde_json::Value::String(self.embeds_said.as_str().to_string()),
145 );
146 Ok(serde_json::Value::Object(e))
147 }
148
149 pub fn parse(json: &str) -> Result<Self, IpexError> {
156 let value: serde_json::Value =
157 serde_json::from_str(json).map_err(KeriTranslationError::SerializationFailed)?;
158 let obj = value.as_object().ok_or(IpexError::NotAnObject)?;
159
160 expect_ilk(obj)?;
161 expect_route(obj, Self::ROUTE)?;
162
163 let sender = parse_prefix(obj, "i")?;
164 let dt = parse_str(obj, "dt")?;
165
166 let a = obj
167 .get("a")
168 .and_then(|v| v.as_object())
169 .ok_or(IpexError::MissingField { field: "a" })?;
170 let message = a
171 .get("m")
172 .and_then(|v| v.as_str())
173 .unwrap_or("")
174 .to_string();
175 let recipient = a
176 .get("i")
177 .and_then(|v| v.as_str())
178 .ok_or(IpexError::MissingField { field: "a.i" })
179 .and_then(|s| {
180 Prefix::new(s.to_string()).map_err(|source| IpexError::Prefix {
181 field: "a.i",
182 source,
183 })
184 })?;
185
186 let e = obj
187 .get("e")
188 .and_then(|v| v.as_object())
189 .ok_or(IpexError::MissingField { field: "e" })?;
190 let acdc_value = e
191 .get("acdc")
192 .ok_or(IpexError::MissingField { field: "e.acdc" })?;
193 let acdc: Acdc = serde_json::from_value(acdc_value.clone())
194 .map_err(KeriTranslationError::SerializationFailed)?;
195 acdc.verify_said()?;
198
199 let grant = Self::new(sender, recipient, acdc, message, dt)?;
200 verify_said(obj, &grant.d)?;
201 Ok(grant)
202 }
203}
204
205impl Serialize for IpexGrant {
206 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
207 let mut map = serializer.serialize_map(Some(11))?;
208 map.serialize_entry("v", &self.v)?;
209 map.serialize_entry("t", EXN_ILK)?;
210 map.serialize_entry("d", &self.d)?;
211 map.serialize_entry("i", &self.i)?;
212 map.serialize_entry("rp", "")?;
213 map.serialize_entry("p", "")?;
214 map.serialize_entry("dt", &self.dt)?;
215 map.serialize_entry("r", IpexGrant::ROUTE)?;
216 map.serialize_entry("q", &serde_json::Map::<String, serde_json::Value>::new())?;
217 let mut a = serde_json::Map::new();
218 a.insert("m".into(), serde_json::Value::String(self.m.clone()));
219 a.insert(
220 "i".into(),
221 serde_json::Value::String(self.recipient.to_string()),
222 );
223 map.serialize_entry("a", &serde_json::Value::Object(a))?;
224 let mut e = serde_json::Map::new();
225 e.insert(
226 "acdc".into(),
227 serde_json::to_value(&self.acdc).map_err(serde::ser::Error::custom)?,
228 );
229 e.insert(
230 "d".into(),
231 serde_json::Value::String(self.embeds_said.as_str().to_string()),
232 );
233 map.serialize_entry("e", &serde_json::Value::Object(e))?;
234 map.end()
235 }
236}
237
238#[derive(Debug, Clone, PartialEq, Eq)]
246pub struct IpexAdmit {
247 pub v: String,
249 pub d: Said,
251 pub i: Prefix,
253 pub m: String,
255 pub prior: Said,
257 pub dt: String,
259}
260
261impl IpexAdmit {
262 pub const ROUTE: &'static str = "/ipex/admit";
264
265 pub fn new(
268 sender: Prefix,
269 grant_said: Said,
270 message: impl Into<String>,
271 dt: impl Into<String>,
272 ) -> Result<Self, IpexError> {
273 let mut admit = Self {
274 v: KERI_VERSION_PLACEHOLDER.to_string(),
275 d: Said::default(),
276 i: sender,
277 m: message.into(),
278 prior: grant_said,
279 dt: dt.into(),
280 };
281 admit.saidify()?;
282 Ok(admit)
283 }
284
285 fn saidify(&mut self) -> Result<(), IpexError> {
286 let body =
287 serde_json::to_value(&*self).map_err(KeriTranslationError::SerializationFailed)?;
288 self.d = compute_said_with_protocol(&body, Protocol::Keri)?;
289 self.v = recompute_version_string(&*self)?;
290 Ok(())
291 }
292
293 pub fn parse(json: &str) -> Result<Self, IpexError> {
296 let value: serde_json::Value =
297 serde_json::from_str(json).map_err(KeriTranslationError::SerializationFailed)?;
298 let obj = value.as_object().ok_or(IpexError::NotAnObject)?;
299
300 expect_ilk(obj)?;
301 expect_route(obj, Self::ROUTE)?;
302
303 let sender = parse_prefix(obj, "i")?;
304 let dt = parse_str(obj, "dt")?;
305 let prior_str = parse_str(obj, "p")?;
306 if prior_str.is_empty() {
307 return Err(IpexError::MissingPrior);
310 }
311 let prior =
312 Said::new(prior_str).map_err(|source| IpexError::Prefix { field: "p", source })?;
313
314 let admit = Self::new(sender, prior, message_of(obj), dt)?;
315 verify_said(obj, &admit.d)?;
316 Ok(admit)
317 }
318}
319
320impl Serialize for IpexAdmit {
321 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
322 let mut map = serializer.serialize_map(Some(10))?;
323 map.serialize_entry("v", &self.v)?;
324 map.serialize_entry("t", EXN_ILK)?;
325 map.serialize_entry("d", &self.d)?;
326 map.serialize_entry("i", &self.i)?;
327 map.serialize_entry("rp", "")?;
328 map.serialize_entry("p", &self.prior)?;
329 map.serialize_entry("dt", &self.dt)?;
330 map.serialize_entry("r", IpexAdmit::ROUTE)?;
331 map.serialize_entry("q", &serde_json::Map::<String, serde_json::Value>::new())?;
332 let mut a = serde_json::Map::new();
333 a.insert("m".into(), serde_json::Value::String(self.m.clone()));
334 map.serialize_entry("a", &serde_json::Value::Object(a))?;
335 map.serialize_entry("e", &serde_json::Map::<String, serde_json::Value>::new())?;
336 map.end()
337 }
338}
339
340fn message_of(obj: &serde_json::Map<String, serde_json::Value>) -> String {
342 obj.get("a")
343 .and_then(|v| v.as_object())
344 .and_then(|a| a.get("m"))
345 .and_then(|v| v.as_str())
346 .unwrap_or("")
347 .to_string()
348}
349
350fn expect_ilk(obj: &serde_json::Map<String, serde_json::Value>) -> Result<(), IpexError> {
352 match obj.get("t").and_then(|v| v.as_str()) {
353 Some(EXN_ILK) => Ok(()),
354 other => Err(IpexError::WrongIlk(other.unwrap_or("").to_string())),
355 }
356}
357
358fn expect_route(
360 obj: &serde_json::Map<String, serde_json::Value>,
361 route: &'static str,
362) -> Result<(), IpexError> {
363 match obj.get("r").and_then(|v| v.as_str()) {
364 Some(r) if r == route => Ok(()),
365 other => Err(IpexError::WrongRoute {
366 expected: route,
367 found: other.unwrap_or("").to_string(),
368 }),
369 }
370}
371
372fn parse_str(
374 obj: &serde_json::Map<String, serde_json::Value>,
375 field: &'static str,
376) -> Result<String, IpexError> {
377 obj.get(field)
378 .and_then(|v| v.as_str())
379 .map(str::to_string)
380 .ok_or(IpexError::MissingField { field })
381}
382
383fn parse_prefix(
385 obj: &serde_json::Map<String, serde_json::Value>,
386 field: &'static str,
387) -> Result<Prefix, IpexError> {
388 let s = parse_str(obj, field)?;
389 Prefix::new(s).map_err(|source| IpexError::Prefix { field, source })
390}
391
392fn verify_said(
394 obj: &serde_json::Map<String, serde_json::Value>,
395 computed: &Said,
396) -> Result<(), IpexError> {
397 let found = obj
398 .get("d")
399 .and_then(|v| v.as_str())
400 .ok_or(IpexError::MissingField { field: "d" })?;
401 if found != computed.as_str() {
402 return Err(IpexError::SaidMismatch {
403 computed: computed.as_str().to_string(),
404 found: found.to_string(),
405 });
406 }
407 Ok(())
408}
409
410#[derive(Debug, thiserror::Error)]
412pub enum IpexError {
413 #[error("IPEX record is not a JSON object")]
415 NotAnObject,
416 #[error("IPEX record is not an exn (t = {0:?})")]
418 WrongIlk(String),
419 #[error("IPEX record route is {found:?}, expected {expected:?}")]
421 WrongRoute {
422 expected: &'static str,
424 found: String,
426 },
427 #[error("IPEX record missing field {field}")]
429 MissingField {
430 field: &'static str,
432 },
433 #[error("IPEX admit has no prior grant (p is empty)")]
435 MissingPrior,
436 #[error("invalid {field} in IPEX record: {source}")]
438 Prefix {
439 field: &'static str,
441 source: crate::types::KeriTypeError,
443 },
444 #[error("IPEX record SAID mismatch: computed {computed}, found {found}")]
446 SaidMismatch {
447 computed: String,
449 found: String,
451 },
452 #[error("IPEX embedded ACDC failed: {0}")]
454 Acdc(#[from] AcdcError),
455 #[error("KERI record build failed: {0}")]
457 Record(#[from] KeriTranslationError),
458}
459
460#[cfg(test)]
461#[allow(clippy::unwrap_used, clippy::expect_used)]
462mod tests {
463 use super::*;
464 use crate::acdc::Acdc;
465
466 const SENDER: &str = "EOoC9Auw5kgKLi0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM";
467 const RECP: &str = "EBHnCvYya3Udo4SEGo82HeOPt7WkVDEC0KWfKYnZpupF";
468 const REGISTRY: &str = "EO0_SHla5Gnzc-T3jkTNAclpA1iv1L9k3lQZw5cFOe9o";
469 const SCHEMA: &str = "EMQWEcCnVRk1hatTNyK3sIykYSrrFvafX3bHQ9Gkk1kC";
470 const DT: &str = "2024-01-01T00:00:00.000000+00:00";
471 const GRANT_SAID: &str = "EGTOcVx8ghSFYwMQT_q4YMjEzlUIh93kKfvnIzgtfgkS";
473
474 fn fixture_acdc() -> Acdc {
476 Acdc::new(
477 Prefix::new(SENDER.to_string()).unwrap(),
478 Said::new(REGISTRY.to_string()).unwrap(),
479 Said::new(SCHEMA.to_string()).unwrap(),
480 Prefix::new(RECP.to_string()).unwrap(),
481 DT.to_string(),
482 serde_json::Map::new(),
483 )
484 .saidify()
485 .unwrap()
486 }
487
488 #[test]
493 fn grant_exn_byte_exact_keripy() {
494 let grant = IpexGrant::new(
495 Prefix::new(SENDER.to_string()).unwrap(),
496 Prefix::new(RECP.to_string()).unwrap(),
497 fixture_acdc(),
498 "",
499 DT,
500 )
501 .unwrap();
502 let json = serde_json::to_string(&grant).unwrap();
503 let expected = r#"{"v":"KERI10JSON0002d4_","t":"exn","d":"EGTOcVx8ghSFYwMQT_q4YMjEzlUIh93kKfvnIzgtfgkS","i":"EOoC9Auw5kgKLi0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM","rp":"","p":"","dt":"2024-01-01T00:00:00.000000+00:00","r":"/ipex/grant","q":{},"a":{"m":"","i":"EBHnCvYya3Udo4SEGo82HeOPt7WkVDEC0KWfKYnZpupF"},"e":{"acdc":{"v":"ACDC10JSON00017a_","d":"ECK0Ep4HfnszjMpQDgovp19ioPdn1jwxGdnEtNHCN2Sy","i":"EOoC9Auw5kgKLi0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM","ri":"EO0_SHla5Gnzc-T3jkTNAclpA1iv1L9k3lQZw5cFOe9o","s":"EMQWEcCnVRk1hatTNyK3sIykYSrrFvafX3bHQ9Gkk1kC","a":{"d":"EKP_MEtpMtJfInZdMOiivHrYtz3zyObVfjDySEGxGT-V","i":"EBHnCvYya3Udo4SEGo82HeOPt7WkVDEC0KWfKYnZpupF","dt":"2024-01-01T00:00:00.000000+00:00"}},"d":"EOXgGpKt_2f6rr_JyxVwEBT1z6xbACKW0PLDhoULb0ag"}}"#;
504 assert_eq!(json, expected);
505 }
506
507 #[test]
511 fn admit_exn_byte_exact_keripy() {
512 let admit = IpexAdmit::new(
513 Prefix::new(RECP.to_string()).unwrap(),
514 Said::new(GRANT_SAID.to_string()).unwrap(),
515 "",
516 DT,
517 )
518 .unwrap();
519 let json = serde_json::to_string(&admit).unwrap();
520 let expected = r#"{"v":"KERI10JSON000119_","t":"exn","d":"EEAwH5LPMA4bkj5ceowBjGDnpe7aWW1BQ530djvBp1kv","i":"EBHnCvYya3Udo4SEGo82HeOPt7WkVDEC0KWfKYnZpupF","rp":"","p":"EGTOcVx8ghSFYwMQT_q4YMjEzlUIh93kKfvnIzgtfgkS","dt":"2024-01-01T00:00:00.000000+00:00","r":"/ipex/admit","q":{},"a":{"m":""},"e":{}}"#;
521 assert_eq!(json, expected);
522 }
523
524 #[test]
525 fn grant_round_trips_through_parse() {
526 let grant = IpexGrant::new(
527 Prefix::new(SENDER.to_string()).unwrap(),
528 Prefix::new(RECP.to_string()).unwrap(),
529 fixture_acdc(),
530 "",
531 DT,
532 )
533 .unwrap();
534 let json = serde_json::to_string(&grant).unwrap();
535 let parsed = IpexGrant::parse(&json).unwrap();
536 assert_eq!(parsed, grant);
537 assert_eq!(parsed.acdc.d, grant.acdc.d);
539 parsed.acdc.verify_said().unwrap();
540 }
541
542 #[test]
543 fn admit_round_trips_through_parse() {
544 let admit = IpexAdmit::new(
545 Prefix::new(RECP.to_string()).unwrap(),
546 Said::new(GRANT_SAID.to_string()).unwrap(),
547 "",
548 DT,
549 )
550 .unwrap();
551 let json = serde_json::to_string(&admit).unwrap();
552 let parsed = IpexAdmit::parse(&json).unwrap();
553 assert_eq!(parsed, admit);
554 }
555
556 #[test]
557 fn parse_rejects_tampered_grant_said() {
558 let grant = IpexGrant::new(
559 Prefix::new(SENDER.to_string()).unwrap(),
560 Prefix::new(RECP.to_string()).unwrap(),
561 fixture_acdc(),
562 "",
563 DT,
564 )
565 .unwrap();
566 let mut value: serde_json::Value =
567 serde_json::from_str(&serde_json::to_string(&grant).unwrap()).unwrap();
568 value["dt"] = serde_json::Value::String("2099-01-01T00:00:00.000000+00:00".into());
569 let tampered = serde_json::to_string(&value).unwrap();
570 let err = IpexGrant::parse(&tampered).unwrap_err();
571 assert!(matches!(err, IpexError::SaidMismatch { .. }));
572 }
573
574 #[test]
575 fn parse_rejects_tampered_embedded_acdc() {
576 let grant = IpexGrant::new(
577 Prefix::new(SENDER.to_string()).unwrap(),
578 Prefix::new(RECP.to_string()).unwrap(),
579 fixture_acdc(),
580 "",
581 DT,
582 )
583 .unwrap();
584 let mut value: serde_json::Value =
585 serde_json::from_str(&serde_json::to_string(&grant).unwrap()).unwrap();
586 value["e"]["acdc"]["a"]["dt"] =
588 serde_json::Value::String("2099-01-01T00:00:00.000000+00:00".into());
589 let tampered = serde_json::to_string(&value).unwrap();
590 let err = IpexGrant::parse(&tampered).unwrap_err();
591 assert!(matches!(err, IpexError::Acdc(_)));
592 }
593
594 #[test]
595 fn parse_rejects_wrong_route() {
596 let grant = IpexGrant::new(
597 Prefix::new(SENDER.to_string()).unwrap(),
598 Prefix::new(RECP.to_string()).unwrap(),
599 fixture_acdc(),
600 "",
601 DT,
602 )
603 .unwrap();
604 let json = serde_json::to_string(&grant).unwrap();
605 let err = IpexAdmit::parse(&json).unwrap_err();
607 assert!(matches!(err, IpexError::WrongRoute { .. }));
608 }
609
610 #[test]
611 fn admit_parse_rejects_missing_prior() {
612 let admit = IpexAdmit::new(
613 Prefix::new(RECP.to_string()).unwrap(),
614 Said::new(GRANT_SAID.to_string()).unwrap(),
615 "",
616 DT,
617 )
618 .unwrap();
619 let mut value: serde_json::Value =
620 serde_json::from_str(&serde_json::to_string(&admit).unwrap()).unwrap();
621 value["p"] = serde_json::Value::String(String::new());
622 let stripped = serde_json::to_string(&value).unwrap();
623 let err = IpexAdmit::parse(&stripped).unwrap_err();
624 assert!(matches!(err, IpexError::MissingPrior));
625 }
626}