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 MasterEncKeyPart = 14,
251
252 Example = 65535,
254}
255
256impl MessageCode {
257 pub fn to_bytes(&self) -> [u8; 2] {
259 u16::deserialize(serde_json::to_value(self).unwrap().into_deserializer())
261 .unwrap()
262 .to_be_bytes()
263 }
264}
265
266impl std::fmt::Display for MessageCode {
267 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
268 self.serialize(&mut *f)?; write!(f, " ({:?})", &self)
270 }
271}
272
273pub const MESSAGE_CODE_CLAIM: &str = "ph-mc";
275
276pub const CONSTELLATION_CLAIM: &str = "ph-ci";
281
282#[derive(Serialize, Deserialize, Debug, Clone)]
284pub struct ConstellationClaim {
285 #[serde(rename = "i")]
287 id: id::Id,
288
289 #[serde(rename = "c")]
291 created_at: NumericDate,
292}
293
294impl From<&Constellation> for ConstellationClaim {
295 fn from(c: &Constellation) -> Self {
296 Self {
297 id: c.id,
298 created_at: c.created_at,
299 }
300 }
301}
302
303impl ConstellationClaim {
304 pub fn compare(self, my_constellation: &Constellation) -> ConstellationCompRes {
306 match self.created_at.cmp(&my_constellation.created_at) {
307 std::cmp::Ordering::Less => ConstellationCompRes {
308 update_my_constellation: false,
309 update_constellation_claim: true,
310 },
311 std::cmp::Ordering::Greater => ConstellationCompRes {
312 update_my_constellation: true,
313 update_constellation_claim: false,
314 },
315 std::cmp::Ordering::Equal => {
316 if my_constellation.id == self.id {
317 ConstellationCompRes {
318 update_my_constellation: false,
319 update_constellation_claim: false,
320 }
321 } else {
322 ConstellationCompRes {
323 update_my_constellation: true,
324 update_constellation_claim: true,
325 }
326 }
327 }
328 }
329 }
330}
331
332#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
334pub struct ConstellationCompRes {
335 pub update_constellation_claim: bool,
338
339 pub update_my_constellation: bool,
342}
343
344impl ConstellationCompRes {
345 fn are_equal(&self) -> bool {
347 !self.update_constellation_claim && !self.update_my_constellation
348 }
349}
350
351pub trait Signable: serde::de::DeserializeOwned + serde::Serialize {
353 const CODE: MessageCode;
354
355 const CONSTELLATION_BOUND: bool = false;
360}
361
362#[macro_export]
363macro_rules! having_message_code {
364 { $tn:ty, $mc:ident } => {
365 impl $crate::api::Signable for $tn {
366 const CODE: $crate::api::MessageCode = $crate::api::MessageCode::$mc;
367 }
368 };
369}
370pub use having_message_code;
382
383#[cfg(test)]
384mod test {
385 use super::*;
386
387 #[test]
388 fn test_message_code() {
389 assert_eq!(
390 &format!("{}", MessageCode::PhcHubTicketReq),
391 "1 (PhcHubTicketReq)"
392 );
393
394 assert_eq!(MessageCode::PhcHubTicketReq.to_bytes(), [0, 1]);
395 assert_eq!(MessageCode::Example.to_bytes(), [255, 255]);
396 }
397}