sdjwt 0.8.1

SD-JWT support for Issuers, Holders, and Verifiers
Documentation
use crate::{
    base64_hash, decode, sd_jwt_parts,
    utils::{drop_kb, remove_digests, restore_disclosures},
    Error, HashAlgorithm, KeyForDecoding, Validation,
};
use base64::Engine;
use serde_json::Value;

/// # Verifier Module
///
/// Represents a Verifier.  Verifies SD-JWT presentations.
///
/// ## Features
///
/// - Verifying SD-JWT presentations.
///
/// Example
///
/// ```
/// use sdjwt::{Verifier, KeyForDecoding, Validation, Error};
/// use std::collections::HashSet;
///
/// const ISSUER_PUBKEY: &str = "-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEA07aCbyrCS2/qYkuOyznaU/vQdobGtz/SvUKSzx4ic9Ax+pGi8OJM\noewxNg/6zFWkZeuZ1NMQMd/3aJLH+L+SqBNDox8cjWSzgR/Gf8xjVpMNiFrxrTx3\nz1ABaYfgsiDW/PhgoXCC7vF2dqLPTVBuObwmULjgmvPDFKUGEu9w/t05FaT+sccv\n2sMw1b8grlqG392etgbjKcvy29qG8Okj+CVPmYUe69Ce87mUOM5H4S9SF/yNLoFU\nczkUHQSa+sWe+QG6RskKay+3xophsMYYk4g4RHZuArg2LUvlDObmv/rsxKOVE3/B\nzV1DDjLs3AhHTwow2qCkFEZFof1dVOIjNwIDAQAB\n-----END RSA PUBLIC KEY-----\n";
/// const PRESENTATION: &str = "eyJ0eXAiOiJzZC1qd3QiLCJhbGciOiJSUzI1NiJ9.eyJfc2QiOlsiYlQzVnNrcVBwc0F1RWJ5VXBVb0o1UVVZaFp6TkZWSWw5TUhkN0hiWjNOSSIsInRWam9RWW1iT2FUOEt6YmRTMFpmUTdUTlU2UFlmV1RxQU1nNVlOUVJ1OUEiXSwiX3NkX2FsZyI6InNoYS0yNTYiLCJhZGRyZXNzIjp7Il9zZCI6WyJ5WC13SXRkMmk1M2pCaV9jeHk3TE5Wd1J6Mm84ajlyd1IxQVJnVVFtVm9vIiwiQi14a3FHNzRvQzFCOUdheDlqQWZTWlVtQlBrVldhVmR1QVBSYlJkWHIyYyJdLCJjb3VudHJ5IjoiVVMiLCJyZWdpb24iOiJBbnlzdGF0ZSJ9LCJiaXJ0aGRhdGUiOiIxOTQwLTAxLTAxIiwiY25mIjp7ImFsZyI6IlJTMjU2IiwiZSI6IkFRQUIiLCJrdHkiOiJSU0EiLCJuIjoiMFEta0s0aGZQbzZsMmFvVzlWUHR6S2hTaV9iN2t6ZTZ6eTlfVThTZjFsRmdxUGIwVXBvRTNuTW4zRUpyc0Jfb1hhb1RmY0RxaG4zTi1EblRFUFFmSTBfRTdnaHc3M0g1TWxiREdZM2VyajdzamE0enFIbmUyX1BZRnJvTFd3V0tjZDMzbUQ3VzhVYTdVSGV1a21GekFreXFEZlp1b0ZRcFdYLTFaVVdnalc0LUpoUUtYSXB4NVF6U1ZDX1hwaUFibzN3Zk5jQlFaaE8xSGxlTDV3VnFyMVZrUTgxcXl6Tlo3UFVRTWd0VlJGdkIyX3lPTlBDZ3piVzQ0TGNVQUFzYk5HNkdyX095WlBvblhuQml3b085LUxnNXdoQVc1TnlkU2ZwVi05UzE0NjV3Nm9IenpxdU1DX0JhcUQ5WVFTZ2pPVXpJb21fc3lYZG5GSTNyWWRZaG93IiwidXNlIjoic2lnIn0sImVtYWlsIjoiam9obmRvZUBleGFtcGxlLmNvbSIsImV4cCI6MTcwMzg2NDkxMSwibmF0aW9uYWxpdGllcyI6W3siLi4uIjoiRDVSLXVQVEhMaTVFNVJqWEJwaW5Ia0VfV1Jxckl0UVFndnFyYWpEZ3ZPTSJ9LHsiLi4uIjoiNTJwZGc4enYtQ1RLT3U1bDhnVUpRalNKQ0I2dHF0NVJ1dUk5WkRESTJCdyJ9XSwicGhvbmVfbnVtYmVyIjoiKzEtMjAyLTU1NS0wMTAxIiwicGhvbmVfbnVtYmVyX3ZlcmlmaWVkIjp0cnVlLCJzdWIiOiJ1c2VyXzQyIiwidXBkYXRlZF9hdCI6MTU3MDAwMDAwMH0.aziX_zt4VylvCt4b_ILZacHQYWGFGsMUd0KEVgg4qtj8JwljDoL8845eHjV1ldpBp7hyWnkrV1X7ZtM7WK1F987ntNv5hK9o-5C2H18UpYKI9YZz5f8yETkWBmu9sH5HKtPv0lstJFc-kQB-jKRyidMxhwO_MU_oR_UtjpIjVd6atRLrwlud4ZM-un8R2R209au8TIE4JIAyzJA1IC5NTR4FdCcwGJiodj62lGRVpmvWhQspxtA9aGKSrnx0x8rL82_dE0hBrRkq5cfbiPR5GM1BN7FtA68OrWK9STHCAaH3VQxe0htOg3o8wlQ6rPMIP5B1Oc0932K56bGwXDZPCg~WyJGSjNhS2JyaWNONUdZRGQtdVk2dGVnIiwiZ2l2ZW5fbmFtZSIsIkpvaG4iXQ~WyItQkFxQ2VJN0kzVUdaREJQR1RNcUpRIiwibG9jYWxpdHkiLCJBbnl0b3duIl0~WyI2RF8zUFpoSlQxTHVDR3o2WTVOMjVBIiwiREUiXQ~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJodHRwczovL3NvbWVvbmUuZXhhbXBsZS5jb20iLCJpYXQiOjE3MDM4NjQ4NTEsIm5vbmNlIjoiODEzOWN2ZUdVTjFKQW1QTllGeWg5eTdqWmZab2VMZXIiLCJzZF9oYXNoIjoidUU1MTY0eTVqZ1NFNWg1V2FiUFpnU0lLWDFOX015Ti1qMlJhNnE3NDJ0ayJ9.BtYvadr-iT6poH9DQV5xAJxAxIFFsNRJ6AQ1rrGySpCVZ-1Dg7a9mvkP3Tf7dJ-r8O-cndJEaUaiKXSFZW7H8j-wO3hp0hrEqlp9OpCNON2EnwUrSm_XLFUFe-MinJZDMZ3qJeCLk7-AMvOgEHXHautwA3Sj2W_G4oDtH05tEHdy50lTVSblqINOLTdy8Vkz82Hs1WW7CVeUOQbsGbKNNAPczTDf00fQg18n6nGmpkHp7rgMV-Sq4qV2qxDeuXE00AkgPAzcMRyCx3Gk7NSWn9NtkTPK9Bporf58r_p5hf4lp-RoqRT0Uza1d5FcaoONl9GtLnhYURLKlCo9yhCbOA";
///
/// fn main() -> Result<(), Error> {
///     let validation = Validation::default().without_expiry();
///     let mut kb_validation = Validation::default().without_expiry();
///     let mut audience = HashSet::new();
///     audience.insert("https://someone.example.com".to_string());
///     kb_validation.aud = Some(audience);
///     let decoding_key = KeyForDecoding::from_rsa_pem(ISSUER_PUBKEY.as_bytes())?;
///     let (ver_header, ver_claims) = Verifier::verify(
///         PRESENTATION,
///         &decoding_key,
///         &validation,
///         &Some(&kb_validation),
///     )?;
///     Ok(())
/// }
/// ```
pub struct Verifier {}

impl Verifier {
    pub fn verify_raw(
        issuer_token: &str,
        key: &KeyForDecoding,
        validation: &Validation,
        kb_validation: &Option<&Validation>,
    ) -> Result<(Value, Value, Vec<String>), Error> {
        let (issuer_sd_jwt, disclosures, kb_jwt) = sd_jwt_parts(issuer_token);
        let (header, claims) = decode(&issuer_sd_jwt, key, validation)?;

        if claims["cnf"].is_null() && kb_jwt.is_some() {
            return Err(Error::SDJWTRejected(
                "Issuer SD JWT must contain cnf claim if key binding JWT is included".to_string(),
            ));
        }

        if !claims["cnf"].is_null() && kb_jwt.is_none() {
            return Err(Error::SDJWTRejected(
                "Key binding JWT must be included if cnf claim is included".to_string(),
            ));
        }

        let hash_alg = match HashAlgorithm::try_from(claims["_sd_alg"].as_str().ok_or(
            Error::SDJWTRejected("Issuer SD JWT must contain _sd_alg claim".to_string()),
        )?) {
            Ok(alg) => alg,
            Err(e) => {
                return Err(Error::InvalidHashAlgorithm(e.to_string()));
            }
        };

        if let Some(kb) = kb_jwt {
            let (_, kb_claims) = verify_kb(
                &kb,
                &claims["cnf"],
                kb_validation.ok_or(Error::SDJWTRejected(
                    "Key binding validation missing".to_string(),
                ))?,
            )?;
            if let Some(sd_hash) = kb_claims["sd_hash"].as_str() {
                if base64_hash(hash_alg, &drop_kb(issuer_token)) != sd_hash {
                    return Err(Error::SDJWTRejected(
                        "KB JWT sd_hash does not match hash of issuer JWT and disclosures"
                            .to_string(),
                    ));
                }
            } else {
                return Err(Error::SDJWTRejected(
                    "Issuer KB JWT must contain sd_hash claim".to_string(),
                ));
            }
        }

        Ok((header, claims, disclosures))
    }

    /// Verifyies SD-JWT presentation received from Holder.
    ///
    /// Example
    ///
    /// ```
    /// use sdjwt::{Verifier, KeyForDecoding, Validation, Error};
    /// use std::collections::HashSet;
    ///
    /// const ISSUER_PUBKEY: &str = "-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEA07aCbyrCS2/qYkuOyznaU/vQdobGtz/SvUKSzx4ic9Ax+pGi8OJM\noewxNg/6zFWkZeuZ1NMQMd/3aJLH+L+SqBNDox8cjWSzgR/Gf8xjVpMNiFrxrTx3\nz1ABaYfgsiDW/PhgoXCC7vF2dqLPTVBuObwmULjgmvPDFKUGEu9w/t05FaT+sccv\n2sMw1b8grlqG392etgbjKcvy29qG8Okj+CVPmYUe69Ce87mUOM5H4S9SF/yNLoFU\nczkUHQSa+sWe+QG6RskKay+3xophsMYYk4g4RHZuArg2LUvlDObmv/rsxKOVE3/B\nzV1DDjLs3AhHTwow2qCkFEZFof1dVOIjNwIDAQAB\n-----END RSA PUBLIC KEY-----\n";
    /// const PRESENTATION: &str = "eyJ0eXAiOiJzZC1qd3QiLCJhbGciOiJSUzI1NiJ9.eyJfc2QiOlsiYlQzVnNrcVBwc0F1RWJ5VXBVb0o1UVVZaFp6TkZWSWw5TUhkN0hiWjNOSSIsInRWam9RWW1iT2FUOEt6YmRTMFpmUTdUTlU2UFlmV1RxQU1nNVlOUVJ1OUEiXSwiX3NkX2FsZyI6InNoYS0yNTYiLCJhZGRyZXNzIjp7Il9zZCI6WyJ5WC13SXRkMmk1M2pCaV9jeHk3TE5Wd1J6Mm84ajlyd1IxQVJnVVFtVm9vIiwiQi14a3FHNzRvQzFCOUdheDlqQWZTWlVtQlBrVldhVmR1QVBSYlJkWHIyYyJdLCJjb3VudHJ5IjoiVVMiLCJyZWdpb24iOiJBbnlzdGF0ZSJ9LCJiaXJ0aGRhdGUiOiIxOTQwLTAxLTAxIiwiY25mIjp7ImFsZyI6IlJTMjU2IiwiZSI6IkFRQUIiLCJrdHkiOiJSU0EiLCJuIjoiMFEta0s0aGZQbzZsMmFvVzlWUHR6S2hTaV9iN2t6ZTZ6eTlfVThTZjFsRmdxUGIwVXBvRTNuTW4zRUpyc0Jfb1hhb1RmY0RxaG4zTi1EblRFUFFmSTBfRTdnaHc3M0g1TWxiREdZM2VyajdzamE0enFIbmUyX1BZRnJvTFd3V0tjZDMzbUQ3VzhVYTdVSGV1a21GekFreXFEZlp1b0ZRcFdYLTFaVVdnalc0LUpoUUtYSXB4NVF6U1ZDX1hwaUFibzN3Zk5jQlFaaE8xSGxlTDV3VnFyMVZrUTgxcXl6Tlo3UFVRTWd0VlJGdkIyX3lPTlBDZ3piVzQ0TGNVQUFzYk5HNkdyX095WlBvblhuQml3b085LUxnNXdoQVc1TnlkU2ZwVi05UzE0NjV3Nm9IenpxdU1DX0JhcUQ5WVFTZ2pPVXpJb21fc3lYZG5GSTNyWWRZaG93IiwidXNlIjoic2lnIn0sImVtYWlsIjoiam9obmRvZUBleGFtcGxlLmNvbSIsImV4cCI6MTcwMzg2NDkxMSwibmF0aW9uYWxpdGllcyI6W3siLi4uIjoiRDVSLXVQVEhMaTVFNVJqWEJwaW5Ia0VfV1Jxckl0UVFndnFyYWpEZ3ZPTSJ9LHsiLi4uIjoiNTJwZGc4enYtQ1RLT3U1bDhnVUpRalNKQ0I2dHF0NVJ1dUk5WkRESTJCdyJ9XSwicGhvbmVfbnVtYmVyIjoiKzEtMjAyLTU1NS0wMTAxIiwicGhvbmVfbnVtYmVyX3ZlcmlmaWVkIjp0cnVlLCJzdWIiOiJ1c2VyXzQyIiwidXBkYXRlZF9hdCI6MTU3MDAwMDAwMH0.aziX_zt4VylvCt4b_ILZacHQYWGFGsMUd0KEVgg4qtj8JwljDoL8845eHjV1ldpBp7hyWnkrV1X7ZtM7WK1F987ntNv5hK9o-5C2H18UpYKI9YZz5f8yETkWBmu9sH5HKtPv0lstJFc-kQB-jKRyidMxhwO_MU_oR_UtjpIjVd6atRLrwlud4ZM-un8R2R209au8TIE4JIAyzJA1IC5NTR4FdCcwGJiodj62lGRVpmvWhQspxtA9aGKSrnx0x8rL82_dE0hBrRkq5cfbiPR5GM1BN7FtA68OrWK9STHCAaH3VQxe0htOg3o8wlQ6rPMIP5B1Oc0932K56bGwXDZPCg~WyJGSjNhS2JyaWNONUdZRGQtdVk2dGVnIiwiZ2l2ZW5fbmFtZSIsIkpvaG4iXQ~WyItQkFxQ2VJN0kzVUdaREJQR1RNcUpRIiwibG9jYWxpdHkiLCJBbnl0b3duIl0~WyI2RF8zUFpoSlQxTHVDR3o2WTVOMjVBIiwiREUiXQ~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJodHRwczovL3NvbWVvbmUuZXhhbXBsZS5jb20iLCJpYXQiOjE3MDM4NjQ4NTEsIm5vbmNlIjoiODEzOWN2ZUdVTjFKQW1QTllGeWg5eTdqWmZab2VMZXIiLCJzZF9oYXNoIjoidUU1MTY0eTVqZ1NFNWg1V2FiUFpnU0lLWDFOX015Ti1qMlJhNnE3NDJ0ayJ9.BtYvadr-iT6poH9DQV5xAJxAxIFFsNRJ6AQ1rrGySpCVZ-1Dg7a9mvkP3Tf7dJ-r8O-cndJEaUaiKXSFZW7H8j-wO3hp0hrEqlp9OpCNON2EnwUrSm_XLFUFe-MinJZDMZ3qJeCLk7-AMvOgEHXHautwA3Sj2W_G4oDtH05tEHdy50lTVSblqINOLTdy8Vkz82Hs1WW7CVeUOQbsGbKNNAPczTDf00fQg18n6nGmpkHp7rgMV-Sq4qV2qxDeuXE00AkgPAzcMRyCx3Gk7NSWn9NtkTPK9Bporf58r_p5hf4lp-RoqRT0Uza1d5FcaoONl9GtLnhYURLKlCo9yhCbOA";
    ///
    /// fn main() -> Result<(), Error> {
    ///     let validation = Validation::default().without_expiry();
    ///     let mut kb_validation = Validation::default().without_expiry();
    ///     let mut audience = HashSet::new();
    ///     audience.insert("https://someone.example.com".to_string());
    ///     kb_validation.aud = Some(audience);
    ///     let decoding_key = KeyForDecoding::from_rsa_pem(ISSUER_PUBKEY.as_bytes())?;
    ///     let (ver_header, ver_claims) = Verifier::verify(
    ///         PRESENTATION,
    ///         &decoding_key,
    ///         &validation,
    ///         &Some(&kb_validation),
    ///     )?;
    ///     Ok(())
    /// }
    /// ```
    pub fn verify(
        issuer_token: &str,
        key: &KeyForDecoding,
        validation: &Validation,
        kb_validation: &Option<&Validation>,
    ) -> Result<(Value, Value), Error> {
        let (header, claims, disclosures) =
            Verifier::verify_raw(issuer_token, key, validation, kb_validation)?;
        let mut updated_claims = claims.clone();
        let algorithm = claims["_sd_alg"].as_str().unwrap_or("");
        let algorithm = HashAlgorithm::try_from(algorithm)?;
        let mut disclosure_paths = Vec::new();
        restore_disclosures(
            &mut updated_claims,
            &disclosures,
            &mut disclosure_paths,
            algorithm,
        )?;

        remove_digests(&mut updated_claims)?;
        Ok((header, updated_claims))
    }
}

pub fn verify_kb(
    kb_jwt: &str,
    kb_jwk: &Value,
    validation: &Validation,
) -> Result<(Value, Value), Error> {
    if kb_jwk["kty"].as_str() != Some("RSA") {
        return Err(Error::SDJWTRejected(
            "Issuer SD JWT cnf claim must contain RSA key".to_string(),
        ));
    }
    let e_str = kb_jwk["e"].as_str().ok_or(Error::SDJWTRejected(
        "Issuer SD JWT cnf claim must contain RSA key, invalid exponent".to_string(),
    ))?;
    let n_str = kb_jwk["n"].as_str().ok_or(Error::SDJWTRejected(
        "Issuer SD JWT cnf claim must contain RSA key, invalid modulus".to_string(),
    ))?;
    let n = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(n_str)?;
    let e = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(e_str)?;
    let (header, claims) = decode(
        kb_jwt,
        &KeyForDecoding::from_rsa_components(&n, &e)?,
        validation,
    )?;

    if header["typ"].as_str() != Some("kb+jwt") {
        return Err(Error::SDJWTRejected(
            "KB JWT type must be kb+jwt".to_string(),
        ));
    }

    Ok((header, claims))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_utils::common_test_utils::{
        compare_json_values, convert_to_pem, disclosures2vec, keys, publickey_to_jwk,
        separate_jwt_and_disclosures,
    };
    use crate::{
        utils::{decode_claims_no_verification, get_jwt_part, JWTPart},
        Algorithm, Disclosure, Holder, Issuer, Jwk, KeyForEncoding, Validation,
    };
    use std::collections::HashSet;

    const TEST_CLAIMS: &str = r#"{
        "sub": "user_42",
        "given_name": "John",
        "family_name": "Doe",
        "email": "johndoe@example.com",
        "phone_number": "+1-202-555-0101",
        "phone_number_verified": true,
        "address": {
            "street_address": "123 Main St",
            "locality": "Anytown",
            "region": "Anystate",
            "country": "US"
        },
        "birthdate": "1940-01-01",
        "updated_at": 1570000000,
        "nationalities": [
            "US",
            "DE"
        ]
    }"#;

    const TEST_VERIFIER_EXPECTED_CLAIMS: &str = r#"{
        "sub": "user_42",
        "given_name": "John",
        "email": "johndoe@example.com",
        "phone_number": "+1-202-555-0101",
        "phone_number_verified": true,
        "address": {
            "locality": "Anytown",
            "region": "Anystate",
            "country": "US"
        },
        "birthdate": "1940-01-01",
        "updated_at": 1570000000,
        "nationalities": [
            "DE"
        ]
    }"#;

    #[test]
    fn test_presentation_verification_with_kb() -> Result<(), Error> {
        // create issuer sd-jwt
        let (priv_key, pub_key) = keys();
        let (issuer_private_key, issuer_public_key) = convert_to_pem(priv_key, pub_key);
        let (holder_private_key, holder_public_key) = keys();
        let holder_jwk = publickey_to_jwk(&holder_public_key);
        let (holder_private_key_pem, _) = convert_to_pem(holder_private_key, holder_public_key);
        let claims: Value = serde_json::from_str(TEST_CLAIMS).unwrap();
        let mut issuer = Issuer::new(claims)?;
        let issuer_sd_jwt = issuer
            .expires_in_seconds(60)
            .disclosable("/given_name")
            .disclosable("/family_name")
            .disclosable("/address/street_address")
            .disclosable("/address/locality")
            .disclosable("/nationalities/0")
            .disclosable("/nationalities/1")
            .require_key_binding(Jwk::from_value(holder_jwk)?)
            .encode(&KeyForEncoding::from_rsa_pem(
                issuer_private_key.as_bytes(),
            )?)?;
        println!("issuer_sd_jwt: {:?}", issuer_sd_jwt);

        // verify issuer sd-jwt by holder
        let validation = Validation::default();
        let decoding_key = KeyForDecoding::from_rsa_pem(issuer_public_key.as_bytes())?;
        let (header, decoded_claims, disclosure_paths) =
            Holder::verify(&issuer_sd_jwt, &decoding_key, &validation)?;
        println!("header: {:?}", header);
        println!("claims: {:?}", decoded_claims);
        println!("disclosure_paths: {:?}", disclosure_paths);

        // holder creates presentation
        let presentation = Holder::presentation(&issuer_sd_jwt)?
            .redact("/family_name")?
            .redact("/address/street_address")?
            .redact("/nationalities/0")?
            .key_binding(
                "https://someone.example.com",
                &KeyForEncoding::from_rsa_pem(holder_private_key_pem.as_bytes())?,
                Algorithm::RS256,
            )?
            .build()?;
        println!("presentation: {:?}", presentation);
        let (issuer_jwt, disclosures, kb_jwt) = sd_jwt_parts(&presentation);

        let issuer_dot_segments = issuer_jwt.split('.').count();
        let kb_jwt_dot_segments = kb_jwt.as_ref().unwrap().split('.').count();

        assert_eq!(issuer_dot_segments, 3);
        assert_eq!(kb_jwt_dot_segments, 3);
        assert_eq!(disclosures.len(), 3);

        let kb_header = decode_claims_no_verification(&get_jwt_part(
            kb_jwt.as_ref().unwrap().as_str(),
            JWTPart::Header,
        )?)?;
        let kb_claims = decode_claims_no_verification(&get_jwt_part(
            kb_jwt.as_ref().unwrap().as_str(),
            JWTPart::Claims,
        )?)?;
        assert!(compare_json_values(
            &serde_json::json!({
              "typ": "kb+jwt",
              "alg": "RS256"
            }),
            &kb_header,
        ));
        assert_eq!(kb_claims["aud"], "https://someone.example.com");
        assert!(kb_claims["nonce"].is_string());
        assert!(kb_claims["iat"].is_number());
        assert!(kb_claims["sd_hash"].is_string());
        let mut issuer_jwt_with_disclosures = issuer_jwt.clone();
        disclosures.iter().for_each(|disclosure| {
            issuer_jwt_with_disclosures.push('~');
            issuer_jwt_with_disclosures.push_str(disclosure);
        });
        issuer_jwt_with_disclosures.push('~');
        assert_eq!(
            kb_claims["sd_hash"],
            base64_hash(HashAlgorithm::SHA256, &issuer_jwt_with_disclosures)
        );

        let (_, disclosure_parts) = separate_jwt_and_disclosures(&presentation);
        let disclosures = disclosures2vec(&disclosure_parts);
        assert_eq!(disclosures.len(), 3);
        let d0 = Disclosure::from_base64(&disclosures[0], HashAlgorithm::SHA256)?;
        let d1 = Disclosure::from_base64(&disclosures[1], HashAlgorithm::SHA256)?;
        let d2 = Disclosure::from_base64(&disclosures[2], HashAlgorithm::SHA256)?;
        assert_eq!(d0.key(), &Some("given_name".to_string()));
        assert_eq!(d0.value(), &serde_json::json!("John"));
        assert_eq!(d1.key(), &Some("locality".to_string()));
        assert_eq!(d1.value(), &serde_json::json!("Anytown"));
        assert_eq!(d2.key(), &None);
        assert_eq!(d2.value(), &serde_json::json!("DE"));

        // Verifier verifies presentation
        let validation = Validation::default();
        let mut kb_validation = Validation::default().without_expiry();
        let mut audience = HashSet::new();
        audience.insert("https://someone.example.com".to_string());
        kb_validation.aud = Some(audience);
        let decoding_key = KeyForDecoding::from_rsa_pem(issuer_public_key.as_bytes())?;
        let (ver_header, ver_claims) = Verifier::verify(
            &presentation,
            &decoding_key,
            &validation,
            &Some(&kb_validation),
        )?;

        println!("ver_header: {:?}", ver_header);
        println!("ver_claims: {:?}", ver_claims);

        let mut ver_claims_without_exp = ver_claims.clone();
        ver_claims_without_exp
            .as_object_mut()
            .unwrap()
            .remove("exp");
        ver_claims_without_exp
            .as_object_mut()
            .unwrap()
            .remove("cnf");
        assert!(compare_json_values(
            &serde_json::from_str(TEST_VERIFIER_EXPECTED_CLAIMS)?,
            &ver_claims_without_exp
        ));

        Ok(())
    }
}