sdjwt 0.8.1

SD-JWT support for Issuers, Holders, and Verifiers
Documentation
use chrono::{Duration, Utc};
use sdjwt::{
    decode, parse_yaml, sd_jwt_parts, Algorithm, Disclosure, Error, HashAlgorithm, Holder, Issuer,
    Jwk, KeyForDecoding, KeyForEncoding, Validation, Verifier,
};
use std::collections::HashSet;

use common::{
    compare_json_values, convert_to_pem, disclosures2vec, keys, publickey_to_jwk,
    separate_jwt_and_disclosures,
};
use serde_json::value::Value;

mod common;

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_CLAIMS_YAML: &str = r#"
    sub: user_42
    !sd given_name: John
    !sd family_name: Doe
    email: johndoe@example.com
    phone_number: +1-202-555-0101
    phone_number_verified: true
    address:
        !sd street_address: 123 Main St
        !sd locality: Anytown
        region: Anystate
        country: US
    birthdate: 1940-01-01
    updated_at: 1570000000
    nationalities:
        - !sd US
        - !sd 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_basic_encoding_decoding() -> Result<(), Error> {
    let (priv_key, pub_key) = keys();
    let (issuer_private_key, issuer_public_key) = convert_to_pem(priv_key, pub_key);
    let mut claims: Value = serde_json::from_str(TEST_CLAIMS).unwrap();
    let now = Utc::now();
    let expiration = now + Duration::minutes(5);
    let exp = expiration.timestamp();
    claims["exp"] = serde_json::json!(exp);
    let mut issuer = Issuer::new(claims)?;
    let encoded = issuer
        .disclosable("/given_name")
        .disclosable("/family_name")
        .disclosable("/address/street_address")
        .disclosable("/address/locality")
        .disclosable("/nationalities/0")
        .disclosable("/nationalities/1")
        .encode(&crate::KeyForEncoding::from_rsa_pem(
            issuer_private_key.as_bytes(),
        )?)?;
    println!("encoded: {:?}", encoded);
    let dot_segments = encoded.split('.').count();
    let disclosure_segments = encoded.split('~').count() - 2;

    assert_eq!(dot_segments, 3);
    assert_eq!(disclosure_segments, 6);

    // get issuer JWT by splitting left part of the string at the first ~
    let issuer_jwt = encoded.split('~').next().unwrap();
    let (header, claims) = decode(
        issuer_jwt,
        &KeyForDecoding::from_rsa_pem(issuer_public_key.as_bytes()).unwrap(),
        &Validation::default(),
    )?;
    println!("header: {:?}", header);
    println!("claims: {:?}", claims);

    assert_eq!(header["alg"], "RS256");
    assert_eq!(header["typ"], "sd-jwt");
    assert_eq!(claims["sub"], "user_42");
    assert!(claims["_sd"].is_array());
    assert_eq!(claims["_sd"].as_array().unwrap().len(), 2);
    assert!(claims["address"]["_sd"].is_array());
    assert_eq!(claims["address"]["_sd"].as_array().unwrap().len(), 2);
    assert_eq!(claims["_sd_alg"], "sha-256");
    assert!(claims["nationalities"].is_array());
    assert_eq!(claims["nationalities"].as_array().unwrap().len(), 2);
    assert!(claims["nationalities"][0].is_object());
    assert!(claims["nationalities"][1].is_object());
    Ok(())
}

#[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 (_, 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(())
}

#[test]
fn test_issue_claims_with_yaml() -> 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, tagged_paths) = parse_yaml(TEST_CLAIMS_YAML)?;
    let mut issuer = Issuer::new(claims)?;
    let issuer_sd_jwt = issuer
        .expires_in_seconds(60)
        .require_key_binding(Jwk::from_value(holder_jwk)?)
        .iter_disclosable(tagged_paths.iter())
        .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 (_, 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(())
}