jsonprooftoken/jwp/
presented.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::PresentationProofAlgorithm, bbs_plus::BBSplusAlgorithm},
23    jpt::{
24        claims::Claims,
25        payloads::{PayloadType, Payloads},
26    },
27    jwk::key::Jwk,
28};
29
30use super::{
31    header::{IssuerProtectedHeader, PresentationProtectedHeader},
32    issued::JwpIssued,
33};
34
35/// Takes the result of a rsplit and ensure we only get 4 parts (JwpPresented)
36/// Errors if we don't
37macro_rules! expect_four {
38    ($iter:expr) => {{
39        let mut i = $iter;
40        match (i.next(), i.next(), i.next(), i.next()) {
41            (Some(first), Some(second), Some(third), Some(fourth)) => {
42                (first, second, third, fourth)
43            }
44            _ => return Err(CustomError::InvalidPresentedJwp),
45        }
46    }};
47}
48
49/// Used to build a new JSON Web Proof in the Presentation form from an verified Issued JWP
50#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
51pub struct JwpPresentedBuilder {
52    issuer_protected_header: IssuerProtectedHeader,
53    presentation_protected_header: Option<PresentationProtectedHeader>,
54    payloads: Payloads,
55    issuer_proof: Vec<u8>,
56}
57
58impl JwpPresentedBuilder {
59    pub fn new(issued_jwp: &JwpIssued) -> Self {
60        Self {
61            issuer_protected_header: issued_jwp.get_issuer_protected_header().clone(),
62            presentation_protected_header: None,
63            payloads: issued_jwp.get_payloads().clone(),
64            issuer_proof: issued_jwp.get_proof().to_vec(),
65        }
66    }
67
68    pub fn set_presentation_protected_header(
69        &mut self,
70        header: PresentationProtectedHeader,
71    ) -> &mut Self {
72        self.presentation_protected_header = Some(header);
73        self
74    }
75
76    // Getter for issuer_protected_header
77    pub fn get_issuer_protected_header(&self) -> &IssuerProtectedHeader {
78        &self.issuer_protected_header
79    }
80
81    // Getter for presentation_protected_header
82    pub fn get_presentation_protected_header(&self) -> Option<&PresentationProtectedHeader> {
83        self.presentation_protected_header.as_ref()
84    }
85
86    // Getter for payloads
87    pub fn get_payloads(&self) -> &Payloads {
88        &self.payloads
89    }
90
91    // Getter for issuer_proof
92    pub fn issuer_proof(&self) -> &Vec<u8> {
93        &self.issuer_proof
94    }
95
96    pub fn set_undisclosed(&mut self, claim: &str) -> Result<&mut Self, CustomError> {
97        let index = self
98            .issuer_protected_header
99            .claims()
100            .and_then(|c| c.0.iter().position(|x| x == claim))
101            .ok_or(CustomError::SelectiveDisclosureError)?;
102        self.payloads.set_undisclosed(index);
103        Ok(self)
104    }
105
106    pub fn build_with_proof(&self, proof: Vec<u8>) -> Result<JwpPresented, CustomError> {
107        if let Some(presentation_protected_header) = self.presentation_protected_header.clone() {
108            Ok(JwpPresented {
109                issuer_protected_header: self.issuer_protected_header.clone(),
110                presentation_protected_header,
111                payloads: self.payloads.clone(),
112                proof,
113            })
114        } else {
115            Err(CustomError::IncompleteJwpBuild(
116                crate::errors::IncompleteJwpBuild::NoIssuerHeader,
117            ))
118        }
119    }
120
121    pub fn build(&self, jwk: &Jwk) -> Result<JwpPresented, CustomError> {
122        if let Some(presentation_protected_header) = self.presentation_protected_header.clone() {
123            let issuer_header_oct = serde_json::to_vec(&self.issuer_protected_header).unwrap();
124            let presentation_header_oct =
125                serde_json::to_vec(&self.presentation_protected_header).unwrap();
126
127            let proof = Self::generate_proof(
128                presentation_protected_header.alg(),
129                jwk,
130                &self.issuer_proof,
131                &issuer_header_oct,
132                &presentation_header_oct,
133                &self.payloads,
134            )?;
135            Ok(JwpPresented {
136                issuer_protected_header: self.issuer_protected_header.clone(),
137                presentation_protected_header,
138                payloads: self.payloads.clone(),
139                proof,
140            })
141        } else {
142            Err(CustomError::IncompleteJwpBuild(
143                crate::errors::IncompleteJwpBuild::NoIssuerHeader,
144            ))
145        }
146    }
147
148    fn generate_proof(
149        alg: PresentationProofAlgorithm,
150        key: &Jwk,
151        issuer_proof: &[u8],
152        issuer_header_oct: &[u8],
153        presentation_header_oct: &[u8],
154        payloads: &Payloads,
155    ) -> Result<Vec<u8>, CustomError> {
156        let proof = match alg {
157            PresentationProofAlgorithm::BBS
158            | PresentationProofAlgorithm::BBS_SHAKE256 => {
159                BBSplusAlgorithm::generate_presentation_proof(
160                    alg,
161                    issuer_proof,
162                    payloads,
163                    key,
164                    issuer_header_oct,
165                    presentation_header_oct,
166                )?
167            }
168            PresentationProofAlgorithm::SU_ES256 => todo!(),
169            PresentationProofAlgorithm::SU_ES384 => todo!(),
170            PresentationProofAlgorithm::SU_ES512 => todo!(),
171            PresentationProofAlgorithm::MAC_H256 => todo!(),
172            PresentationProofAlgorithm::MAC_H384 => todo!(),
173            PresentationProofAlgorithm::MAC_H512 => todo!(),
174            PresentationProofAlgorithm::MAC_K25519 => todo!(),
175            PresentationProofAlgorithm::MAC_K448 => todo!(),
176            PresentationProofAlgorithm::MAC_H256K => todo!(),
177        };
178
179        Ok(proof)
180    }
181}
182
183/// Used for both decoding and verifing a JSON Proof Token representing a JWP in the Presentation form
184#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
185pub struct JwpPresentedDecoder {
186    issuer_protected_header: IssuerProtectedHeader,
187    presentation_protected_header: PresentationProtectedHeader,
188    payloads: Payloads,
189    proof: Vec<u8>,
190}
191
192impl JwpPresentedDecoder {
193    /// Decode a JSON Proof Token. The token must represent a Presented JWP, otherwise will return an error.
194    pub fn decode(jpt: &str, serialization: SerializationType) -> Result<Self, CustomError> {
195        match serialization {
196            SerializationType::COMPACT => {
197                let (
198                    encoded_issuer_protected_header,
199                    encoded_presentation_protected_header,
200                    encoded_payloads,
201                    encoded_proof,
202                ) = expect_four!(jpt.splitn(4, '.'));
203                let presentation_protected_header: PresentationProtectedHeader =
204                    serde_json::from_slice(&base64url_decode(
205                        encoded_presentation_protected_header,
206                    ))
207                    .map_err(|_| CustomError::SerializationError)?;
208                let issuer_protected_header: IssuerProtectedHeader =
209                    serde_json::from_slice(&base64url_decode(encoded_issuer_protected_header))
210                        .map_err(|_| CustomError::SerializationError)?;
211                let payloads = Payloads(
212                    encoded_payloads
213                        .splitn(issuer_protected_header.claims().unwrap().0.len(), "~")
214                        .map(|v| {
215                            if v == "" {
216                                (serde_json::Value::Null, PayloadType::Undisclosed)
217                            } else {
218                                (
219                                    serde_json::from_slice(&base64url_decode(v)).unwrap(),
220                                    PayloadType::Disclosed,
221                                )
222                            }
223                        })
224                        .collect(),
225                );
226
227                if !match issuer_protected_header.claims() {
228                    Some(claims) => claims.0.len() == payloads.0.len(),
229                    None => payloads.0.len() == 0,
230                } {
231                    return Err(CustomError::InvalidIssuedJwp);
232                }
233
234                let proof = base64url_decode(encoded_proof);
235
236                Ok(Self {
237                    issuer_protected_header,
238                    payloads,
239                    proof: proof,
240                    presentation_protected_header,
241                })
242            }
243            SerializationType::JSON => todo!(),
244            SerializationType::CBOR => todo!(),
245        }
246    }
247
248    /// Verify the decoded JWP
249    pub fn verify(&self, key: &Jwk) -> Result<JwpPresented, CustomError> {
250        let issuer_header_oct = serde_json::to_vec(&self.issuer_protected_header).unwrap();
251        let presentation_header_oct =
252            serde_json::to_vec(&self.presentation_protected_header).unwrap();
253        Self::verify_proof(
254            self.presentation_protected_header.alg(),
255            key,
256            &self.proof,
257            &presentation_header_oct,
258            &issuer_header_oct,
259            &self.payloads,
260        )?;
261        Ok(JwpPresented {
262            issuer_protected_header: self.issuer_protected_header.clone(),
263            presentation_protected_header: self.presentation_protected_header.clone(),
264            payloads: self.payloads.clone(),
265            proof: self.proof.clone(),
266        })
267    }
268
269    pub fn get_issuer_header(&self) -> &IssuerProtectedHeader {
270        &self.issuer_protected_header
271    }
272
273    pub fn get_presentation_header(&self) -> &PresentationProtectedHeader {
274        &self.presentation_protected_header
275    }
276
277    pub fn get_payloads(&self) -> &Payloads {
278        &self.payloads
279    }
280
281    fn verify_proof(
282        alg: PresentationProofAlgorithm,
283        key: &Jwk,
284        proof: &[u8],
285        presentation_header_oct: &[u8],
286        issuer_header_oct: &[u8],
287        payloads: &Payloads,
288    ) -> Result<(), CustomError> {
289        let check = match alg {
290            PresentationProofAlgorithm::BBS
291            | PresentationProofAlgorithm::BBS_SHAKE256 => {
292                BBSplusAlgorithm::verify_presentation_proof(
293                    alg,
294                    &key,
295                    proof,
296                    presentation_header_oct,
297                    issuer_header_oct,
298                    payloads,
299                )
300            }
301            PresentationProofAlgorithm::SU_ES256 => todo!(),
302            PresentationProofAlgorithm::SU_ES384 => todo!(),
303            PresentationProofAlgorithm::SU_ES512 => todo!(),
304            PresentationProofAlgorithm::MAC_H256 => todo!(),
305            PresentationProofAlgorithm::MAC_H384 => todo!(),
306            PresentationProofAlgorithm::MAC_H512 => todo!(),
307            PresentationProofAlgorithm::MAC_K25519 => todo!(),
308            PresentationProofAlgorithm::MAC_K448 => todo!(),
309            PresentationProofAlgorithm::MAC_H256K => todo!(),
310        };
311
312        check
313    }
314}
315
316/// Decoded and verified JSON Web Proof in the Presentation form
317#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
318pub struct JwpPresented {
319    issuer_protected_header: IssuerProtectedHeader,
320    presentation_protected_header: PresentationProtectedHeader,
321    payloads: Payloads,
322    proof: Vec<u8>,
323}
324
325impl JwpPresented {
326    /// Encode the currently crafted JWP
327    pub fn encode(&self, serialization: SerializationType) -> Result<String, CustomError> {
328        // let encoded_issuer_header = base64url_encode_serializable(&self.issuer_protected_header);
329        // let encoded_presentation_header = base64url_encode_serializable(&self.presentation_protected_header);
330
331        let issuer_header_oct = serde_json::to_vec(&self.issuer_protected_header)
332            .map_err(|_| CustomError::SerializationError)?;
333
334        let presentation_header_oct = serde_json::to_vec(&self.presentation_protected_header)
335            .map_err(|_| CustomError::SerializationError)?;
336
337        let jwp = Self::serialize(
338            serialization,
339            &presentation_header_oct,
340            &issuer_header_oct,
341            &self.payloads,
342            &self.proof,
343        );
344
345        Ok(jwp)
346    }
347
348    pub fn get_issuer_protected_header(&self) -> &IssuerProtectedHeader {
349        &self.issuer_protected_header
350    }
351
352    pub fn get_presentation_protected_header(&self) -> &PresentationProtectedHeader {
353        &self.presentation_protected_header
354    }
355
356    pub fn get_claims(&self) -> Option<&Claims> {
357        self.issuer_protected_header.claims()
358    }
359
360    pub fn get_payloads(&self) -> &Payloads {
361        &self.payloads
362    }
363
364    pub fn get_proof(&self) -> &[u8] {
365        &self.proof
366    }
367
368    fn serialize(
369        serialization: SerializationType,
370        presentation_header_oct: &[u8],
371        issuer_header_oct: &[u8],
372        payloads: &Payloads,
373        proof: &[u8],
374    ) -> String {
375        let encoded_issuer_header = base64url_encode(issuer_header_oct);
376        let encoded_presentation_header = base64url_encode(presentation_header_oct);
377        let encoded_proof = base64url_encode(proof);
378
379        let jwp = match serialization {
380            SerializationType::COMPACT => {
381                let encoded_payloads = payloads
382                    .0
383                    .iter()
384                    .map(|p| {
385                        if p.1 == PayloadType::Undisclosed {
386                            "".to_string()
387                        } else {
388                            base64url_encode_serializable(&p.0)
389                        }
390                    })
391                    .collect::<Vec<String>>()
392                    .join("~");
393                format!(
394                    "{}.{}.{}.{}",
395                    encoded_issuer_header,
396                    encoded_presentation_header,
397                    encoded_payloads,
398                    encoded_proof
399                )
400            }
401            SerializationType::JSON => todo!(),
402            SerializationType::CBOR => todo!(),
403        };
404
405        jwp
406    }
407}
408
409
410#[cfg(test)] 
411mod tests {
412
413    use crate::{
414        encoding::SerializationType,
415        jpa::algs::{PresentationProofAlgorithm, ProofAlgorithm},
416        jpt::claims::JptClaims,
417        jwk::{key::Jwk, types::KeyPairSubtype},
418        jwp::{
419            header::{IssuerProtectedHeader, PresentationProtectedHeader},
420            issued::{JwpIssuedBuilder, JwpIssuedDecoder},
421            presented::{JwpPresentedBuilder, JwpPresentedDecoder},
422        },
423    };
424
425    #[test]
426    fn test_jwp_presented(){
427        let custom_claims = serde_json::json!({
428            "degree": {
429                "type": "BachelorDegree",
430                "name": "Bachelor of Science and Arts",
431                },
432            "name": "John Doe"
433        });
434    
435        let mut jpt_claims = JptClaims::new();
436        jpt_claims.set_iss("https://issuer.example".to_owned());
437        jpt_claims.set_claim(Some("vc"), custom_claims, true);
438    
439        let issued_header = IssuerProtectedHeader::new(ProofAlgorithm::BBS);
440    
441        let bbs_jwk = Jwk::generate(KeyPairSubtype::BLS12381G2Sha256).unwrap();
442    
443        let issued_jwp = JwpIssuedBuilder::new(issued_header, jpt_claims)
444            .build(&bbs_jwk)
445            .unwrap();
446    
447        let compact_issued_jwp = issued_jwp.encode(SerializationType::COMPACT).unwrap();
448    
449        let decoded_issued_jwp =
450            JwpIssuedDecoder::decode(&compact_issued_jwp, SerializationType::COMPACT)
451                .unwrap()
452                .verify(&bbs_jwk.to_public().unwrap())
453                .unwrap();
454    
455        let mut presentation_header = PresentationProtectedHeader::new(PresentationProofAlgorithm::BBS);
456        presentation_header.set_aud(Some("https://recipient.example.com".to_owned()));
457        presentation_header.set_nonce(Some("wrmBRkKtXjQ".to_owned()));
458    
459        let presented_jwp = JwpPresentedBuilder::new(&decoded_issued_jwp)
460            .set_presentation_protected_header(presentation_header)
461            .build(&bbs_jwk.to_public().unwrap())
462            .unwrap();
463    
464        let compact_presented_jwp = presented_jwp.encode(SerializationType::COMPACT).unwrap();
465        
466        let decoded_presented_jwp =
467            JwpPresentedDecoder::decode(&compact_presented_jwp, SerializationType::COMPACT)
468                .unwrap()
469                .verify(&bbs_jwk.to_public().unwrap())
470                .unwrap();
471
472        assert_eq!(presented_jwp, decoded_presented_jwp);
473
474    }
475
476    #[test]
477    fn test_jwp_presented_selective_disclosure(){
478        let custom_claims = serde_json::json!({
479            "degree": {
480                "type": "BachelorDegree",
481                "name": "Bachelor of Science and Arts",
482                "ciao": [
483                    {"u1": "value1"},
484                    {"u2": "value2"}
485                    ]
486                },
487            "name": "John Doe"
488        });
489    
490        let mut jpt_claims = JptClaims::new();
491        jpt_claims.set_iss("https://issuer.example".to_owned());
492        jpt_claims.set_claim(Some("vc"), custom_claims, true);
493    
494        let issued_header = IssuerProtectedHeader::new(ProofAlgorithm::BBS);
495    
496        let bbs_jwk = Jwk::generate(KeyPairSubtype::BLS12381G2Sha256).unwrap();
497    
498        let issued_jwp = JwpIssuedBuilder::new(issued_header, jpt_claims)
499            .build(&bbs_jwk)
500            .unwrap();
501    
502        let compact_issued_jwp = issued_jwp.encode(SerializationType::COMPACT).unwrap();
503    
504        let decoded_issued_jwp =
505            JwpIssuedDecoder::decode(&compact_issued_jwp, SerializationType::COMPACT)
506                .unwrap()
507                .verify(&bbs_jwk.to_public().unwrap())
508                .unwrap();
509        
510        let mut presentation_header = PresentationProtectedHeader::new(
511            decoded_issued_jwp
512                .get_issuer_protected_header()
513                .alg()
514                .into(),
515        );
516        presentation_header.set_aud(Some("https://recipient.example.com".to_owned()));
517        presentation_header.set_nonce(Some("wrmBRkKtXjQ".to_owned()));
518    
519        let presented_jwp = JwpPresentedBuilder::new(&decoded_issued_jwp)
520            .set_presentation_protected_header(presentation_header)
521            .set_undisclosed("vc.degree.name")
522            .unwrap()
523            .set_undisclosed("vc.degree.ciao[0].u1")
524            .unwrap()
525            .set_undisclosed("vc.name")
526            .unwrap()
527            .build(&bbs_jwk.to_public().unwrap())
528            .unwrap();
529    
530        let presented_claims = presented_jwp.get_claims().unwrap();
531
532        assert!(presented_claims.0.contains(&"vc.degree.name".to_owned()));
533        assert!(presented_claims.0.contains(&"vc.degree.ciao[0].u1".to_owned()));
534        assert!(presented_claims.0.contains(&"vc.name".to_owned()));
535        assert!(!presented_claims.0.contains(&"vc.degree.ciao[0].u2".to_owned()));
536
537    }
538
539}