sd-jwt-payload 0.1.1

Rust implementation of the Selective Disclosure for JWTs (SD-JWT)
Documentation
// Copyright 2020-2023 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

use josekit::jws::JwsAlgorithm;
use josekit::jws::JwsHeader;
use josekit::jws::JwsVerifier;
use josekit::jws::HS256;
use josekit::jwt::JwtPayload;
use josekit::jwt::{self};
use serde_json::json;
use serde_json::Map;
use serde_json::Value;

use sd_jwt_payload::Disclosure;
use sd_jwt_payload::SdJwt;
use sd_jwt_payload::SdObjectDecoder;
use sd_jwt_payload::SdObjectEncoder;

#[test]
fn test_complex_structure() {
  // Values taken from https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-07.html#appendix-A.2
  let object = json!({
    "verified_claims": {
      "verification": {
        "trust_framework": "de_aml",
        "time": "2012-04-23T18:25Z",
        "verification_process": "f24c6f-6d3f-4ec5-973e-b0d8506f3bc7",
        "evidence": [
          {
            "type": "document",
            "method": "pipp",
            "time": "2012-04-22T11:30Z",
            "document": {
              "type": "idcard",
              "issuer": {
                "name": "Stadt Augsburg",
                "country": "DE"
              },
              "number": "53554554",
              "date_of_issuance": "2010-03-23",
              "date_of_expiry": "2020-03-22"
            }
          },
          "evidence2"
        ]
      },
      "claims": {
        "given_name": "Max",
        "family_name": "Müller",
        "nationalities": [
          "DE"
        ],
        "birthdate": "1956-01-28",
        "place_of_birth": {
          "country": "IS",
          "locality": "Þykkvabæjarklaustur"
        },
        "address": {
          "locality": "Maxstadt",
          "postal_code": "12344",
          "country": "DE",
          "street_address": "Weidenstraße 22"
        }
      }
    },
    "birth_middle_name": "Timotheus",
    "salutation": "Dr.",
    "msisdn": "49123456789"
  });

  let mut disclosures: Vec<Disclosure> = vec![];
  let mut encoder = SdObjectEncoder::try_from(object.clone()).unwrap();
  let disclosure = encoder.conceal(&["verified_claims", "verification", "time"], None);
  disclosures.push(disclosure.unwrap());

  let disclosure = encoder.conceal_array_entry(&["verified_claims", "verification", "evidence"], 0, None);
  disclosures.push(disclosure.unwrap());

  let disclosure = encoder.conceal_array_entry(&["verified_claims", "verification", "evidence"], 1, None);
  disclosures.push(disclosure.unwrap());

  let disclosure = encoder.conceal(&["verified_claims", "verification", "evidence"], None);
  disclosures.push(disclosure.unwrap());

  let disclosure = encoder.conceal(&["verified_claims", "claims", "place_of_birth", "locality"], None);
  disclosures.push(disclosure.unwrap());

  let disclosure = encoder.conceal(&["verified_claims", "claims"], None);
  disclosures.push(disclosure.unwrap());

  println!(
    "encoded object: {}",
    serde_json::to_string_pretty(&encoder.object()).unwrap()
  );
  // Create the JWT.
  // Creating JWTs is out of the scope of this library, josekit is used here as an example
  let mut header = JwsHeader::new();
  header.set_token_type("SD-JWT");

  // Use the encoded object as a payload for the JWT.
  let payload = JwtPayload::from_map(encoder.object().clone()).unwrap();
  let key = b"0123456789ABCDEF0123456789ABCDEF";
  let signer = HS256.signer_from_bytes(key).unwrap();
  let jwt = jwt::encode_with_signer(&payload, &header, &signer).unwrap();

  // Create an SD_JWT by collecting the disclosures and creating an `SdJwt` instance.
  let disclosures: Vec<String> = disclosures
    .into_iter()
    .map(|disclosure| disclosure.to_string())
    .collect();
  let sd_jwt: SdJwt = SdJwt::new(jwt, disclosures.clone(), None);
  let sd_jwt: String = sd_jwt.presentation();

  // Decoding the SD-JWT
  // Extract the payload from the JWT of the SD-JWT after verifying the signature.
  let sd_jwt: SdJwt = SdJwt::parse(&sd_jwt).unwrap();
  let verifier = HS256.verifier_from_bytes(key).unwrap();
  let (payload, _header) = jwt::decode_with_verifier(&sd_jwt.jwt, &verifier).unwrap();

  // Decode the payload by providing the disclosures that were parsed from the SD-JWT.
  let decoder = SdObjectDecoder::new_with_sha256();
  let decoded = decoder.decode(payload.claims_set(), &sd_jwt.disclosures).unwrap();
  println!("decoded object: {}", serde_json::to_string_pretty(&decoded).unwrap());
  assert_eq!(Value::Object(decoded), object);
}

#[test]
fn concealed_object_in_array() {
  let mut disclosures: Vec<Disclosure> = vec![];
  let nested_object = json!({
    "test1": 123,
  });
  let mut encoder = SdObjectEncoder::try_from(nested_object.clone()).unwrap();
  let disclosure = encoder.conceal(&["test1"], None);
  disclosures.push(disclosure.unwrap());

  let object = json!({
        "test2": [
          "value1",
          encoder.object()
        ]
  });

  let expected = json!({
        "test2": [
          "value1",
          {
           "test1": 123,
          }
        ]
  });
  let mut encoder = SdObjectEncoder::try_from(object.clone()).unwrap();
  let disclosure = encoder.conceal_array_entry(&["test2"], 0, None);
  disclosures.push(disclosure.unwrap());
  let disclosure = encoder.conceal(&["test2"], None);
  disclosures.push(disclosure.unwrap());

  let disclosures: Vec<String> = disclosures
    .into_iter()
    .map(|disclosure| disclosure.to_string())
    .collect();
  let decoder = SdObjectDecoder::new_with_sha256();
  let decoded = decoder.decode(encoder.object(), &disclosures).unwrap();
  assert_eq!(Value::Object(decoded), expected);
}

#[test]
fn decode() {
  // Values taken from https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-06.html#name-example-2-handling-structur
  let sd_jwt = "eyJhbGciOiAiRVMyNTYifQ.eyJfc2QiOiBbIkM5aW5wNllvUmFFWFI0Mjd6WUpQN1FyazFXSF84YmR3T0FfWVVyVW5HUVUiLCAiS3VldDF5QWEwSElRdlluT1ZkNTloY1ZpTzlVZzZKMmtTZnFZUkJlb3d2RSIsICJNTWxkT0ZGekIyZDB1bWxtcFRJYUdlcmhXZFVfUHBZZkx2S2hoX2ZfOWFZIiwgIlg2WkFZT0lJMnZQTjQwVjd4RXhad1Z3ejd5Um1MTmNWd3Q1REw4Ukx2NGciLCAiWTM0em1JbzBRTExPdGRNcFhHd2pCZ0x2cjE3eUVoaFlUMEZHb2ZSLWFJRSIsICJmeUdwMFdUd3dQdjJKRFFsbjFsU2lhZW9iWnNNV0ExMGJRNTk4OS05RFRzIiwgIm9tbUZBaWNWVDhMR0hDQjB1eXd4N2ZZdW8zTUhZS08xNWN6LVJaRVlNNVEiLCAiczBCS1lzTFd4UVFlVTh0VmxsdE03TUtzSVJUckVJYTFQa0ptcXhCQmY1VSJdLCAiaXNzIjogImh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAiYWRkcmVzcyI6IHsiX3NkIjogWyI2YVVoelloWjdTSjFrVm1hZ1FBTzN1MkVUTjJDQzFhSGhlWnBLbmFGMF9FIiwgIkF6TGxGb2JrSjJ4aWF1cFJFUHlvSnotOS1OU2xkQjZDZ2pyN2ZVeW9IemciLCAiUHp6Y1Z1MHFiTXVCR1NqdWxmZXd6a2VzRDl6dXRPRXhuNUVXTndrclEtayIsICJiMkRrdzBqY0lGOXJHZzhfUEY4WmN2bmNXN3p3Wmo1cnlCV3ZYZnJwemVrIiwgImNQWUpISVo4VnUtZjlDQ3lWdWIyVWZnRWs4anZ2WGV6d0sxcF9KbmVlWFEiLCAiZ2xUM2hyU1U3ZlNXZ3dGNVVEWm1Xd0JUdzMyZ25VbGRJaGk4aEdWQ2FWNCIsICJydkpkNmlxNlQ1ZWptc0JNb0d3dU5YaDlxQUFGQVRBY2k0MG9pZEVlVnNBIiwgInVOSG9XWWhYc1poVkpDTkUyRHF5LXpxdDd0NjlnSkt5NVFhRnY3R3JNWDQiXX0sICJfc2RfYWxnIjogInNoYS0yNTYifQ.IjE4EfnYu1RZ1uz6yqtFh5Lppq36VC4VeSr-hLDFpZ9zqBNmMrT5JHLLXTuMJqKQp3NIzDsLaft4GK5bYyfqhg~WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgInJlZ2lvbiIsICJcdTZlMmZcdTUzM2EiXQ~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImNvdW50cnkiLCAiSlAiXQ~";
  let sd_jwt: SdJwt = SdJwt::parse(sd_jwt).unwrap();
  let (payload, _header) = jwt::decode_with_verifier(&sd_jwt.jwt, &DecoyJwsVerifier {}).unwrap();
  let decoder = SdObjectDecoder::new_with_sha256();
  let decoded: Map<String, Value> = decoder.decode(payload.claims_set(), &sd_jwt.disclosures).unwrap();
  let expected_object = json!({
    "address": {
      "country": "JP",
      "region": "港区"
    },
    "iss": "https://issuer.example.com",
    "iat": 1683000000,
    "exp": 1883000000
  }
  )
  .as_object()
  .unwrap()
  .clone();
  assert_eq!(expected_object, decoded);
}

// Boilerplate to allow extracting JWS payload without verifying the signature.
#[derive(Debug, Clone)]
struct DecoyJwsAlgorithm;
impl JwsAlgorithm for DecoyJwsAlgorithm {
  fn name(&self) -> &str {
    "ES256"
  }

  fn box_clone(&self) -> Box<dyn JwsAlgorithm> {
    Box::new(self.clone())
  }
}

#[derive(Debug, Clone)]
struct DecoyJwsVerifier;
impl JwsVerifier for DecoyJwsVerifier {
  fn algorithm(&self) -> &dyn josekit::jws::JwsAlgorithm {
    &DecoyJwsAlgorithm {}
  }

  fn key_id(&self) -> Option<&str> {
    None
  }

  fn verify(&self, _message: &[u8], _signature: &[u8]) -> Result<(), josekit::JoseError> {
    Ok(())
  }

  fn box_clone(&self) -> Box<dyn JwsVerifier> {
    Box::new(self.clone())
  }
}