sd-jwt-payload 0.5.1

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

use async_trait::async_trait;
use josekit::jws::alg::hmac::HmacJwsSigner;
use josekit::jws::JwsHeader;
use josekit::jws::HS256;
use josekit::jwt;
use josekit::jwt::JwtPayload;
use sd_jwt_payload::Hasher;
use sd_jwt_payload::JsonObject;
use sd_jwt_payload::JwsSigner;
use sd_jwt_payload::KeyBindingJwt;
use sd_jwt_payload::Sha256Hasher;
use serde_json::json;
use serde_json::Value;

use sd_jwt_payload::SdJwt;
use sd_jwt_payload::SdJwtBuilder;

const HMAC_SECRET: &[u8; 32] = b"0123456789ABCDEF0123456789ABCDEF";

struct HmacSignerAdapter(HmacJwsSigner);

#[async_trait]
impl JwsSigner for HmacSignerAdapter {
  type Error = josekit::JoseError;
  async fn sign(&self, header: &JsonObject, payload: &JsonObject) -> Result<Vec<u8>, Self::Error> {
    let header = JwsHeader::from_map(header.clone())?;
    let payload = JwtPayload::from_map(payload.clone())?;

    jwt::encode_with_signer(&payload, &header, &self.0).map(String::into_bytes)
  }
}

async fn make_sd_jwt(object: Value, disclosable_values: impl IntoIterator<Item = &str>) -> SdJwt {
  let signer = HmacSignerAdapter(HS256.signer_from_bytes(HMAC_SECRET).unwrap());
  disclosable_values
    .into_iter()
    .fold(SdJwtBuilder::new(object).unwrap(), |builder, path| {
      builder.make_concealable(path).unwrap()
    })
    .finish(&signer, "HS256")
    .await
    .unwrap()
}

async fn make_kb_jwt(sd_jwt: &SdJwt, hasher: &dyn Hasher) -> KeyBindingJwt {
  let signer = HmacSignerAdapter(HS256.signer_from_bytes(HMAC_SECRET).unwrap());
  KeyBindingJwt::builder()
    .nonce("abcdefghi")
    .aud("https://example.com")
    .iat(1458304832)
    .finish(sd_jwt, hasher, "HS256", &signer)
    .await
    .unwrap()
}

#[test]
fn simple_sd_jwt() {
  // 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 = "eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOiBbIkM5aW5wNllvUmFFWFI0Mjd6WUpQN1FyazFXSF84YmR3T0FfWVVyVW5HUVUiLCAiS3VldDF5QWEwSElRdlluT1ZkNTloY1ZpTzlVZzZKMmtTZnFZUkJlb3d2RSIsICJNTWxkT0ZGekIyZDB1bWxtcFRJYUdlcmhXZFVfUHBZZkx2S2hoX2ZfOWFZIiwgIlg2WkFZT0lJMnZQTjQwVjd4RXhad1Z3ejd5Um1MTmNWd3Q1REw4Ukx2NGciLCAiWTM0em1JbzBRTExPdGRNcFhHd2pCZ0x2cjE3eUVoaFlUMEZHb2ZSLWFJRSIsICJmeUdwMFdUd3dQdjJKRFFsbjFsU2lhZW9iWnNNV0ExMGJRNTk4OS05RFRzIiwgIm9tbUZBaWNWVDhMR0hDQjB1eXd4N2ZZdW8zTUhZS08xNWN6LVJaRVlNNVEiLCAiczBCS1lzTFd4UVFlVTh0VmxsdE03TUtzSVJUckVJYTFQa0ptcXhCQmY1VSJdLCAiaXNzIjogImh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAiYWRkcmVzcyI6IHsiX3NkIjogWyI2YVVoelloWjdTSjFrVm1hZ1FBTzN1MkVUTjJDQzFhSGhlWnBLbmFGMF9FIiwgIkF6TGxGb2JrSjJ4aWF1cFJFUHlvSnotOS1OU2xkQjZDZ2pyN2ZVeW9IemciLCAiUHp6Y1Z1MHFiTXVCR1NqdWxmZXd6a2VzRDl6dXRPRXhuNUVXTndrclEtayIsICJiMkRrdzBqY0lGOXJHZzhfUEY4WmN2bmNXN3p3Wmo1cnlCV3ZYZnJwemVrIiwgImNQWUpISVo4VnUtZjlDQ3lWdWIyVWZnRWs4anZ2WGV6d0sxcF9KbmVlWFEiLCAiZ2xUM2hyU1U3ZlNXZ3dGNVVEWm1Xd0JUdzMyZ25VbGRJaGk4aEdWQ2FWNCIsICJydkpkNmlxNlQ1ZWptc0JNb0d3dU5YaDlxQUFGQVRBY2k0MG9pZEVlVnNBIiwgInVOSG9XWWhYc1poVkpDTkUyRHF5LXpxdDd0NjlnSkt5NVFhRnY3R3JNWDQiXX0sICJfc2RfYWxnIjogInNoYS0yNTYifQ.gR6rSL7urX79CNEvTQnP1MH5xthG11ucIV44SqKFZ4Pvlu_u16RfvXQd4k4CAIBZNKn2aTI18TfvFwV97gJFoA~WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgInJlZ2lvbiIsICJcdTZlMmZcdTUzM2EiXQ~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImNvdW50cnkiLCAiSlAiXQ~";
  let sd_jwt: SdJwt = SdJwt::parse(sd_jwt).unwrap();
  let disclosed = sd_jwt.into_disclosed_object(&Sha256Hasher::new()).unwrap();
  let expected_object = json!({
    "address": {
      "country": "JP",
      "region": "港区"
    },
    "iss": "https://issuer.example.com",
    "iat": 1683000000,
    "exp": 1883000000
  }
  );
  assert_eq!(expected_object.as_object().unwrap(), &disclosed);
}

#[tokio::test]
async fn concealing_parent_also_removes_all_sub_disclosures() -> anyhow::Result<()> {
  let hasher = Sha256Hasher::new();
  let sd_jwt = make_sd_jwt(
    json!({"parent": {"property1": "value1", "property2": [1, 2, 3]}}),
    ["/parent/property1", "/parent/property2/0", "/parent"],
  )
  .await;

  let removed_disclosures = sd_jwt.into_presentation(&hasher)?.conceal("/parent")?.finish().1;
  assert_eq!(removed_disclosures.len(), 3);

  Ok(())
}

#[tokio::test]
async fn concealing_property_of_concealable_value_works() -> anyhow::Result<()> {
  let hasher = Sha256Hasher::new();
  let sd_jwt = make_sd_jwt(
    json!({"parent": {"property1": "value1", "property2": [1, 2, 3]}}),
    ["/parent/property1", "/parent/property2/0", "/parent"],
  )
  .await;

  sd_jwt
    .into_presentation(&hasher)?
    .conceal("/parent/property2/0")?
    .finish();

  Ok(())
}

#[tokio::test]
async fn conceal_all_works() -> anyhow::Result<()> {
  let hasher = Sha256Hasher::new();
  let sd_jwt = make_sd_jwt(json!({"key1": "value1", "key2": "value2"}), ["/key1", "/key2"]).await;

  let (_, omitted_disclosures) = sd_jwt.into_presentation(&hasher)?.conceal_all().finish();

  assert_eq!(omitted_disclosures.len(), 2);

  Ok(())
}

#[tokio::test]
async fn disclose_works() -> anyhow::Result<()> {
  let hasher = Sha256Hasher::new();
  let sd_jwt = make_sd_jwt(
    json!({"parent": {"property1": "value1", "property2": [1, 2, 3]}, "array": ["be gentle im very sensitive information"]}),
    ["/parent/property1", "/parent/property2/0", "/parent", "/array/0"],
  )
  .await;

  let (presented_token, mut omitted_disclosures) = sd_jwt
    .into_presentation(&hasher)?
    .conceal_all()
    .disclose("/parent/property1")?
    .disclose("/array/0")?
    .finish();

  assert_eq!(
    omitted_disclosures.pop().map(|d| d.claim_value),
    Some(Value::Number(1.into()))
  );
  assert_eq!(presented_token.disclosures().len(), 3);

  Ok(())
}

#[tokio::test]
async fn sd_jwt_is_verifiable() -> anyhow::Result<()> {
  let sd_jwt = make_sd_jwt(json!({"key": "value"}), []).await;
  let jwt = sd_jwt.presentation().split_once('~').unwrap().0.to_string();
  let verifier = HS256.verifier_from_bytes(HMAC_SECRET)?;

  josekit::jwt::decode_with_verifier(&jwt, &verifier)?;
  Ok(())
}

#[tokio::test]
async fn sd_jwt_without_disclosures_works() -> anyhow::Result<()> {
  let hasher = Sha256Hasher::new();
  let sd_jwt = make_sd_jwt(json!({"parent": {"property1": "value1", "property2": [1, 2, 3]}}), []).await;
  // Try to serialize & deserialize `sd_jwt`.
  let mut sd_jwt = {
    let s = sd_jwt.to_string();
    s.parse::<SdJwt>()?
  };

  assert!(sd_jwt.disclosures().is_empty());
  assert!(sd_jwt.key_binding_jwt().is_none());

  let kb_jwt = make_kb_jwt(&sd_jwt, &hasher).await;
  sd_jwt.attach_key_binding_jwt(kb_jwt);
  // Try to serialize & deserialize `with_kb`.
  let with_kb = {
    let s = sd_jwt.to_string();
    s.parse::<SdJwt>()?
  };

  assert!(with_kb.disclosures().is_empty());
  assert!(with_kb.key_binding_jwt().is_some());

  Ok(())
}

#[tokio::test]
async fn kb_jwt_sd_hash_is_correct() -> anyhow::Result<()> {
  let sd_jwt = make_sd_jwt(json!({"key": "value"}), []).await;
  let kb_jwt = make_kb_jwt(&sd_jwt, &Sha256Hasher).await;

  let expected_sd_hash = Sha256Hasher.encoded_digest(&sd_jwt.to_string());
  assert_eq!(expected_sd_hash, kb_jwt.claims().sd_hash);

  Ok(())
}