1use std::collections::BTreeSet;
23use std::fmt::{self, Display, Formatter};
24use std::str::{FromStr, Utf8Error};
25
26use baid64::Baid64ParseError;
27use chrono::{DateTime, Utc};
28use fluent_uri::Uri;
29use percent_encoding::{percent_decode_str, utf8_percent_encode, AsciiSet, CONTROLS};
30use sha2::{Digest, Sha256};
31
32use crate::{InvalidSig, SsiPub, SsiSecret, SsiSig};
33
34#[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)]
35#[display(doc_comments)]
36pub enum UidParseError {
37 #[from]
38 Utf8(Utf8Error),
40 NoId(String),
42 NoSchema(String),
44}
45
46#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Display)]
47#[display("{name} <{schema}:{id}>", alt = "{name} {schema}:{id}")]
48pub struct Uid {
49 pub name: String,
50 pub schema: String,
51 pub id: String,
52}
53
54impl Uid {
55 pub fn from_url_str(s: &str) -> Result<Self, UidParseError> {
56 let s = percent_decode_str(s).decode_utf8()?.replace('+', " ");
57 Self::parse_str(&s)
58 }
59
60 fn parse_str(s: &str) -> Result<Self, UidParseError> {
61 let (name, rest) = s
62 .rsplit_once(' ')
63 .ok_or_else(|| UidParseError::NoId(s.to_string()))?;
64 let (schema, id) = rest
65 .split_once(':')
66 .ok_or_else(|| UidParseError::NoSchema(rest.to_owned()))?;
67 Ok(Self {
68 name: name.to_owned(),
69 schema: schema.to_owned(),
70 id: id.to_owned(),
71 })
72 }
73}
74
75impl FromStr for Uid {
76 type Err = UidParseError;
77
78 fn from_str(s: &str) -> Result<Self, Self::Err> { Self::parse_str(&s.replace(['<', '>'], "")) }
79}
80
81#[derive(Clone, Eq, PartialEq, Hash, Debug)]
82pub struct Ssi {
83 pub pk: SsiPub,
84 pub uids: BTreeSet<Uid>,
85 pub expiry: Option<DateTime<Utc>>,
86 pub sig: SsiSig,
87}
88
89impl Ssi {
90 pub fn new(uids: BTreeSet<Uid>, expiry: Option<DateTime<Utc>>, secret: &SsiSecret) -> Self {
91 let mut me = Self {
92 pk: secret.to_public(),
93 uids,
94 expiry,
95 sig: SsiSig([0u8; 64]),
96 };
97 me.sig = secret.sign(me.to_message());
98 me
99 }
100
101 fn to_message(&self) -> [u8; 32] {
102 let s = self.to_string();
103 let (mut s, _) = s.rsplit_once("sig=").expect("no signature");
104 s = s.trim_end_matches(&['&', '?']);
105 let msg = Sha256::digest(s);
106 Sha256::digest(msg).into()
107 }
108
109 pub fn check_integrity(&self) -> Result<(), InvalidSig> {
110 self.pk.verify(self.to_message(), self.sig)
111 }
112}
113
114#[derive(Debug, Display, Error, From)]
115#[display(doc_comments)]
116pub enum SsiParseError {
117 #[from]
118 #[display(inner)]
119 InvalidUri(fluent_uri::ParseError),
120 NoUriScheme,
122 InvalidScheme(String),
124 Unsigned,
126 InvalidQueryParam(String),
128 UnknownParam(String),
130 RepeatedExpiry,
132 RepeatedSig,
134
135 #[from]
136 InvalidUid(UidParseError),
138
139 #[from]
140 WrongSig(InvalidSig),
142
143 #[from]
144 WrongExpiry(chrono::ParseError),
146
147 InvalidPub(Baid64ParseError),
149 InvalidSig(Baid64ParseError),
151}
152
153impl FromStr for Ssi {
154 type Err = SsiParseError;
155
156 fn from_str(s: &str) -> Result<Self, Self::Err> {
157 let uri = Uri::parse(s)?;
158
159 let scheme = uri.scheme().ok_or(SsiParseError::NoUriScheme)?;
160 if scheme.as_str() != "ssi" {
161 return Err(SsiParseError::InvalidScheme(scheme.to_string()));
162 }
163
164 let pk = uri.path().as_str();
165 let pk = SsiPub::from_str(pk).map_err(SsiParseError::InvalidPub)?;
166
167 let query = uri.query().ok_or(SsiParseError::Unsigned)?.as_str();
168
169 let mut expiry = None;
170 let mut sig = None;
171 let mut uids = bset![];
172 for p in query.split('&') {
173 let (k, v) = p
174 .split_once('=')
175 .ok_or_else(|| SsiParseError::InvalidQueryParam(p.to_owned()))?;
176 match k {
177 "expiry" if expiry.is_none() => {
178 expiry = Some(DateTime::parse_from_str(v, "%Y-%m-%d")?.to_utc())
179 }
180 "expiry" => return Err(SsiParseError::RepeatedExpiry),
181 "uid" => {
182 uids.insert(Uid::from_url_str(v)?);
183 }
184 "sig" if sig.is_none() => {
185 sig = Some(SsiSig::from_str(v).map_err(SsiParseError::InvalidSig)?)
186 }
187 "sig" => return Err(SsiParseError::RepeatedSig),
188 other => return Err(SsiParseError::UnknownParam(other.to_owned())),
189 }
190 }
191
192 let Some(sig) = sig else {
193 return Err(SsiParseError::Unsigned);
194 };
195 let ssi = Self {
196 pk,
197 uids,
198 expiry,
199 sig,
200 };
201 ssi.check_integrity()?;
202
203 Ok(ssi)
204 }
205}
206
207impl Display for Ssi {
208 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
209 const SET: &AsciiSet = &CONTROLS.add(b'?').add(b'&').add(b'+').add(b'=');
210
211 write!(f, "{}?", self.pk)?;
212
213 for uid in &self.uids {
214 let uid = uid.to_string().replace(['<', '>'], "");
215 write!(f, "uid={}&", utf8_percent_encode(&uid, SET).to_string().replace(' ', "+"),)?;
216 }
217
218 if let Some(expiry) = self.expiry {
219 write!(f, "expiry={}&", expiry.format("%Y-%m-%d"))?;
220 }
221
222 write!(f, "sig={}", self.sig)?;
223
224 Ok(())
225 }
226}