1use core::marker::PhantomData;
2
3use std::fmt;
4
5use serde::de::IntoDeserializer as _;
6use serde::{Deserialize, Serialize};
7
8use crate::id;
9use crate::misc::jwt;
10use crate::servers::Constellation;
11
12use crate::api::*;
13
14#[derive(Serialize, Deserialize, Debug, Clone)]
16#[serde(transparent)]
17pub struct Signed<T> {
18 inner: jwt::JWT,
19
20 phantom: PhantomData<T>,
21}
22
23#[derive(thiserror::Error, Debug)]
25pub enum OpenError {
26 #[error("signature intended for other constellation")]
27 OtherConstellation(ConstellationCompRes),
28
29 #[error("signature expired")]
30 Expired,
31
32 #[error("invalid signature")]
33 InvalidSignature,
34
35 #[error("malformed jwt")]
36 OtherwiseInvalid,
37
38 #[error("unexpected error - consult logs")]
39 InternalError,
40}
41
42impl<T> Signed<T> {
43 pub fn open<VK: jwt::VerifyingKey>(
45 self,
46 key: &VK,
47 constellation: Option<&Constellation>,
48 ) -> std::result::Result<T, OpenError>
49 where
50 T: Signable,
51 {
52 if T::CONSTELLATION_BOUND != constellation.is_some() {
53 log::error!(
54 "internal error: a constellation must (only) be provided to Signed::open \
55 if the type T is constellation bound"
56 );
57 return Err(OpenError::InternalError);
58 }
59
60 let check_constellation = |mut claims| -> std::result::Result<jwt::Claims, OpenError> {
61 if !T::CONSTELLATION_BOUND {
62 return Ok(claims);
63 }
64
65 let Ok(Some(constellation_claim)) =
66 claims.extract::<ConstellationClaim>(CONSTELLATION_CLAIM)
67 else {
68 return Err(OpenError::OtherwiseInvalid);
69 };
70
71 let ccr = constellation_claim.compare(constellation.unwrap());
72 if ccr.are_equal() {
73 return Ok(claims);
74 }
75
76 Err(OpenError::OtherConstellation(ccr))
77 };
78
79 let claims: jwt::Claims = self.inner.open(key).map_err(|err| {
80 log::debug!(
81 "could not open signed message (of type {}): {}",
82 std::any::type_name::<T>(),
83 err
84 );
85
86 match err {
87 jwt::Error::Expired { .. } => OpenError::Expired,
88 jwt::Error::DeserializingHeader(_)
89 | jwt::Error::ClaimsNotJsonMap(_)
90 | jwt::Error::MissingDot
91 | jwt::Error::InvalidBase64(_)
92 | jwt::Error::UnexpectedAlgorithm { .. } => OpenError::OtherwiseInvalid,
93 jwt::Error::InvalidSignature { claims, .. } => {
94 match check_constellation(claims) {
95 Ok(..) => {
96 OpenError::InvalidSignature
99 }
100 Err(err) => err,
101 }
102 }
103 _ => {
104 log::error!("unexpected error opening signed message: {err}");
105 OpenError::InternalError
106 }
107 }
108 })?;
109
110 let claims = check_constellation(claims)?;
111
112 let claims = claims
114 .check_present_and(
115 MESSAGE_CODE_CLAIM,
116 |claim_name: &'static str,
117 mesg_code: MessageCode|
118 -> std::result::Result<(), jwt::Error> {
119 if mesg_code == T::CODE {
120 return Ok(());
121 }
122
123 Err(jwt::Error::InvalidClaim {
124 claim_name,
125 source: anyhow::anyhow!(
126 "expected message code {}, but got {}",
127 T::CODE,
128 mesg_code
129 ),
130 })
131 },
132 )
133 .map_err(|err| {
134 log::debug!("could not verify signed message's claims: {err}");
135 OpenError::OtherwiseInvalid
136 })?;
137
138 let res = claims.into_custom().map_err(|err| {
139 log::info!(
140 "could not parse signed message jwt into {}: {}",
141 std::any::type_name::<T>(),
142 err
143 );
144 OpenError::OtherwiseInvalid
145 })?;
146
147 Ok(res)
148 }
149
150 pub fn open_without_checking_signature(self) -> std::result::Result<T, OpenError>
153 where
154 T: Signable,
155 {
156 self.open(&jwt::IgnoreSignature, None)
157 }
158
159 pub fn new<SK: jwt::SigningKey>(
161 sk: &SK,
162 message: &T,
163 valid_for: std::time::Duration,
164 ) -> Result<Self>
165 where
166 T: Signable,
167 {
168 Self::new_opts(sk, message, valid_for, None)
169 }
170
171 pub fn new_opts<SK: jwt::SigningKey>(
173 sk: &SK,
174 message: &T,
175 valid_for: std::time::Duration,
176 constellation: Option<&Constellation>,
177 ) -> Result<Self>
178 where
179 T: Signable,
180 {
181 if T::CONSTELLATION_BOUND != constellation.is_some() {
182 log::error!(
183 "internal error: a constellation must (only) be provided to Signed::new \
184 if the type T is constellation bound"
185 );
186 return Err(ErrorCode::InternalError);
187 }
188
189 let result = || -> std::result::Result<jwt::JWT, jwt::Error> {
190 let mut claims = jwt::Claims::from_custom(message)?
191 .nbf()?
192 .exp_after(valid_for)?
193 .claim(MESSAGE_CODE_CLAIM, T::CODE)?;
194
195 if T::CONSTELLATION_BOUND {
196 let constellation = constellation.unwrap();
197
198 claims =
199 claims.claim(CONSTELLATION_CLAIM, ConstellationClaim::from(constellation))?;
200 }
201
202 claims.sign(sk)
203 }();
204
205 let jwt = match result {
206 Ok(jwt) => jwt,
207 Err(err) => {
208 log::warn!("failed to create signed message: {err}");
209 return Result::Err(ErrorCode::InternalError);
210 }
211 };
212
213 Result::Ok(Self {
214 inner: jwt,
215 phantom: PhantomData,
216 })
217 }
218
219 pub fn as_str(&self) -> &str {
220 self.inner.as_str()
221 }
222}
223
224#[non_exhaustive]
227#[repr(u16)]
228#[derive(serde_repr::Serialize_repr, serde_repr::Deserialize_repr, PartialEq, Eq, Debug)]
229pub enum MessageCode {
230 PhcHubTicketReq = 1,
233 PhcHubTicket = 2,
234 #[deprecated = "legacy; was used by phct::hub::KeyEP"]
235 PhcTHubKeyReq = 3,
236 #[deprecated = "legacy; was used by phct::hub::KeyEP"]
237 PhcTHubKeyResp = 4,
238 AdminUpdateConfigReq = 5,
239 AdminInfoReq = 6,
240 Attr = 7,
241 Ppp = 8,
242 Ehpp = 9,
243 PpNonce = 10,
244 Hhpp = 11,
245 CardPseudPackage = 12,
247 HubPing = 13,
248
249 Example = 65535,
251}
252
253impl MessageCode {
254 pub fn to_bytes(&self) -> [u8; 2] {
256 u16::deserialize(serde_json::to_value(self).unwrap().into_deserializer())
258 .unwrap()
259 .to_be_bytes()
260 }
261}
262
263impl std::fmt::Display for MessageCode {
264 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
265 self.serialize(&mut *f)?; write!(f, " ({:?})", &self)
267 }
268}
269
270pub const MESSAGE_CODE_CLAIM: &str = "ph-mc";
272
273pub const CONSTELLATION_CLAIM: &str = "ph-ci";
278
279#[derive(Serialize, Deserialize, Debug, Clone)]
281pub struct ConstellationClaim {
282 #[serde(rename = "i")]
284 id: id::Id,
285
286 #[serde(rename = "c")]
288 created_at: NumericDate,
289}
290
291impl From<&Constellation> for ConstellationClaim {
292 fn from(c: &Constellation) -> Self {
293 Self {
294 id: c.id,
295 created_at: c.created_at,
296 }
297 }
298}
299
300impl ConstellationClaim {
301 pub fn compare(self, my_constellation: &Constellation) -> ConstellationCompRes {
303 match self.created_at.cmp(&my_constellation.created_at) {
304 std::cmp::Ordering::Less => ConstellationCompRes {
305 update_my_constellation: false,
306 update_constellation_claim: true,
307 },
308 std::cmp::Ordering::Greater => ConstellationCompRes {
309 update_my_constellation: true,
310 update_constellation_claim: false,
311 },
312 std::cmp::Ordering::Equal => {
313 if my_constellation.id == self.id {
314 ConstellationCompRes {
315 update_my_constellation: false,
316 update_constellation_claim: false,
317 }
318 } else {
319 ConstellationCompRes {
320 update_my_constellation: true,
321 update_constellation_claim: true,
322 }
323 }
324 }
325 }
326 }
327}
328
329#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
331pub struct ConstellationCompRes {
332 pub update_constellation_claim: bool,
335
336 pub update_my_constellation: bool,
339}
340
341impl ConstellationCompRes {
342 fn are_equal(&self) -> bool {
344 !self.update_constellation_claim && !self.update_my_constellation
345 }
346}
347
348pub trait Signable: serde::de::DeserializeOwned + serde::Serialize {
350 const CODE: MessageCode;
351
352 const CONSTELLATION_BOUND: bool = false;
357}
358
359#[macro_export]
360macro_rules! having_message_code {
361 { $tn:ty, $mc:ident } => {
362 impl $crate::api::Signable for $tn {
363 const CODE: $crate::api::MessageCode = $crate::api::MessageCode::$mc;
364 }
365 };
366}
367pub use having_message_code;
379
380#[cfg(test)]
381mod test {
382 use super::*;
383
384 #[test]
385 fn test_message_code() {
386 assert_eq!(
387 &format!("{}", MessageCode::PhcHubTicketReq),
388 "1 (PhcHubTicketReq)"
389 );
390
391 assert_eq!(MessageCode::PhcHubTicketReq.to_bytes(), [0, 1]);
392 assert_eq!(MessageCode::Example.to_bytes(), [255, 255]);
393 }
394}