ssi_status/impl/token_status_list/
json.rs

1//! JWT encoding of a Status Lists.
2use std::borrow::Cow;
3
4use base64::prelude::{Engine, BASE64_URL_SAFE};
5use flate2::Compression;
6use iref::UriBuf;
7use serde::{Deserialize, Serialize};
8use ssi_claims_core::ValidateClaims;
9use ssi_jws::ValidateJwsHeader;
10use ssi_jwt::{
11    match_claim_type, AnyClaims, Claim, ClaimSet, InvalidClaimValue, IssuedAt, Issuer, JWTClaims,
12    Subject,
13};
14
15use crate::{
16    token_status_list::{BitString, StatusSize},
17    StatusMapEntry, StatusMapEntrySet,
18};
19
20use super::{DecodeError, StatusList};
21
22/// Status List JWT.
23///
24/// See: <https://www.ietf.org/archive/id/draft-ietf-oauth-status-list-02.html#name-status-list-token>
25pub type StatusListJwt = JWTClaims<StatusListJwtPrivateClaims>;
26
27pub fn decode_status_list_jwt(mut claims: impl ClaimSet) -> Result<StatusList, DecodeError> {
28    let _ = claims
29        .try_remove::<Issuer>()
30        .map_err(DecodeError::claim)?
31        .ok_or(DecodeError::MissingIssuer)?;
32
33    let _ = claims
34        .try_remove::<Subject>()
35        .map_err(DecodeError::claim)?
36        .ok_or(DecodeError::MissingSubject)?;
37
38    let _ = claims
39        .try_remove::<IssuedAt>()
40        .map_err(DecodeError::claim)?
41        .ok_or(DecodeError::MissingSubject)?;
42    let ttl = claims
43        .try_remove()
44        .map_err(DecodeError::claim)?
45        .map(TimeToLiveClaim::unwrap);
46
47    let bit_string = claims
48        .try_remove::<JsonStatusList>()
49        .map_err(DecodeError::claim)?
50        .ok_or(DecodeError::MissingStatusList)?
51        .decode(None)?;
52
53    Ok(StatusList::new(bit_string, ttl))
54}
55
56/// Status List JWT private claims.
57///
58/// This includes status list specific claims such as `ttl` and `status_list`,
59/// but also all the extra claims unrelated to status lists.
60///
61/// See: <https://www.ietf.org/archive/id/draft-ietf-oauth-status-list-02.html#name-status-list-token>
62#[derive(Serialize, Deserialize)]
63pub struct StatusListJwtPrivateClaims {
64    /// Time to live.
65    ///
66    /// Maximum amount of time, in seconds, that the Status List Token can be
67    /// cached by a consumer before a fresh copy *should* be retrieved. The
68    /// value of the claim *must* be a positive number.
69    #[serde(rename = "ttl")]
70    pub time_to_live: Option<TimeToLiveClaim>,
71
72    /// Status list.
73    pub status_list: Option<JsonStatusList>,
74
75    /// Other claims.
76    #[serde(flatten)]
77    pub other_claims: AnyClaims,
78}
79
80impl ClaimSet for StatusListJwtPrivateClaims {
81    fn contains<C: Claim>(&self) -> bool {
82        match_claim_type! {
83            match C {
84                TimeToLiveClaim => self.time_to_live.is_some(),
85                JsonStatusList => self.status_list.is_some(),
86                _ => ClaimSet::contains::<C>(&self.other_claims)
87            }
88        }
89    }
90
91    fn try_get<C: Claim>(&self) -> Result<Option<Cow<C>>, InvalidClaimValue> {
92        match_claim_type! {
93            match C {
94                TimeToLiveClaim => {
95                    Ok(self.time_to_live.as_ref().map(Cow::Borrowed))
96                },
97                JsonStatusList => {
98                    Ok(self.status_list.as_ref().map(Cow::Borrowed))
99                },
100                _ => {
101                    self.other_claims.try_get()
102                }
103            }
104        }
105    }
106
107    fn try_set<C: Claim>(&mut self, claim: C) -> Result<Result<(), C>, InvalidClaimValue> {
108        match_claim_type! {
109            match claim: C {
110                TimeToLiveClaim => {
111                    self.time_to_live = Some(claim);
112                    Ok(Ok(()))
113                },
114                JsonStatusList => {
115                    self.status_list = Some(claim);
116                    Ok(Ok(()))
117                },
118                _ => {
119                    self.other_claims.try_set(claim)
120                }
121            }
122        }
123    }
124
125    fn try_remove<C: Claim>(&mut self) -> Result<Option<C>, InvalidClaimValue> {
126        match_claim_type! {
127            match C {
128                TimeToLiveClaim => {
129                    Ok(self.time_to_live.take())
130                },
131                JsonStatusList => {
132                    Ok(self.status_list.take())
133                },
134                _ => {
135                    self.other_claims.try_remove()
136                }
137            }
138        }
139    }
140}
141
142impl<E, P> ValidateClaims<E, P> for StatusListJwtPrivateClaims {
143    fn validate_claims(&self, _env: &E, _proof: &P) -> ssi_claims_core::ClaimsValidity {
144        Ok(())
145    }
146}
147
148impl<E> ValidateJwsHeader<E> for StatusListJwtPrivateClaims {
149    fn validate_jws_header(
150        &self,
151        _env: &E,
152        _header: &ssi_jws::Header,
153    ) -> ssi_claims_core::ClaimsValidity {
154        Ok(())
155    }
156}
157
158/// Time to live JWT claim.
159#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
160#[serde(transparent)]
161pub struct TimeToLiveClaim(pub u64);
162
163impl TimeToLiveClaim {
164    pub fn unwrap(self) -> u64 {
165        self.0
166    }
167}
168
169impl Claim for TimeToLiveClaim {
170    const JWT_CLAIM_NAME: &'static str = "ttl";
171}
172
173/// JSON Status List.
174///
175/// See: <https://www.ietf.org/archive/id/draft-ietf-oauth-status-list-02.html#name-status-list>
176#[derive(Clone, Serialize, Deserialize)]
177pub struct JsonStatusList {
178    /// Number of bits per Referenced Token in `lst`.
179    bits: StatusSize,
180
181    /// Status values for all the Referenced Tokens it conveys statuses for.
182    lst: String,
183}
184
185impl JsonStatusList {
186    pub fn encode(bit_string: &BitString, compression: Compression) -> Self {
187        let bytes = bit_string.to_compressed_bytes(compression);
188        Self {
189            bits: bit_string.status_size(),
190            lst: BASE64_URL_SAFE.encode(bytes),
191        }
192    }
193
194    pub fn decode(&self, limit: Option<u64>) -> Result<BitString, DecodeError> {
195        let bytes = BASE64_URL_SAFE.decode(&self.lst)?;
196        Ok(BitString::from_compressed_bytes(self.bits, &bytes, limit)?)
197    }
198}
199
200impl Claim for JsonStatusList {
201    const JWT_CLAIM_NAME: &'static str = "status_list";
202}
203
204/// Status claim value.
205#[derive(Clone, Serialize, Deserialize)]
206pub struct Status {
207    pub status_list: StatusListReference,
208}
209
210impl Claim for Status {
211    const JWT_CLAIM_NAME: &'static str = "status";
212}
213
214impl StatusMapEntrySet for Status {
215    type Entry<'a> = &'a StatusListReference;
216
217    fn get_entry(&self, _purpose: crate::StatusPurpose<&str>) -> Option<Self::Entry<'_>> {
218        Some(&self.status_list)
219    }
220}
221
222#[derive(Clone, Serialize, Deserialize)]
223pub struct StatusListReference {
224    /// Index to check for status information in the Status List for the current
225    /// Referenced Token.
226    pub idx: usize,
227
228    /// Identifies the Status List or Status List Token containing the status
229    /// information for the Referenced Token.
230    pub uri: UriBuf,
231}
232
233impl StatusMapEntry for StatusListReference {
234    type Key = usize;
235    type StatusSize = StatusSize;
236
237    fn key(&self) -> Self::Key {
238        self.idx
239    }
240
241    fn status_list_url(&self) -> &iref::Uri {
242        &self.uri
243    }
244
245    fn status_size(&self) -> Option<Self::StatusSize> {
246        None
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use crate::token_status_list::json::JsonStatusList;
253
254    #[test]
255    fn deserialize_json_status_list() {
256        assert!(serde_json::from_str::<JsonStatusList>(
257            r#"{
258                "bits": 1,
259                "lst": "eNrbuRgAAhcBXQ"
260            }"#
261        )
262        .is_ok())
263    }
264}