jsonprooftoken/jwp/
issued.rs

1// Copyright 2025 Fondazione LINKS
2
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6
7//     http://www.apache.org/licenses/LICENSE-2.0
8
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use serde::{Deserialize, Serialize};
16
17use crate::{
18    encoding::{
19        base64url_decode, base64url_encode, base64url_encode_serializable, SerializationType,
20    },
21    errors::CustomError,
22    jpa::{algs::ProofAlgorithm, bbs_plus::BBSplusAlgorithm},
23    jpt::{
24        claims::{Claims, JptClaims},
25        payloads::{PayloadType, Payloads},
26    },
27    jwk::key::Jwk,
28};
29
30use super::header::IssuerProtectedHeader;
31
32/// Takes the result of a rsplit and ensure we only get 3 parts (JwpIssued)
33/// Errors if we don't
34macro_rules! expect_three {
35    ($iter:expr) => {{
36        let mut i = $iter;
37        match (i.next(), i.next(), i.next()) {
38            (Some(first), Some(second), Some(third)) => (first, second, third),
39            _ => return Err(CustomError::InvalidIssuedJwp),
40        }
41    }};
42}
43
44/// Used to build a new JSON Web Proof in the Issuer form
45#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
46pub struct JwpIssuedBuilder {
47    issuer_protected_header: Option<IssuerProtectedHeader>,
48    payloads: Option<Payloads>,
49}
50
51impl JwpIssuedBuilder {
52    pub fn new(issuer_protected_header: IssuerProtectedHeader, jpt_claims: JptClaims) -> Self {
53        let (claims, payloads) = jpt_claims.get_claims_and_payloads();
54        //Set claims
55        let mut issuer_protected_header = issuer_protected_header;
56        issuer_protected_header.set_claims(Some(claims));
57
58        Self {
59            issuer_protected_header: Some(issuer_protected_header),
60            payloads: Some(payloads),
61        }
62    }
63
64    pub fn get_issuer_protected_header(&self) -> Option<&IssuerProtectedHeader> {
65        self.issuer_protected_header.as_ref()
66    }
67
68    pub fn get_payloads(&self) -> Option<&Payloads> {
69        self.payloads.as_ref()
70    }
71
72    pub fn build_with_proof(&self, proof: Vec<u8>) -> Result<JwpIssued, CustomError> {
73        if let Some(issuer_protected_header) = self.issuer_protected_header.clone() {
74            if let Some(payloads) = self.payloads.clone() {
75                Ok(JwpIssued {
76                    issuer_protected_header,
77                    payloads,
78                    proof,
79                })
80            } else {
81                Err(CustomError::IncompleteJwpBuild(
82                    crate::errors::IncompleteJwpBuild::NoClaimsAndPayloads,
83                ))
84            }
85        } else {
86            Err(CustomError::IncompleteJwpBuild(
87                crate::errors::IncompleteJwpBuild::NoIssuerHeader,
88            ))
89        }
90    }
91
92    pub fn build(&self, jwk: &Jwk) -> Result<JwpIssued, CustomError> {
93        if let Some(issuer_protected_header) = self.issuer_protected_header.clone() {
94            if let Some(payloads) = self.payloads.clone() {
95                let issuer_header_oct = serde_json::to_vec(&self.issuer_protected_header).unwrap();
96                let proof = Self::generate_proof(
97                    issuer_protected_header.alg(),
98                    &jwk,
99                    &issuer_header_oct,
100                    &payloads,
101                )?;
102
103                Ok(JwpIssued {
104                    issuer_protected_header,
105                    payloads,
106                    proof,
107                })
108            } else {
109                Err(CustomError::IncompleteJwpBuild(
110                    crate::errors::IncompleteJwpBuild::NoClaimsAndPayloads,
111                ))
112            }
113        } else {
114            Err(CustomError::IncompleteJwpBuild(
115                crate::errors::IncompleteJwpBuild::NoIssuerHeader,
116            ))
117        }
118    }
119
120    fn generate_proof(
121        alg: ProofAlgorithm,
122        key: &Jwk,
123        issuer_header_oct: &[u8],
124        payloads: &Payloads,
125    ) -> Result<Vec<u8>, CustomError> {
126        let proof = match alg {
127            ProofAlgorithm::BBS | ProofAlgorithm::BBS_SHAKE256 => {
128                        BBSplusAlgorithm::generate_issuer_proof(alg, payloads, key, issuer_header_oct)?
129                    }
130            ProofAlgorithm::SU_ES256 => todo!(),
131            ProofAlgorithm::SU_ES384 => todo!(),
132            ProofAlgorithm::SU_ES512 => todo!(),
133            ProofAlgorithm::MAC_H256 => todo!(),
134            ProofAlgorithm::MAC_H384 => todo!(),
135            ProofAlgorithm::MAC_H512 => todo!(),
136            ProofAlgorithm::MAC_K25519 => todo!(),
137            ProofAlgorithm::MAC_K448 => todo!(),
138            ProofAlgorithm::MAC_H256K => todo!(),
139        };
140
141        Ok(proof)
142    }
143}
144
145/// Used for both decoding and verifing a JSON Proof Token representing a JWP in the Issuer form
146#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
147pub struct JwpIssuedDecoder {
148    issuer_protected_header: IssuerProtectedHeader,
149    payloads: Payloads,
150    proof: Vec<u8>,
151}
152
153impl JwpIssuedDecoder {
154    /// Decode a JSON Proof Token. The token must represent an Issued JWP, otherwise will return an error.
155    pub fn decode(jpt: &str, serialization: SerializationType) -> Result<Self, CustomError> {
156        match serialization {
157            SerializationType::COMPACT => {
158                let (encoded_issuer_protected_header, encoded_payloads, encoded_proof) =
159                    expect_three!(jpt.splitn(3, '.'));
160                let issuer_protected_header: IssuerProtectedHeader =
161                    serde_json::from_slice(&base64url_decode(encoded_issuer_protected_header))
162                        .map_err(|_| CustomError::SerializationError)?;
163                //TODO: this could not have much sense for now (maybe useful to handle blind signatures?)
164                let payloads = Payloads(
165                    encoded_payloads
166                        .splitn(issuer_protected_header.claims().unwrap().0.len(), "~")
167                        .map(|v| {
168                            if v == "" {
169                                (serde_json::Value::Null, PayloadType::Undisclosed)
170                            } else {
171                                (
172                                    serde_json::from_slice(&base64url_decode(v)).unwrap(),
173                                    PayloadType::Disclosed,
174                                )
175                            }
176                        })
177                        .collect(),
178                );
179
180                if !match issuer_protected_header.claims() {
181                    Some(claims) => claims.0.len() == payloads.0.len(),
182                    None => payloads.0.len() == 0,
183                } {
184                    return Err(CustomError::InvalidIssuedJwp);
185                }
186
187                let proof = base64url_decode(encoded_proof);
188                Ok(Self {
189                    issuer_protected_header,
190                    payloads,
191                    proof: proof,
192                })
193            }
194            SerializationType::JSON => todo!(),
195            SerializationType::CBOR => todo!(),
196        }
197    }
198
199    /// Verify the decoded JWP
200    pub fn verify(&self, key: &Jwk) -> Result<JwpIssued, CustomError> {
201        let issuer_header_oct = serde_json::to_vec(&self.issuer_protected_header).unwrap();
202
203        Self::verify_proof(
204            self.issuer_protected_header.alg(),
205            key,
206            &self.proof,
207            &issuer_header_oct,
208            &self.payloads,
209        )?;
210
211        Ok(JwpIssued {
212            issuer_protected_header: self.issuer_protected_header.clone(),
213            payloads: self.payloads.clone(),
214            proof: self.proof.clone(),
215        })
216    }
217
218    pub fn get_header(&self) -> &IssuerProtectedHeader {
219        &self.issuer_protected_header
220    }
221
222    pub fn get_payloads(&self) -> &Payloads {
223        &self.payloads
224    }
225
226    fn verify_proof(
227        alg: ProofAlgorithm,
228        key: &Jwk,
229        proof: &[u8],
230        issuer_header_oct: &[u8],
231        payloads: &Payloads,
232    ) -> Result<(), CustomError> {
233        let check = match alg {
234            ProofAlgorithm::BBS | ProofAlgorithm::BBS_SHAKE256 => {
235                BBSplusAlgorithm::verify_issuer_proof(alg, &key, proof, issuer_header_oct, payloads)
236            }
237            ProofAlgorithm::SU_ES256 => todo!(),
238            ProofAlgorithm::SU_ES384 => todo!(),
239            ProofAlgorithm::SU_ES512 => todo!(),
240            ProofAlgorithm::MAC_H256 => todo!(),
241            ProofAlgorithm::MAC_H384 => todo!(),
242            ProofAlgorithm::MAC_H512 => todo!(),
243            ProofAlgorithm::MAC_K25519 => todo!(),
244            ProofAlgorithm::MAC_K448 => todo!(),
245            ProofAlgorithm::MAC_H256K => todo!(),
246        };
247
248        check
249    }
250}
251
252/// Decoded and verified JSON Web Proof in the Issuer form
253#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
254pub struct JwpIssued {
255    issuer_protected_header: IssuerProtectedHeader,
256    payloads: Payloads,
257    proof: Vec<u8>,
258}
259
260impl JwpIssued {
261    pub fn encode(&self, serialization: SerializationType) -> Result<String, CustomError> {
262        // let encoded_issuer_header = base64url_encode_serializable(&self.issuer_protected_header);
263
264        let issuer_header_oct = serde_json::to_vec(&self.issuer_protected_header)
265            .map_err(|_| CustomError::SerializationError)?;
266
267        let jwp = Self::serialize(
268            serialization,
269            &issuer_header_oct,
270            &self.payloads,
271            &self.proof,
272        );
273
274        Ok(jwp)
275    }
276
277    pub fn get_issuer_protected_header(&self) -> &IssuerProtectedHeader {
278        &self.issuer_protected_header
279    }
280
281    pub fn get_claims(&self) -> Option<&Claims> {
282        self.issuer_protected_header.claims()
283    }
284
285    pub fn set_claims(&mut self, claims: Claims) {
286        self.issuer_protected_header.set_claims(Some(claims));
287    }
288
289    pub fn get_payloads(&self) -> &Payloads {
290        &self.payloads
291    }
292
293    pub fn set_payloads(&mut self, payloads: Payloads) {
294        self.payloads = payloads;
295    }
296
297    pub fn get_proof(&self) -> &[u8] {
298        self.proof.as_ref()
299    }
300
301    pub fn set_proof(&mut self, proof: &[u8]) {
302        self.proof = proof.to_vec();
303    }
304
305    fn serialize(
306        serialization: SerializationType,
307        issuer_header_oct: &[u8],
308        payloads: &Payloads,
309        proof: &[u8],
310    ) -> String {
311        let encoded_issuer_header = base64url_encode(issuer_header_oct);
312        let encoded_proof = base64url_encode(proof);
313        let jwp = match serialization {
314            SerializationType::COMPACT => {
315                let encoded_payloads = payloads
316                    .0
317                    .iter()
318                    .map(|p| {
319                        if p.1 == PayloadType::Undisclosed {
320                            "".to_string()
321                        } else {
322                            base64url_encode_serializable(&p.0)
323                        }
324                    })
325                    .collect::<Vec<String>>()
326                    .join("~");
327
328                format!(
329                    "{}.{}.{}",
330                    encoded_issuer_header, encoded_payloads, encoded_proof
331                )
332            }
333            SerializationType::JSON => todo!(),
334            SerializationType::CBOR => todo!(),
335        };
336
337        jwp
338    }
339}
340
341
342#[cfg(test)] 
343mod tests {
344    use crate::encoding::SerializationType;
345    use crate::jpt::claims::JptClaims;
346    use crate::jpa::algs::ProofAlgorithm;
347    use crate::jwk::key::Jwk;
348    use crate::jwk::types::KeyPairSubtype;
349    use crate::jwp::header::IssuerProtectedHeader;
350    use crate::jwp::issued::{JwpIssuedBuilder, JwpIssuedDecoder};
351
352    #[test]
353    fn test_jwp_issued() {
354        let custom_claims = serde_json::json!({
355            "degree": {
356                "type": "BachelorDegree",
357                "name": "Bachelor of Science and Arts",
358                },
359            "name": "John Doe"
360        });
361    
362        let mut jpt_claims = JptClaims::new();
363        jpt_claims.set_iss("https://issuer.example".to_owned());
364        jpt_claims.set_claim(Some("vc"), custom_claims, true);
365    
366        let issued_header = IssuerProtectedHeader::new(ProofAlgorithm::BBS);
367    
368        let bbs_jwk = Jwk::generate(KeyPairSubtype::BLS12381G2Sha256).unwrap();
369
370        let issued_jwp = JwpIssuedBuilder::new(issued_header, jpt_claims)
371            .build(&bbs_jwk)
372            .unwrap();
373    
374        let compact_issued_jwp = issued_jwp.encode(SerializationType::COMPACT).unwrap();
375    
376        let decoded_issued_jwp =
377            JwpIssuedDecoder::decode(&compact_issued_jwp, SerializationType::COMPACT)
378                .unwrap()
379                .verify(&bbs_jwk.to_public().unwrap())
380                .unwrap();
381    
382        assert_eq!(issued_jwp, decoded_issued_jwp);
383
384    }
385
386}