From a2cd13b28f98c4e2b3c2b935590a16967cb7b55e Mon Sep 17 00:00:00 2001 From: Rob Sliwa Date: Fri, 15 Dec 2023 21:08:37 -0500 Subject: [PATCH] Initial commit. --- .github/workflows/ci.yml | 22 ++ .gitignore | 2 + Cargo.toml | 24 ++ LICENSE | 21 + README.md | 147 +++++++ src/algorithm/algorithm.rs | 18 + src/algorithm/hashalgorithm.rs | 159 ++++++++ src/algorithm/mod.rs | 5 + src/decoding.rs | 218 ++++++++++ src/disclosure.rs | 212 ++++++++++ src/disclosure_path.rs | 16 + src/encoding.rs | 104 +++++ src/error.rs | 34 ++ src/header.rs | 40 ++ src/holder.rs | 702 +++++++++++++++++++++++++++++++++ src/issuer.rs | 487 +++++++++++++++++++++++ src/jwk.rs | 29 ++ src/lib.rs | 29 ++ src/test_utils.rs | 106 +++++ src/utils.rs | 183 +++++++++ src/validation.rs | 51 +++ src/verifier.rs | 365 +++++++++++++++++ 22 files changed, 2974 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 src/algorithm/algorithm.rs create mode 100644 src/algorithm/hashalgorithm.rs create mode 100644 src/algorithm/mod.rs create mode 100644 src/decoding.rs create mode 100644 src/disclosure.rs create mode 100644 src/disclosure_path.rs create mode 100644 src/encoding.rs create mode 100644 src/error.rs create mode 100644 src/header.rs create mode 100644 src/holder.rs create mode 100644 src/issuer.rs create mode 100644 src/jwk.rs create mode 100644 src/lib.rs create mode 100644 src/test_utils.rs create mode 100644 src/utils.rs create mode 100644 src/validation.rs create mode 100644 src/verifier.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..31000a2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,22 @@ +name: Rust + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..b010460 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "sd-jwt" +version = "0.7.0" +authors = ["Rob Sliwa "] +license = "MIT" +readme = "README.md" +description = "SD-JWT support for Issuers, Holders, and Verifiers" +homepage = "https://github.com/robjsliwa/sd-jwt" +repository = "https://github.com/robjsliwa/sd-jwt" +keywords = ["sd-jwt", "sdjwt", "token", "sd_jwt", "selective disclosure"] +edition = "2021" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +thiserror = "1.0.51" +rand = "0.8.5" +base64 = "0.21.5" +sha2 = "0.10.8" +jsonwebtoken = "9.2.0" +chrono = "0.4.31" + +[dev-dependencies] +rsa = "0.5" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0b1b2a3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Rob Śliwa + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..348d30f --- /dev/null +++ b/README.md @@ -0,0 +1,147 @@ +# sd-jwt + +This crate implement draft of [SD-JWT](https://datatracker.ietf.org/doc/draft-ietf-oauth-selective-disclosure-jwt/), currently at version 7. + +# Installation + +Add the following to Cargo.toml: + +``` +sd-jwt = "0.7.0" +``` + +# How to use + +There are three use cases: Issuer, Holder, and Verifier. + +## Issuer + +The Issuer module represents an issuer of claims, issuing SD-JWT with all disclosures. +Key features include: + +- Creating new issuers with custom claims. +- Marking claims as disclosable. +- Optionally requiring a key binding. +- Encoding the issuer's claims into a SD-JWT. + +Example: +```rust +use sd_jwt::{Issuer, Jwk, Error, KeyForEncoding}; +use serde_json::Value; +const ISSUER_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 ISSUER_SIGNING_KEY_PEM: &str = "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDSwzyVZp2AIxS3\n802n0AfwKsMUcMYATMM6kK5VVS21ku3d6QC8kfhvJ0Pcb24dmGUWAJ95H9m19qDF\nbLrVZ9b4iobOsNlXNhKn4TRrsVFa8EaGXAJjGNRPPcL+gFwfV9y3tfR00tkokhR5\nZhhMifwKJf55QlEzY96yyk8ISzhagwO6Kf/E980Eoby1tvhX8q8HIwLG4GjFnmXx\nbKqxVQR1T07vFKHsF1MK8/d6a7+samHPWjoSlLvKSE4rdK8gouRpN/5Who4iS2s7\nlhfS2DcnxCnxj9S9BBm4GIQNk0Tc+lR20btBm+JiehAyEV9vX222BVSLUC9z9HGD\nk39b9ezbAgMBAAECggEBAIXuRxtxX/jDUjEqzVgsXD8EDX95wnkCTrVypzXWsPtH\naRyxKiSqZcLMotT7gnAQHXyD3NMtqD13geazF27xU6wQ62WBADvpQqWn+JXO0jIF\nqetLoMC0UIYiaz0q+F96h+m+GJ/8NL8RRS138U0CCkWwqysHN25+sk/PO7W7hw4M\nOAN/97rBkXqyzJJSvNwl2A66ga+9WC8G/9YgweqkS6re6WAyo4z1KyZAE1r655JR\nEaiIR6GYvahNsy/dNjVtGR189o8bf6xnTPbDUXQ/D61nO3Kg3B7Ca/uQWiDbI9VJ\nMXZxgip9Q7Qil9WuK1vVCUSf6WK38NV6r9fubw/DgsECgYEA70drCiGrC3pvIJF0\nLJL46H6x6SFClR876BZEnN51udJGXRstWV+Ya6NULSTykwusaTYUnr2BC6r3tT4S\nrRLfnXTaI0Tr6Bws6kBSJJC0CS0lLqK2tlKbcypQXv0T6Ulv2NXDq0VqQB3txED6\n8m5GieppHNueqLQqGqM1V4JYw5ECgYEA4X2s7ccLB8MX01j4T6Fnj4BGaZsyc1kV\nn6VHsuAsUxA9ZuwV+lk5k6xaWxDYmQR3xZ4XcQEntRUtGFu4TMLVpCcK26Vqafrp\nymbGjJGFagIaP9YOhQ+5ZMfO0obYUEaDGhPjXH3G9O/dTXoRg5nP5JvdcAnf853y\nm1BaYBHbG6sCgYAfVkQffI9RHoTFSCdl2w28LTORq6hzrTaES75KqRvT7UUH1pJW\n3R0yI57XlroqJeI7mTiUHY9z/r0YQHvjrNAaZ/5VliYrLN15BFl9rnHVrdLry6WQ\nNTtklssV1aEw8UwzorNQj/O9V+4WwMfczjJwx4FipSSfRZEqEevffROw8QKBgGNK\nba0+KjM+yuz7jkuyLOHZgCfcePilz4m+w7WWVK42xnLdnkfgpiPKjvbukhG/D+Zq\n2LOf6JYqPvMs4Bic6mof7v4M9rC4Fd5UJzWaln65ckmNvlMFO4OPIBk/21xt0CjZ\nfRIrKEKOpIoLKE8kmZB2uakuD/k8IaoWVdVbx3mFAoGAMFFWZAAHpB18WaATQRR6\n86JnudPD3TlOw+8Zw4tlOoGv4VXCPVsyAH8CWNSONyTRxeSJpe8Pn6ZvPJ7YBt6c\nchNSaqFIl9UnkMJ1ckE7EX2zKFCg3k8VzqYRLC9TcqqwKTJcNdRu1SbWkAds6Sd8\nKKRrCm+L44uQ01gUYvYYv5c=\n-----END PRIVATE KEY-----\n"; + +fn main() -> Result<(), Error> { + // holder's public key required for key binding + let holder_jwk = Jwk::from_value(serde_json::json!({ + "kty": "RSA", + "n": "...", + "e": "...", + "alg": "RS256", + "use": "sig", + }))?; + // create issuer's claims + let claims: Value = serde_json::from_str(ISSUER_CLAIMS).unwrap(); + let issuer = Issuer::new(claims)? + .disclosable("/given_name") + .disclosable("/family_name") + .disclosable("/address/street_address") + .disclosable("/address/locality") + .disclosable("/nationalities/0") + .disclosable("/nationalities/1") + .require_key_binding(holder_jwk) + .encode(&KeyForEncoding::from_rsa_pem( + ISSUER_SIGNING_KEY_PEM.as_bytes(), + )?)?; + Ok(()) +} +``` + +## Holder + +The Holder module represents a Holder, presenting SD-JWT including selected disclosures. +Key features include: + +- Verifying SD-JWTs for authenticity and integrity. +- Creating presentations with selective disclosures and optional key binding. + +Example Verify SD-JWT from Issuer: +```rust +use sd_jwt::{Holder, Error, KeyForDecoding, Validation}; +const ISSUER_PUBKEY: &str = "-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEA2a7Pz5WA1AmtGfIxSKwB8vU9OL1ti7udYhvC6048l74loAlmJGps\n0hb4u64jv8sAmdGjYeya2Oza1dydtSmlLArMkbeAiSV/n+KKmK0mpA7D7R8ARLKK\n/BZG7Z/QaxEORJl1KspliBQ2mUJJbcFH+EUko9bAdWEWx9GLkRH2pDm9nMO2lTtE\nqzO+JBjnuEoTn/NZ9Ur4dQDf3nWLBwEFyyJfJ90Ga2f6LFeHL2cOcAbHiofW5NAa\nGqh/JWxf6dSClyOUG0Bpe+RV8t0hnFhIC7RFV0aVbp50sqTM4mwYtOPk/2qWVVMF\nBOaswXYbi0ADUc9CqIaGDCAWnmHrHL/J4wIDAQAB\n-----END RSA PUBLIC KEY-----\n"; +const ISSUER_SD_JWT: &str = "eyJ0eXAiOiJzZC1qd3QiLCJhbGciOiJSUzI1NiJ9.eyJfc2QiOlsiVFhsUEt1RjM1cDQ3ZW9XTlpEcklxS0w0R0JFaDBFWXJEQnBjNmFCWjUyQSIsIkdYWlpyVUlsdnBtaDB4b0h4WURadzFOZ211WXJrd1VVS09rNG1XTHZKYUEiXSwiX3NkX2FsZyI6InNoYS0yNTYiLCJhZGRyZXNzIjp7Il9zZCI6WyJiUjVKM21ULXQ0a05pZ0V0dDJ5RVd1MU92b0hVMzBmSTZ1RVdJd2ozZWJBIiwiczhicTVKeUtJaFFwcVR1Vl9hcVNtd090UVN5UHV1TUlUU2xINXg1UWI5RSJdLCJjb3VudHJ5IjoiVVMiLCJyZWdpb24iOiJBbnlzdGF0ZSJ9LCJiaXJ0aGRhdGUiOiIxOTQwLTAxLTAxIiwiY25mIjp7ImFsZyI6IlJTMjU2IiwiZSI6IkFRQUIiLCJrdHkiOiJSU0EiLCJuIjoiNS1EZDU0WHNNQU5UWm9KMllCcHVpWmFfYXpyMzJIcEJ3MUZjanA1d1UwWFBqbW9NQTdKVllDSk4wU05maDZ0dFhyWHhhYWhFNXdmUzd4S1E0N1ZvWXhYTjlLa3kxMzdDSUx0Q0xPWUJDZkdULWFRRXJKS0FJWUVORWtzbVNpU3k0VnVWRk1yTzlMOV9KTzViZk02QjZ6X3pickJYX2MxU2s0UFRLTnBqRTcxcTJHenU4ak5GdTR0c0JaOFFSdmtJVldxNGdxVklQNTFQQmZEcmNfTm53dk1aallGN2pfc0Z5eGg2ZExTVV96QkRrZjJOVWo4VXQ0M25vcW9YMGJoaE96aGdyTlpadGpFMTlrZGFlZTJYbjBweG0td3QzRjBxUjZxd2F2TFRJT21LVHE0OFdXSGxvUk5QWXpGbEo4OHNOaVNLeW9Ta0hXMG9SVDlscUhGX3ZRIiwidXNlIjoic2lnIn0sImVtYWlsIjoiam9obmRvZUBleGFtcGxlLmNvbSIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InhnU2FMYS1CNk03OWpwVWZtaE9Hb0pkSHdNS0RNR0s3eUVKdC0tX0xScDAifSx7Ii4uLiI6Im5vNWxNSkVJSmRWdHozS3lDMVRXVkk2T2tsQnZIMjFCOExOOVEzWkxWRmMifV0sInBob25lX251bWJlciI6IisxLTIwMi01NTUtMDEwMSIsInBob25lX251bWJlcl92ZXJpZmllZCI6dHJ1ZSwic3ViIjoidXNlcl80MiIsInVwZGF0ZWRfYXQiOjE1NzAwMDAwMDB9.K2h-DNDgnq6q61tSxm1Gv-Hfo46SD8rEcP7yLFxcAlQNKBY-l1-bpXCJcqVZ7jugs2lqng0Cf9e34tM1OPkU3R6Pi5kUMGSyJ2y2ifsaZhGLCgxzNKk5W2ZxdkehzZQ6nHy6iu4flbT92Szv0eBR0hmS3hYTCtHlE4xib9G2dKWTQigB4ylPMkoRzbiKjgkucGkxSLN5ZQRXdxkez19bk5Q9BwuNLQMKG0lanq4ZJWq1C4LPt_K0WhEntyTL6SxVxGfR5HaUSxeYPCCOWSz9AVyZ46DWZGRx48PbuXGgLDH1UJYIsMej2F89CU-3QkWUrFq9b-DCYCQMxbBBekeLog~WyJoV2xxekkxY3piQzhCMnF2Mm5vN3pBIiwiZ2l2ZW5fbmFtZSIsIkpvaG4iXQ~WyJ4NXdpQVg1Qks3MFNfYzhXX2Vybm5nIiwiZmFtaWx5X25hbWUiLCJEb2UiXQ~WyI4Q1BKSmNKV2tiOGVwT09yZkl5YUNRIiwic3RyZWV0X2FkZHJlc3MiLCIxMjMgTWFpbiBTdCJd~WyJDTGo2S0tjblA1M2taOG5kOWFueWxnIiwibG9jYWxpdHkiLCJBbnl0b3duIl0~WyI4UEVqT3FlY245cjhGY0llWThhRjh3IiwiVVMiXQ~WyJMR2hVZmV2Y0FkTGVUUEVzRnlCNi1BIiwiREUiXQ~"; +fn main() -> Result<(), Error> { + let mut validation = Validation::default().no_exp(); + let decoding_key = KeyForDecoding::from_rsa_pem(ISSUER_PUBKEY.as_bytes())?; + let (header, decoded_claims, disclosure_paths) = + Holder::verify(ISSUER_SD_JWT, &decoding_key, &validation)?; + println!("header: {:?}", header); + println!("claims: {:?}", decoded_claims); + Ok(()) +} +``` + +Example Create Presentation: +```rust +use sd_jwt::{Holder, Error, KeyForEncoding, Algorithm}; +fn main() -> Result<(), Error> { + let sd_jwt = "eyJ0eXAiOiJzZC1qd3QiLCJhbGciOiJSUzI1NiJ9.eyJfc2QiOlsiVFhsUEt1RjM1cDQ3ZW9XTlpEcklxS0w0R0JFaDBFWXJEQnBjNmFCWjUyQSIsIkdYWlpyVUlsdnBtaDB4b0h4WURadzFOZ211WXJrd1VVS09rNG1XTHZKYUEiXSwiX3NkX2FsZyI6InNoYS0yNTYiLCJhZGRyZXNzIjp7Il9zZCI6WyJiUjVKM21ULXQ0a05pZ0V0dDJ5RVd1MU92b0hVMzBmSTZ1RVdJd2ozZWJBIiwiczhicTVKeUtJaFFwcVR1Vl9hcVNtd090UVN5UHV1TUlUU2xINXg1UWI5RSJdLCJjb3VudHJ5IjoiVVMiLCJyZWdpb24iOiJBbnlzdGF0ZSJ9LCJiaXJ0aGRhdGUiOiIxOTQwLTAxLTAxIiwiY25mIjp7ImFsZyI6IlJTMjU2IiwiZSI6IkFRQUIiLCJrdHkiOiJSU0EiLCJuIjoiNS1EZDU0WHNNQU5UWm9KMllCcHVpWmFfYXpyMzJIcEJ3MUZjanA1d1UwWFBqbW9NQTdKVllDSk4wU05maDZ0dFhyWHhhYWhFNXdmUzd4S1E0N1ZvWXhYTjlLa3kxMzdDSUx0Q0xPWUJDZkdULWFRRXJKS0FJWUVORWtzbVNpU3k0VnVWRk1yTzlMOV9KTzViZk02QjZ6X3pickJYX2MxU2s0UFRLTnBqRTcxcTJHenU4ak5GdTR0c0JaOFFSdmtJVldxNGdxVklQNTFQQmZEcmNfTm53dk1aallGN2pfc0Z5eGg2ZExTVV96QkRrZjJOVWo4VXQ0M25vcW9YMGJoaE96aGdyTlpadGpFMTlrZGFlZTJYbjBweG0td3QzRjBxUjZxd2F2TFRJT21LVHE0OFdXSGxvUk5QWXpGbEo4OHNOaVNLeW9Ta0hXMG9SVDlscUhGX3ZRIiwidXNlIjoic2lnIn0sImVtYWlsIjoiam9obmRvZUBleGFtcGxlLmNvbSIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InhnU2FMYS1CNk03OWpwVWZtaE9Hb0pkSHdNS0RNR0s3eUVKdC0tX0xScDAifSx7Ii4uLiI6Im5vNWxNSkVJSmRWdHozS3lDMVRXVkk2T2tsQnZIMjFCOExOOVEzWkxWRmMifV0sInBob25lX251bWJlciI6IisxLTIwMi01NTUtMDEwMSIsInBob25lX251bWJlcl92ZXJpZmllZCI6dHJ1ZSwic3ViIjoidXNlcl80MiIsInVwZGF0ZWRfYXQiOjE1NzAwMDAwMDB9.K2h-DNDgnq6q61tSxm1Gv-Hfo46SD8rEcP7yLFxcAlQNKBY-l1-bpXCJcqVZ7jugs2lqng0Cf9e34tM1OPkU3R6Pi5kUMGSyJ2y2ifsaZhGLCgxzNKk5W2ZxdkehzZQ6nHy6iu4flbT92Szv0eBR0hmS3hYTCtHlE4xib9G2dKWTQigB4ylPMkoRzbiKjgkucGkxSLN5ZQRXdxkez19bk5Q9BwuNLQMKG0lanq4ZJWq1C4LPt_K0WhEntyTL6SxVxGfR5HaUSxeYPCCOWSz9AVyZ46DWZGRx48PbuXGgLDH1UJYIsMej2F89CU-3QkWUrFq9b-DCYCQMxbBBekeLog~WyJoV2xxekkxY3piQzhCMnF2Mm5vN3pBIiwiZ2l2ZW5fbmFtZSIsIkpvaG4iXQ~WyJ4NXdpQVg1Qks3MFNfYzhXX2Vybm5nIiwiZmFtaWx5X25hbWUiLCJEb2UiXQ~WyI4Q1BKSmNKV2tiOGVwT09yZkl5YUNRIiwic3RyZWV0X2FkZHJlc3MiLCIxMjMgTWFpbiBTdCJd~WyJDTGo2S0tjblA1M2taOG5kOWFueWxnIiwibG9jYWxpdHkiLCJBbnl0b3duIl0~WyI4UEVqT3FlY245cjhGY0llWThhRjh3IiwiVVMiXQ~WyJMR2hVZmV2Y0FkTGVUUEVzRnlCNi1BIiwiREUiXQ~"; + let holder_private_key: &str = "-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDUhGTgOOW+FQwC\nQHKFGMvdV5l5P6GffWTZtmQ2QW2x2ncfXR2HCdtETl+qtoD9FQ0+ZOFzaeXEMzGU\nVdoSh8AWsq7UgWOmeQkqWR8qBaRY8rMHYnTyUL9bOWfy8mTI7vidRYwMNfg/9weD\nKSCAELhmlKyN1xsIzd3oBbVE5ma02+Q8q2phK7p3lznYguxWzn4Bykx2ZVcGdTKa\ny5MQATYRJlnoMRfTsTlHjyfp7hFlUNUmBQ5jYFNtAL+HZ6Uoa+NaQwiZLE+fD+Or\n7xrDnWl9GkZt8ZQW/bK5YZWr0Tmbm/iYoaSQKuKVun57NDvJKCgmL+njigpAIBCv\n1wwYiSGpAgMBAAECggEBAIrGWclB3mSeAdWGmEHpy1ai2Ymfz78Cd1TkEdSMLUGy\n048bkyiXeyPDuh0USG77zEYuQjrHsE7Kz1l6JolrNDiePiRuyc/vwdhxkjQysvuS\noO31kUCbEhpUBllTiBTeWGL7A1UF+TJr8e/ob1yxjnkOJRAKo5DAPmRBNfnkKrV2\noZdR4v6suy5syacBgr1whoLtLrQhfAClReQ9HOfmw0QOm7PwO807ywhfIwMYPhn8\nGLaA/3w4qGK6y3GmhFj53SnFk4wu9ifXmMroo8/T5wbXdXeGQRZGwOQk2h2TkaRr\nOHC94WYBs7wx4qIjDHDqsWqIRXTNmpTNDsXzTmUlkgECgYEA6WDy+3ELcnbG9Uvs\n0Q9Wdm8yc/P9lWZ+AiRdKHfGLOSxWz8o5Z7sdFTL9x+IGT2btrV1nDHPk2pb5muU\n7gLU9p57wTWq36NqH2OXkCT4iqP9v2mp9fi1fSLqAFsnLxwQIZtqlSRwbvnySx0f\n/oqfDRWNL5TMzYCLpbLtGhaTi5ECgYEA6R3JjTPwLQq+Xpt/iFKr21WCFf7BVwoH\nRv5GBRy4D9UibCk8XAvnJslnHxIpSDoeVfW021LZAeLlp5N/H/PCY146xNRzwsd5\npANsGlNGMkRKqGCwdtOCekpFiZN7yzvsDAlbOcwKsaQffr0oIaf3FhrLc8+SAQjx\ni9KGns8jOJkCgYEApAGlwF4pFT+zgh7hRenpcUGjyyjkRGHKm+bCMPY7JsFwghdY\nvkV5FiehTwGxu0s4aqYLCMFYhthvzPY9qyYCU238ukLk2lUU9woeMQZKQ+QLJsEy\n19D4egBXQfjNCKZID9YQiM8a1GKCi5bkLRVtwNwsZAvGAYUcnk2nonXLKoECgYEA\ngw0e4MXBEOFIUlFiqdWoDZ8NiaX1NSRLIQsTfA5AH453Uo0ABNMgOLriwSHpmVQq\n97Iw4Ve67YeMCeAuiFz1+/zeVwcEqQyRArZ10HreLKYdvnjU24hegrc8TnJeFsvy\nEHY2FdDydhlJJ2vZosoVaxTXKZ0YfIJ1oGBTE/Zo24kCgYBPyXEMr/ngR4UTLnIK\nbSJXlxgCZtkJt3dB2Usj+HQQKMGwYbp06/ILtwKeseIfSzTBMk/lsc3k4CAAoyp3\nj/XUIVc4hK4xoHK6lzI9oViagKZw8gZHs3tBoMhm1HKQbX0djl52yeeAZby83ugr\n0HEpFk7OJvra7z9Z0jjqIQwVEg==\n-----END PRIVATE KEY-----\n"; + let presentation = Holder::presentation(sd_jwt)? + .redact("/family_name")? + .key_binding( + "https://someone.example.com", + &KeyForEncoding::from_rsa_pem(holder_private_key.as_bytes())?, + Algorithm::RS256, + )? + .build()?; + println!("{:?}", presentation); + Ok(()) +} +``` + +## Verifier + +The Verifier module represents a Verifier, verifying SD-JWT presentations. + +Example +```rust +use sd_jwt::{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().no_exp(); + let mut kb_validation = Validation::default().no_exp(); + 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(()) +} +``` \ No newline at end of file diff --git a/src/algorithm/algorithm.rs b/src/algorithm/algorithm.rs new file mode 100644 index 0000000..c4398db --- /dev/null +++ b/src/algorithm/algorithm.rs @@ -0,0 +1,18 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub enum Algorithm { + HS256, + HS384, + HS512, + ES256, + ES384, + #[default] + RS256, + RS384, + RS512, + PS256, + PS384, + PS512, + EdDSA, +} diff --git a/src/algorithm/hashalgorithm.rs b/src/algorithm/hashalgorithm.rs new file mode 100644 index 0000000..a62d417 --- /dev/null +++ b/src/algorithm/hashalgorithm.rs @@ -0,0 +1,159 @@ +use crate::Error; +use base64::Engine; +use rand::{thread_rng, Rng}; +use sha2::{Digest, Sha256, Sha384, Sha512}; +use std::convert::TryFrom; + +pub(crate) fn generate_salt(len: usize) -> String { + let mut salt = vec![0u8; len]; + thread_rng().fill(&mut salt[..]); + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(salt) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HashAlgorithm { + SHA256, + SHA384, + SHA512, +} + +impl ToString for HashAlgorithm { + fn to_string(&self) -> String { + match self { + HashAlgorithm::SHA256 => "sha-256".to_string(), + HashAlgorithm::SHA384 => "sha-384".to_string(), + HashAlgorithm::SHA512 => "sha-512".to_string(), + } + } +} + +impl TryFrom<&str> for HashAlgorithm { + type Error = Error; + + fn try_from(s: &str) -> Result { + match s { + "sha-256" => Ok(HashAlgorithm::SHA256), + "sha-384" => Ok(HashAlgorithm::SHA384), + "sha-512" => Ok(HashAlgorithm::SHA512), + _ => Err(Error::InvalidHashAlgorithm(s.to_string())), + } + } +} + +enum Hasher { + Sha256(Sha256), + Sha384(Sha384), + Sha512(Sha512), +} + +impl Hasher { + fn new(algorithm: HashAlgorithm) -> Self { + match algorithm { + HashAlgorithm::SHA256 => Hasher::Sha256(Sha256::new()), + HashAlgorithm::SHA384 => Hasher::Sha384(Sha384::new()), + HashAlgorithm::SHA512 => Hasher::Sha512(Sha512::new()), + } + } + + fn update(&mut self, data: &[u8]) { + match self { + Hasher::Sha256(hasher) => hasher.update(data), + Hasher::Sha384(hasher) => hasher.update(data), + Hasher::Sha512(hasher) => hasher.update(data), + } + } + + fn finalize(self) -> Vec { + match self { + Hasher::Sha256(hasher) => hasher.finalize().to_vec(), + Hasher::Sha384(hasher) => hasher.finalize().to_vec(), + Hasher::Sha512(hasher) => hasher.finalize().to_vec(), + } + } +} + +pub fn base64_hash(algorithm: HashAlgorithm, data: &str) -> String { + let mut hasher = Hasher::new(algorithm); + + hasher.update(&data.to_string().into_bytes()); + let hash = hasher.finalize(); + + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(hash) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_generate_salt() { + let len = 16; + let salt = generate_salt(len); + // Length of base64 encoded string without padding + let expected_length = 4 * ((len + 2) / 3) - 2; + assert_eq!(salt.len(), expected_length); + // Ensure randomness + assert_ne!(generate_salt(len), generate_salt(len)); + } + + #[test] + fn test_hasher_new() { + if let Hasher::Sha256(_) = Hasher::new(HashAlgorithm::SHA256) { + } else { + panic!("Expected Sha256"); + } + if let Hasher::Sha384(_) = Hasher::new(HashAlgorithm::SHA384) { + } else { + panic!("Expected Sha384"); + } + if let Hasher::Sha512(_) = Hasher::new(HashAlgorithm::SHA512) { + } else { + panic!("Expected Sha512"); + } + } + + #[test] + fn test_hasher_update_finalize() { + let mut hasher = Hasher::new(HashAlgorithm::SHA256); + hasher.update(b"hello world"); + let hash = hasher.finalize(); + let expected_hash = Sha256::digest(b"hello world"); + assert_eq!(hash, expected_hash.to_vec()); + + let mut hasher = Hasher::new(HashAlgorithm::SHA384); + hasher.update(b"hello world"); + let hash = hasher.finalize(); + let expected_hash = Sha384::digest(b"hello world"); + assert_eq!(hash, expected_hash.to_vec()); + + let mut hasher = Hasher::new(HashAlgorithm::SHA512); + hasher.update(b"hello world"); + let hash = hasher.finalize(); + let expected_hash = Sha512::digest(b"hello world"); + assert_eq!(hash, expected_hash.to_vec()); + } + + #[test] + fn test_create_hash() { + let data = "hello world"; + let hash = base64_hash(HashAlgorithm::SHA256, data); + let expected_hash = Sha256::digest(data.as_bytes()); + let expected_base64 = + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(expected_hash); + assert_eq!(hash, expected_base64); + + let data = "hello world"; + let hash = base64_hash(HashAlgorithm::SHA384, data); + let expected_hash = Sha384::digest(data.as_bytes()); + let expected_base64 = + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(expected_hash); + assert_eq!(hash, expected_base64); + + let data = "hello world"; + let hash = base64_hash(HashAlgorithm::SHA512, data); + let expected_hash = Sha512::digest(data.as_bytes()); + let expected_base64 = + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(expected_hash); + assert_eq!(hash, expected_base64); + } +} diff --git a/src/algorithm/mod.rs b/src/algorithm/mod.rs new file mode 100644 index 0000000..8a06859 --- /dev/null +++ b/src/algorithm/mod.rs @@ -0,0 +1,5 @@ +pub mod algorithm; +pub mod hashalgorithm; + +pub use algorithm::*; +pub use hashalgorithm::*; diff --git a/src/decoding.rs b/src/decoding.rs new file mode 100644 index 0000000..fd6930a --- /dev/null +++ b/src/decoding.rs @@ -0,0 +1,218 @@ +use crate::{Algorithm, Error, Validation}; +use jsonwebtoken::DecodingKey; +use serde_json::Value; + +#[derive(Clone)] +pub struct KeyForDecoding { + key: DecodingKey, +} + +impl KeyForDecoding { + pub fn from_secret(secret: &[u8]) -> Self { + KeyForDecoding { + key: DecodingKey::from_secret(secret), + } + } + + pub fn from_base64_secret(secret: &str) -> Result { + Ok(KeyForDecoding { + key: DecodingKey::from_base64_secret(secret)?, + }) + } + + pub fn from_rsa_pem(key: &[u8]) -> Result { + Ok(KeyForDecoding { + key: DecodingKey::from_rsa_pem(key)?, + }) + } + + pub fn from_rsa_components(modulus: &str, exponent: &str) -> Result { + Ok(KeyForDecoding { + key: DecodingKey::from_rsa_components(modulus, exponent)?, + }) + } + + pub fn from_ec_pem(key: &[u8]) -> Result { + Ok(KeyForDecoding { + key: DecodingKey::from_ec_pem(key)?, + }) + } + + pub fn from_ed_pem(key: &[u8]) -> Result { + Ok(KeyForDecoding { + key: DecodingKey::from_ed_pem(key)?, + }) + } + + pub fn from_rsa_der(der: &[u8]) -> Self { + KeyForDecoding { + key: DecodingKey::from_rsa_der(der), + } + } + + pub fn from_ec_der(der: &[u8]) -> Self { + KeyForDecoding { + key: DecodingKey::from_ec_der(der), + } + } + + pub fn from_ed_der(der: &[u8]) -> Self { + KeyForDecoding { + key: DecodingKey::from_ed_der(der), + } + } +} + +fn build_validation(validation: &Validation) -> jsonwebtoken::Validation { + let mut valid = jsonwebtoken::Validation::new(match validation.algorithms { + Algorithm::HS256 => jsonwebtoken::Algorithm::HS256, + Algorithm::HS384 => jsonwebtoken::Algorithm::HS384, + Algorithm::HS512 => jsonwebtoken::Algorithm::HS512, + Algorithm::RS256 => jsonwebtoken::Algorithm::RS256, + Algorithm::RS384 => jsonwebtoken::Algorithm::RS384, + Algorithm::RS512 => jsonwebtoken::Algorithm::RS512, + Algorithm::ES256 => jsonwebtoken::Algorithm::ES256, + Algorithm::ES384 => jsonwebtoken::Algorithm::ES384, + Algorithm::PS256 => jsonwebtoken::Algorithm::PS256, + Algorithm::PS384 => jsonwebtoken::Algorithm::PS384, + Algorithm::PS512 => jsonwebtoken::Algorithm::PS512, + Algorithm::EdDSA => jsonwebtoken::Algorithm::EdDSA, + }); + valid.required_spec_claims = validation.required_spec_claims.clone(); + valid.leeway = validation.leeway; + valid.validate_exp = validation.validate_exp; + valid.validate_nbf = validation.validate_nbf; + valid.validate_aud = validation.validate_aud; + valid.aud = validation.aud.clone(); + valid.iss = validation.iss.clone(); + valid.sub = validation.sub.clone(); + valid +} + +pub fn decode( + token: &str, + key: &KeyForDecoding, + validation: &Validation, +) -> Result<(Value, Value), Error> { + let validation = build_validation(validation); + let token_data = jsonwebtoken::decode(token, &key.key, &validation)?; + let header: Value = serde_json::from_str(&serde_json::to_string(&token_data.header)?)?; + Ok((header, token_data.claims)) +} + +pub fn sd_jwt_parts(serialized_jwt: &str) -> (String, Vec, Option) { + let parts: Vec<&str> = serialized_jwt.split('~').collect(); + + let issuer_jwt = parts[0].to_string(); + + let disclosures = parts[1..parts.len() - 1] + .iter() + .map(|s| s.to_string()) + .collect(); + + let key_binding_jwt = if !parts[parts.len() - 1].is_empty() { + Some(parts[parts.len() - 1].to_string()) + } else { + None + }; + + (issuer_jwt, disclosures, key_binding_jwt) +} + +#[cfg(test)] +mod tests { + use super::*; + use chrono::{Duration, Utc}; + use rand::rngs::OsRng; + use rsa::{pkcs1::ToRsaPublicKey, pkcs8::ToPrivateKey, RsaPrivateKey, RsaPublicKey}; + + 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" + ] + }"#; + + fn keys() -> (RsaPrivateKey, RsaPublicKey) { + let mut rng = OsRng; + let bits = 2048; + let private_key = RsaPrivateKey::new(&mut rng, bits).unwrap(); + let public_key = RsaPublicKey::from(&private_key); + + (private_key, public_key) + } + + fn convert_to_pem(private_key: RsaPrivateKey, public_key: RsaPublicKey) -> (String, String) { + ( + private_key.to_pkcs8_pem().unwrap().to_string(), + public_key.to_pkcs1_pem().unwrap(), + ) + } + + #[test] + fn test_basic_decode() -> 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 = crate::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(); + // println!("issuer_jwt: {:?}", issuer_jwt); + 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(()) + } +} diff --git a/src/disclosure.rs b/src/disclosure.rs new file mode 100644 index 0000000..4f31fc0 --- /dev/null +++ b/src/disclosure.rs @@ -0,0 +1,212 @@ +use crate::algorithm::{base64_hash, generate_salt, HashAlgorithm}; +use crate::error::Error; +use base64::Engine; +use serde_json::Value; + +const ARRAY_DISCLOSURE_LEN: usize = 2; +const OBJECT_DISCLOSURE_LEN: usize = 3; + +#[derive(Debug, Clone)] +pub struct Disclosure { + disclosure: String, + digest: String, + key: Option, + value: Value, + salt_len: usize, + algorithm: HashAlgorithm, +} + +impl Disclosure { + const DEFAULT_SALT_LEN: usize = 16; + const DEFAULT_ALGORITHM: HashAlgorithm = HashAlgorithm::SHA256; + + pub fn new(key: Option, value: Value) -> Self { + Disclosure { + disclosure: "".to_string(), + digest: String::new(), + key, + value, + salt_len: Disclosure::DEFAULT_SALT_LEN, + algorithm: Disclosure::DEFAULT_ALGORITHM, + } + } + + pub fn salt_len(mut self, salt_len: usize) -> Self { + self.salt_len = salt_len; + self + } + + pub fn algorithm(mut self, algorithm: HashAlgorithm) -> Self { + self.algorithm = algorithm; + self + } + + pub fn get_algorithm(&self) -> HashAlgorithm { + self.algorithm + } + + pub fn build(self) -> Result { + let mut parts: Vec = Vec::with_capacity(3); + let salt = generate_salt(self.salt_len); + parts.push(salt.into()); + + match self.key.as_deref() { + Some("_sd") | Some("...") => { + return Err(Error::InvalidDisclosureKey(self.key.unwrap())); + } + Some(k) => parts.push(k.into()), + None => {} + } + + parts.push(self.value.clone()); + let disclosure = + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(serde_json::to_vec(&parts)?); + let digest = base64_hash(self.algorithm, &disclosure); + Ok(Disclosure { + disclosure, + digest, + key: self.key, + value: self.value, + salt_len: self.salt_len, + algorithm: self.algorithm, + }) + } + + pub fn disclosure(&self) -> &str { + &self.disclosure + } + + pub fn digest(&self) -> &String { + &self.digest + } + + pub fn key(&self) -> &Option { + &self.key + } + + pub fn value(&self) -> &Value { + &self.value + } + + pub fn from_base64(disclosure: &str, algorithm: HashAlgorithm) -> Result { + let decoded_disclosure = + base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(disclosure)?; + let decoded_disclosure = String::from_utf8(decoded_disclosure)?; + + let disclosure_json: Value = serde_json::from_str(decoded_disclosure.as_str())?; + let disclosure_array = disclosure_json + .as_array() + .ok_or(Error::InvalidDisclosureFormat(disclosure.to_string()))?; + match disclosure_array.len() { + ARRAY_DISCLOSURE_LEN | OBJECT_DISCLOSURE_LEN => { + reconstruct_disclosure(disclosure, algorithm, disclosure_array.as_slice()) + } + _ => Err(Error::InvalidDisclosureFormat(disclosure.to_string())), + } + } +} + +pub fn reconstruct_disclosure( + disclosure: &str, + algorithm: HashAlgorithm, + disclosure_array: &[Value], +) -> Result { + let digest = base64_hash(algorithm, disclosure); + let key = if disclosure_array.len() == OBJECT_DISCLOSURE_LEN { + Some(disclosure_array[1].as_str().unwrap_or_default().to_string()) + } else { + None + }; + let value = disclosure_array.last().unwrap().clone(); + + Ok(Disclosure { + disclosure: disclosure.to_string(), + digest, + key, + value, + salt_len: 0, + algorithm, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_new_disclosure() { + let value = serde_json::json!({"test": "value"}); + let disclosure = Disclosure::new(Some("key".to_string()), value.clone()); + assert_eq!(disclosure.salt_len, Disclosure::DEFAULT_SALT_LEN); + assert_eq!(disclosure.algorithm, Disclosure::DEFAULT_ALGORITHM); + assert_eq!(disclosure.key, Some("key".to_string())); + assert_eq!(disclosure.value, value); + } + + #[test] + fn test_salt_len_setter() { + let salt_length = 32; + let disclosure = + Disclosure::new(Some("key".to_string()), serde_json::Value::Null).salt_len(salt_length); + assert_eq!(disclosure.salt_len, salt_length); + } + + #[test] + fn test_algorithm_setter() { + let algorithm = HashAlgorithm::SHA512; + let disclosure = + Disclosure::new(Some("key".to_string()), serde_json::Value::Null).algorithm(algorithm); + assert_eq!(disclosure.algorithm, algorithm); + } + + #[test] + fn test_build_success() { + let disclosure = Disclosure::new(Some("key".to_string()), serde_json::Value::Null).build(); + assert!(disclosure.is_ok()); + } + + #[test] + fn test_build_invalid_key_error() { + let disclosure = Disclosure::new(Some("_sd".to_string()), serde_json::Value::Null).build(); + assert!(disclosure.is_err()); + } + + #[test] + fn test_disclosure_and_digest() { + let disclosure = Disclosure::new(Some("".to_string()), serde_json::Value::Null) + .build() + .unwrap(); + assert!(!disclosure.disclosure().is_empty()); + assert!(!disclosure.digest().is_empty()); + } + + #[test] + fn test_from_base64_for_object() { + let disclosure = Disclosure::new(Some("key".to_string()), serde_json::json!("some value")) + .build() + .unwrap(); + println!("disclosure: {}", disclosure.disclosure()); + let disclosure_from_base64 = + Disclosure::from_base64(disclosure.disclosure(), disclosure.algorithm).unwrap(); + assert_eq!(disclosure_from_base64.disclosure(), disclosure.disclosure()); + assert_eq!(disclosure_from_base64.digest(), disclosure.digest()); + assert_eq!(disclosure_from_base64.key, disclosure.key); + assert_eq!(disclosure_from_base64.value, disclosure.value); + assert_eq!(disclosure_from_base64.algorithm, disclosure.algorithm); + } + + #[test] + fn test_from_base64_for_array() { + let disclosure = Disclosure::new(None, serde_json::json!("some value")) + .build() + .unwrap(); + println!("disclosure: {}", disclosure.disclosure()); + let disclosure_from_base64 = + Disclosure::from_base64(disclosure.disclosure(), disclosure.algorithm).unwrap(); + assert_eq!(disclosure_from_base64.disclosure(), disclosure.disclosure()); + assert_eq!(disclosure_from_base64.digest(), disclosure.digest()); + assert_eq!(disclosure_from_base64.key, disclosure.key); + assert_eq!(disclosure_from_base64.value, disclosure.value); + assert_eq!(disclosure_from_base64.algorithm, disclosure.algorithm); + } +} diff --git a/src/disclosure_path.rs b/src/disclosure_path.rs new file mode 100644 index 0000000..0ef96de --- /dev/null +++ b/src/disclosure_path.rs @@ -0,0 +1,16 @@ +use crate::Disclosure; + +#[derive(Debug, Clone)] +pub struct DisclosurePath { + pub path: String, + pub disclosure: Disclosure, +} + +impl DisclosurePath { + pub fn new(path: &str, disclosure: &Disclosure) -> Self { + DisclosurePath { + path: path.to_string(), + disclosure: disclosure.clone(), + } + } +} diff --git a/src/encoding.rs b/src/encoding.rs new file mode 100644 index 0000000..1960df7 --- /dev/null +++ b/src/encoding.rs @@ -0,0 +1,104 @@ +use crate::Algorithm; +use crate::Error; +use crate::Header; +use jsonwebtoken::EncodingKey; +use serde::Serialize; + +#[derive(Clone)] +pub struct KeyForEncoding { + key: EncodingKey, +} + +impl KeyForEncoding { + pub fn from_secret(secret: &[u8]) -> Self { + KeyForEncoding { + key: EncodingKey::from_secret(secret), + } + } + + pub fn from_base64_secret(secret: &str) -> Result { + Ok(KeyForEncoding { + key: EncodingKey::from_base64_secret(secret)?, + }) + } + + pub fn from_rsa_pem(key: &[u8]) -> Result { + Ok(KeyForEncoding { + key: EncodingKey::from_rsa_pem(key)?, + }) + } + + pub fn from_ec_pem(key: &[u8]) -> Result { + Ok(KeyForEncoding { + key: EncodingKey::from_ec_pem(key)?, + }) + } + + pub fn from_ed_pem(key: &[u8]) -> Result { + Ok(KeyForEncoding { + key: EncodingKey::from_ed_pem(key)?, + }) + } + + pub fn from_rsa_der(der: &[u8]) -> Self { + KeyForEncoding { + key: EncodingKey::from_rsa_der(der), + } + } + + pub fn from_ec_der(der: &[u8]) -> Self { + KeyForEncoding { + key: EncodingKey::from_ec_der(der), + } + } + + pub fn from_ed_der(der: &[u8]) -> Self { + KeyForEncoding { + key: EncodingKey::from_ed_der(der), + } + } +} + +fn build_header(header: &Header) -> Result { + let jwk = match &header.jwk { + Some(jwk) => Some(serde_json::from_value(jwk.clone())?), + None => None, + }; + Ok(jsonwebtoken::Header { + typ: header.typ.clone(), + alg: match header.alg { + Algorithm::HS256 => jsonwebtoken::Algorithm::HS256, + Algorithm::HS384 => jsonwebtoken::Algorithm::HS384, + Algorithm::HS512 => jsonwebtoken::Algorithm::HS512, + Algorithm::RS256 => jsonwebtoken::Algorithm::RS256, + Algorithm::RS384 => jsonwebtoken::Algorithm::RS384, + Algorithm::RS512 => jsonwebtoken::Algorithm::RS512, + Algorithm::ES256 => jsonwebtoken::Algorithm::ES256, + Algorithm::ES384 => jsonwebtoken::Algorithm::ES384, + Algorithm::PS256 => jsonwebtoken::Algorithm::PS256, + Algorithm::PS384 => jsonwebtoken::Algorithm::PS384, + Algorithm::PS512 => jsonwebtoken::Algorithm::PS512, + Algorithm::EdDSA => jsonwebtoken::Algorithm::EdDSA, + }, + cty: header.cty.clone(), + jku: header.jku.clone(), + jwk, + kid: header.kid.clone(), + x5u: header.x5u.clone(), + x5c: header.x5c.clone(), + x5t: header.x5t.clone(), + x5t_s256: header.x5t_s256.clone(), + }) +} + +pub fn encode( + header: &Header, + claims: &T, + key: &KeyForEncoding, +) -> Result { + Ok(jsonwebtoken::encode( + &build_header(header)?, + claims, + &key.key, + )?) +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..64df06f --- /dev/null +++ b/src/error.rs @@ -0,0 +1,34 @@ +use serde_json::Error as SerdeError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("failed to form disclosuer")] + DisclosureFailed(#[from] SerdeError), + #[error("invalid disclosure key {0}")] + InvalidDisclosureKey(String), + #[error("encoding key error")] + EncodingKeyError(#[from] jsonwebtoken::errors::Error), + #[error("invalid path pointer to disclosure")] + InvalidPathPointer, + #[error("invalid path pointer array index")] + InvalidPathPointerArrayIndex(#[from] std::num::ParseIntError), + #[error("invalid _sd type")] + InvalidSDType, + #[error("decoding error")] + DecodingError(#[from] base64::DecodeError), + #[error("from utf8 conversion error")] + FromUtf8Error(#[from] std::string::FromUtf8Error), + #[error("invalid disclosure format {0}")] + InvalidDisclosureFormat(String), + #[error("sd-jwt rejected {0}")] + SDJWTRejected(String), + #[error("invalid hash algorithm {0}")] + InvalidHashAlgorithm(String), + #[error("JWT must have exactly three parts")] + JwtMustHaveThreeParts, + #[error("Key Binding JWT is required for the presentation. Use .key_binding() to set it.")] + KeyBindingJWTRequired, + #[error("KB-JWT parameter missing: {0}")] + KeyBindingJWTParameterMissing(String), +} diff --git a/src/header.rs b/src/header.rs new file mode 100644 index 0000000..66776d1 --- /dev/null +++ b/src/header.rs @@ -0,0 +1,40 @@ +use crate::Algorithm; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Header { + pub typ: Option, + pub alg: Algorithm, + pub cty: Option, + pub jku: Option, + pub jwk: Option, + pub kid: Option, + pub x5u: Option, + pub x5c: Option>, + pub x5t: Option, + pub x5t_s256: Option, +} + +impl Header { + pub fn new(algorithm: Algorithm) -> Self { + Header { + typ: Some("sd-jwt".to_string()), + alg: algorithm, + cty: None, + jku: None, + jwk: None, + kid: None, + x5u: None, + x5c: None, + x5t: None, + x5t_s256: None, + } + } +} + +impl Default for Header { + fn default() -> Self { + Header::new(Algorithm::default()) + } +} diff --git a/src/holder.rs b/src/holder.rs new file mode 100644 index 0000000..76058a3 --- /dev/null +++ b/src/holder.rs @@ -0,0 +1,702 @@ +use crate::utils::{remove_digests, restore_disclosures}; +use crate::{ + base64_hash, decode, encode, sd_jwt_parts, + utils::{decode_claims_no_verification, generate_nonce, get_jwt_part, JWTPart}, + Algorithm, DisclosurePath, Error, HashAlgorithm, Header, KeyForDecoding, KeyForEncoding, + Validation, +}; +use chrono::Utc; +use serde_json::Value; + +/// # Holder Module +/// +/// Represents a Holder. Presents SD-JWT including selected disclosures. +/// +/// ## Features +/// +/// - Verifying SD-JWTs for authenticity and integrity. +/// - Creating presentations with selective disclosures and optional key binding. +/// +/// Example Verify SD-JWT from Issuer: +/// +/// ``` +/// use sd_jwt::{Holder, Error, KeyForDecoding, Validation}; +/// +/// const ISSUER_PUBKEY: &str = "-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEA2a7Pz5WA1AmtGfIxSKwB8vU9OL1ti7udYhvC6048l74loAlmJGps\n0hb4u64jv8sAmdGjYeya2Oza1dydtSmlLArMkbeAiSV/n+KKmK0mpA7D7R8ARLKK\n/BZG7Z/QaxEORJl1KspliBQ2mUJJbcFH+EUko9bAdWEWx9GLkRH2pDm9nMO2lTtE\nqzO+JBjnuEoTn/NZ9Ur4dQDf3nWLBwEFyyJfJ90Ga2f6LFeHL2cOcAbHiofW5NAa\nGqh/JWxf6dSClyOUG0Bpe+RV8t0hnFhIC7RFV0aVbp50sqTM4mwYtOPk/2qWVVMF\nBOaswXYbi0ADUc9CqIaGDCAWnmHrHL/J4wIDAQAB\n-----END RSA PUBLIC KEY-----\n"; +/// const ISSUER_SD_JWT: &str = "eyJ0eXAiOiJzZC1qd3QiLCJhbGciOiJSUzI1NiJ9.eyJfc2QiOlsiVFhsUEt1RjM1cDQ3ZW9XTlpEcklxS0w0R0JFaDBFWXJEQnBjNmFCWjUyQSIsIkdYWlpyVUlsdnBtaDB4b0h4WURadzFOZ211WXJrd1VVS09rNG1XTHZKYUEiXSwiX3NkX2FsZyI6InNoYS0yNTYiLCJhZGRyZXNzIjp7Il9zZCI6WyJiUjVKM21ULXQ0a05pZ0V0dDJ5RVd1MU92b0hVMzBmSTZ1RVdJd2ozZWJBIiwiczhicTVKeUtJaFFwcVR1Vl9hcVNtd090UVN5UHV1TUlUU2xINXg1UWI5RSJdLCJjb3VudHJ5IjoiVVMiLCJyZWdpb24iOiJBbnlzdGF0ZSJ9LCJiaXJ0aGRhdGUiOiIxOTQwLTAxLTAxIiwiY25mIjp7ImFsZyI6IlJTMjU2IiwiZSI6IkFRQUIiLCJrdHkiOiJSU0EiLCJuIjoiNS1EZDU0WHNNQU5UWm9KMllCcHVpWmFfYXpyMzJIcEJ3MUZjanA1d1UwWFBqbW9NQTdKVllDSk4wU05maDZ0dFhyWHhhYWhFNXdmUzd4S1E0N1ZvWXhYTjlLa3kxMzdDSUx0Q0xPWUJDZkdULWFRRXJKS0FJWUVORWtzbVNpU3k0VnVWRk1yTzlMOV9KTzViZk02QjZ6X3pickJYX2MxU2s0UFRLTnBqRTcxcTJHenU4ak5GdTR0c0JaOFFSdmtJVldxNGdxVklQNTFQQmZEcmNfTm53dk1aallGN2pfc0Z5eGg2ZExTVV96QkRrZjJOVWo4VXQ0M25vcW9YMGJoaE96aGdyTlpadGpFMTlrZGFlZTJYbjBweG0td3QzRjBxUjZxd2F2TFRJT21LVHE0OFdXSGxvUk5QWXpGbEo4OHNOaVNLeW9Ta0hXMG9SVDlscUhGX3ZRIiwidXNlIjoic2lnIn0sImVtYWlsIjoiam9obmRvZUBleGFtcGxlLmNvbSIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InhnU2FMYS1CNk03OWpwVWZtaE9Hb0pkSHdNS0RNR0s3eUVKdC0tX0xScDAifSx7Ii4uLiI6Im5vNWxNSkVJSmRWdHozS3lDMVRXVkk2T2tsQnZIMjFCOExOOVEzWkxWRmMifV0sInBob25lX251bWJlciI6IisxLTIwMi01NTUtMDEwMSIsInBob25lX251bWJlcl92ZXJpZmllZCI6dHJ1ZSwic3ViIjoidXNlcl80MiIsInVwZGF0ZWRfYXQiOjE1NzAwMDAwMDB9.K2h-DNDgnq6q61tSxm1Gv-Hfo46SD8rEcP7yLFxcAlQNKBY-l1-bpXCJcqVZ7jugs2lqng0Cf9e34tM1OPkU3R6Pi5kUMGSyJ2y2ifsaZhGLCgxzNKk5W2ZxdkehzZQ6nHy6iu4flbT92Szv0eBR0hmS3hYTCtHlE4xib9G2dKWTQigB4ylPMkoRzbiKjgkucGkxSLN5ZQRXdxkez19bk5Q9BwuNLQMKG0lanq4ZJWq1C4LPt_K0WhEntyTL6SxVxGfR5HaUSxeYPCCOWSz9AVyZ46DWZGRx48PbuXGgLDH1UJYIsMej2F89CU-3QkWUrFq9b-DCYCQMxbBBekeLog~WyJoV2xxekkxY3piQzhCMnF2Mm5vN3pBIiwiZ2l2ZW5fbmFtZSIsIkpvaG4iXQ~WyJ4NXdpQVg1Qks3MFNfYzhXX2Vybm5nIiwiZmFtaWx5X25hbWUiLCJEb2UiXQ~WyI4Q1BKSmNKV2tiOGVwT09yZkl5YUNRIiwic3RyZWV0X2FkZHJlc3MiLCIxMjMgTWFpbiBTdCJd~WyJDTGo2S0tjblA1M2taOG5kOWFueWxnIiwibG9jYWxpdHkiLCJBbnl0b3duIl0~WyI4UEVqT3FlY245cjhGY0llWThhRjh3IiwiVVMiXQ~WyJMR2hVZmV2Y0FkTGVUUEVzRnlCNi1BIiwiREUiXQ~"; +/// +/// fn main() -> Result<(), Error> { +/// let mut validation = Validation::default().no_exp(); +/// let decoding_key = KeyForDecoding::from_rsa_pem(ISSUER_PUBKEY.as_bytes())?; +/// let (header, decoded_claims, disclosure_paths) = +/// Holder::verify(ISSUER_SD_JWT, &decoding_key, &validation)?; +/// println!("header: {:?}", header); +/// println!("claims: {:?}", decoded_claims); +/// +/// Ok(()) +/// } +/// ``` +/// +/// Example Create Presentation: +/// +/// ```rust +/// use sd_jwt::{Holder, Error, KeyForEncoding, Algorithm}; +/// +/// fn main() -> Result<(), Error> { +/// let sd_jwt = "eyJ0eXAiOiJzZC1qd3QiLCJhbGciOiJSUzI1NiJ9.eyJfc2QiOlsiVFhsUEt1RjM1cDQ3ZW9XTlpEcklxS0w0R0JFaDBFWXJEQnBjNmFCWjUyQSIsIkdYWlpyVUlsdnBtaDB4b0h4WURadzFOZ211WXJrd1VVS09rNG1XTHZKYUEiXSwiX3NkX2FsZyI6InNoYS0yNTYiLCJhZGRyZXNzIjp7Il9zZCI6WyJiUjVKM21ULXQ0a05pZ0V0dDJ5RVd1MU92b0hVMzBmSTZ1RVdJd2ozZWJBIiwiczhicTVKeUtJaFFwcVR1Vl9hcVNtd090UVN5UHV1TUlUU2xINXg1UWI5RSJdLCJjb3VudHJ5IjoiVVMiLCJyZWdpb24iOiJBbnlzdGF0ZSJ9LCJiaXJ0aGRhdGUiOiIxOTQwLTAxLTAxIiwiY25mIjp7ImFsZyI6IlJTMjU2IiwiZSI6IkFRQUIiLCJrdHkiOiJSU0EiLCJuIjoiNS1EZDU0WHNNQU5UWm9KMllCcHVpWmFfYXpyMzJIcEJ3MUZjanA1d1UwWFBqbW9NQTdKVllDSk4wU05maDZ0dFhyWHhhYWhFNXdmUzd4S1E0N1ZvWXhYTjlLa3kxMzdDSUx0Q0xPWUJDZkdULWFRRXJKS0FJWUVORWtzbVNpU3k0VnVWRk1yTzlMOV9KTzViZk02QjZ6X3pickJYX2MxU2s0UFRLTnBqRTcxcTJHenU4ak5GdTR0c0JaOFFSdmtJVldxNGdxVklQNTFQQmZEcmNfTm53dk1aallGN2pfc0Z5eGg2ZExTVV96QkRrZjJOVWo4VXQ0M25vcW9YMGJoaE96aGdyTlpadGpFMTlrZGFlZTJYbjBweG0td3QzRjBxUjZxd2F2TFRJT21LVHE0OFdXSGxvUk5QWXpGbEo4OHNOaVNLeW9Ta0hXMG9SVDlscUhGX3ZRIiwidXNlIjoic2lnIn0sImVtYWlsIjoiam9obmRvZUBleGFtcGxlLmNvbSIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InhnU2FMYS1CNk03OWpwVWZtaE9Hb0pkSHdNS0RNR0s3eUVKdC0tX0xScDAifSx7Ii4uLiI6Im5vNWxNSkVJSmRWdHozS3lDMVRXVkk2T2tsQnZIMjFCOExOOVEzWkxWRmMifV0sInBob25lX251bWJlciI6IisxLTIwMi01NTUtMDEwMSIsInBob25lX251bWJlcl92ZXJpZmllZCI6dHJ1ZSwic3ViIjoidXNlcl80MiIsInVwZGF0ZWRfYXQiOjE1NzAwMDAwMDB9.K2h-DNDgnq6q61tSxm1Gv-Hfo46SD8rEcP7yLFxcAlQNKBY-l1-bpXCJcqVZ7jugs2lqng0Cf9e34tM1OPkU3R6Pi5kUMGSyJ2y2ifsaZhGLCgxzNKk5W2ZxdkehzZQ6nHy6iu4flbT92Szv0eBR0hmS3hYTCtHlE4xib9G2dKWTQigB4ylPMkoRzbiKjgkucGkxSLN5ZQRXdxkez19bk5Q9BwuNLQMKG0lanq4ZJWq1C4LPt_K0WhEntyTL6SxVxGfR5HaUSxeYPCCOWSz9AVyZ46DWZGRx48PbuXGgLDH1UJYIsMej2F89CU-3QkWUrFq9b-DCYCQMxbBBekeLog~WyJoV2xxekkxY3piQzhCMnF2Mm5vN3pBIiwiZ2l2ZW5fbmFtZSIsIkpvaG4iXQ~WyJ4NXdpQVg1Qks3MFNfYzhXX2Vybm5nIiwiZmFtaWx5X25hbWUiLCJEb2UiXQ~WyI4Q1BKSmNKV2tiOGVwT09yZkl5YUNRIiwic3RyZWV0X2FkZHJlc3MiLCIxMjMgTWFpbiBTdCJd~WyJDTGo2S0tjblA1M2taOG5kOWFueWxnIiwibG9jYWxpdHkiLCJBbnl0b3duIl0~WyI4UEVqT3FlY245cjhGY0llWThhRjh3IiwiVVMiXQ~WyJMR2hVZmV2Y0FkTGVUUEVzRnlCNi1BIiwiREUiXQ~"; +/// let holder_private_key: &str = "-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDUhGTgOOW+FQwC\nQHKFGMvdV5l5P6GffWTZtmQ2QW2x2ncfXR2HCdtETl+qtoD9FQ0+ZOFzaeXEMzGU\nVdoSh8AWsq7UgWOmeQkqWR8qBaRY8rMHYnTyUL9bOWfy8mTI7vidRYwMNfg/9weD\nKSCAELhmlKyN1xsIzd3oBbVE5ma02+Q8q2phK7p3lznYguxWzn4Bykx2ZVcGdTKa\ny5MQATYRJlnoMRfTsTlHjyfp7hFlUNUmBQ5jYFNtAL+HZ6Uoa+NaQwiZLE+fD+Or\n7xrDnWl9GkZt8ZQW/bK5YZWr0Tmbm/iYoaSQKuKVun57NDvJKCgmL+njigpAIBCv\n1wwYiSGpAgMBAAECggEBAIrGWclB3mSeAdWGmEHpy1ai2Ymfz78Cd1TkEdSMLUGy\n048bkyiXeyPDuh0USG77zEYuQjrHsE7Kz1l6JolrNDiePiRuyc/vwdhxkjQysvuS\noO31kUCbEhpUBllTiBTeWGL7A1UF+TJr8e/ob1yxjnkOJRAKo5DAPmRBNfnkKrV2\noZdR4v6suy5syacBgr1whoLtLrQhfAClReQ9HOfmw0QOm7PwO807ywhfIwMYPhn8\nGLaA/3w4qGK6y3GmhFj53SnFk4wu9ifXmMroo8/T5wbXdXeGQRZGwOQk2h2TkaRr\nOHC94WYBs7wx4qIjDHDqsWqIRXTNmpTNDsXzTmUlkgECgYEA6WDy+3ELcnbG9Uvs\n0Q9Wdm8yc/P9lWZ+AiRdKHfGLOSxWz8o5Z7sdFTL9x+IGT2btrV1nDHPk2pb5muU\n7gLU9p57wTWq36NqH2OXkCT4iqP9v2mp9fi1fSLqAFsnLxwQIZtqlSRwbvnySx0f\n/oqfDRWNL5TMzYCLpbLtGhaTi5ECgYEA6R3JjTPwLQq+Xpt/iFKr21WCFf7BVwoH\nRv5GBRy4D9UibCk8XAvnJslnHxIpSDoeVfW021LZAeLlp5N/H/PCY146xNRzwsd5\npANsGlNGMkRKqGCwdtOCekpFiZN7yzvsDAlbOcwKsaQffr0oIaf3FhrLc8+SAQjx\ni9KGns8jOJkCgYEApAGlwF4pFT+zgh7hRenpcUGjyyjkRGHKm+bCMPY7JsFwghdY\nvkV5FiehTwGxu0s4aqYLCMFYhthvzPY9qyYCU238ukLk2lUU9woeMQZKQ+QLJsEy\n19D4egBXQfjNCKZID9YQiM8a1GKCi5bkLRVtwNwsZAvGAYUcnk2nonXLKoECgYEA\ngw0e4MXBEOFIUlFiqdWoDZ8NiaX1NSRLIQsTfA5AH453Uo0ABNMgOLriwSHpmVQq\n97Iw4Ve67YeMCeAuiFz1+/zeVwcEqQyRArZ10HreLKYdvnjU24hegrc8TnJeFsvy\nEHY2FdDydhlJJ2vZosoVaxTXKZ0YfIJ1oGBTE/Zo24kCgYBPyXEMr/ngR4UTLnIK\nbSJXlxgCZtkJt3dB2Usj+HQQKMGwYbp06/ILtwKeseIfSzTBMk/lsc3k4CAAoyp3\nj/XUIVc4hK4xoHK6lzI9oViagKZw8gZHs3tBoMhm1HKQbX0djl52yeeAZby83ugr\n0HEpFk7OJvra7z9Z0jjqIQwVEg==\n-----END PRIVATE KEY-----\n"; +/// let presentation = Holder::presentation(sd_jwt)? +/// .redact("/family_name")? +/// .key_binding( +/// "https://someone.example.com", +/// &KeyForEncoding::from_rsa_pem(holder_private_key.as_bytes())?, +/// Algorithm::RS256, +/// )? +/// .build()?; +/// println!("{:?}", presentation); +/// Ok(()) +/// } +/// ``` +pub struct Holder { + sd_jwt: String, + redacted: Vec, + disclosure_paths: Vec, + aud: Option, + key: Option, + algorithm: Option, +} + +impl Holder { + /// Create a presentation from SD-JWT received from Issuer. + /// ```rust + /// use sd_jwt::Holder; + /// use sd_jwt::Error; + /// + /// fn main() -> Result<(), Error> { + /// let sd_jwt = "eyJ0eXAiOiJzZC1qd3QiLCJhbGciOiJSUzI1NiJ9.eyJfc2QiOlsiVFhsUEt1RjM1cDQ3ZW9XTlpEcklxS0w0R0JFaDBFWXJEQnBjNmFCWjUyQSIsIkdYWlpyVUlsdnBtaDB4b0h4WURadzFOZ211WXJrd1VVS09rNG1XTHZKYUEiXSwiX3NkX2FsZyI6InNoYS0yNTYiLCJhZGRyZXNzIjp7Il9zZCI6WyJiUjVKM21ULXQ0a05pZ0V0dDJ5RVd1MU92b0hVMzBmSTZ1RVdJd2ozZWJBIiwiczhicTVKeUtJaFFwcVR1Vl9hcVNtd090UVN5UHV1TUlUU2xINXg1UWI5RSJdLCJjb3VudHJ5IjoiVVMiLCJyZWdpb24iOiJBbnlzdGF0ZSJ9LCJiaXJ0aGRhdGUiOiIxOTQwLTAxLTAxIiwiY25mIjp7ImFsZyI6IlJTMjU2IiwiZSI6IkFRQUIiLCJrdHkiOiJSU0EiLCJuIjoiNS1EZDU0WHNNQU5UWm9KMllCcHVpWmFfYXpyMzJIcEJ3MUZjanA1d1UwWFBqbW9NQTdKVllDSk4wU05maDZ0dFhyWHhhYWhFNXdmUzd4S1E0N1ZvWXhYTjlLa3kxMzdDSUx0Q0xPWUJDZkdULWFRRXJKS0FJWUVORWtzbVNpU3k0VnVWRk1yTzlMOV9KTzViZk02QjZ6X3pickJYX2MxU2s0UFRLTnBqRTcxcTJHenU4ak5GdTR0c0JaOFFSdmtJVldxNGdxVklQNTFQQmZEcmNfTm53dk1aallGN2pfc0Z5eGg2ZExTVV96QkRrZjJOVWo4VXQ0M25vcW9YMGJoaE96aGdyTlpadGpFMTlrZGFlZTJYbjBweG0td3QzRjBxUjZxd2F2TFRJT21LVHE0OFdXSGxvUk5QWXpGbEo4OHNOaVNLeW9Ta0hXMG9SVDlscUhGX3ZRIiwidXNlIjoic2lnIn0sImVtYWlsIjoiam9obmRvZUBleGFtcGxlLmNvbSIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InhnU2FMYS1CNk03OWpwVWZtaE9Hb0pkSHdNS0RNR0s3eUVKdC0tX0xScDAifSx7Ii4uLiI6Im5vNWxNSkVJSmRWdHozS3lDMVRXVkk2T2tsQnZIMjFCOExOOVEzWkxWRmMifV0sInBob25lX251bWJlciI6IisxLTIwMi01NTUtMDEwMSIsInBob25lX251bWJlcl92ZXJpZmllZCI6dHJ1ZSwic3ViIjoidXNlcl80MiIsInVwZGF0ZWRfYXQiOjE1NzAwMDAwMDB9.K2h-DNDgnq6q61tSxm1Gv-Hfo46SD8rEcP7yLFxcAlQNKBY-l1-bpXCJcqVZ7jugs2lqng0Cf9e34tM1OPkU3R6Pi5kUMGSyJ2y2ifsaZhGLCgxzNKk5W2ZxdkehzZQ6nHy6iu4flbT92Szv0eBR0hmS3hYTCtHlE4xib9G2dKWTQigB4ylPMkoRzbiKjgkucGkxSLN5ZQRXdxkez19bk5Q9BwuNLQMKG0lanq4ZJWq1C4LPt_K0WhEntyTL6SxVxGfR5HaUSxeYPCCOWSz9AVyZ46DWZGRx48PbuXGgLDH1UJYIsMej2F89CU-3QkWUrFq9b-DCYCQMxbBBekeLog~WyJoV2xxekkxY3piQzhCMnF2Mm5vN3pBIiwiZ2l2ZW5fbmFtZSIsIkpvaG4iXQ~WyJ4NXdpQVg1Qks3MFNfYzhXX2Vybm5nIiwiZmFtaWx5X25hbWUiLCJEb2UiXQ~WyI4Q1BKSmNKV2tiOGVwT09yZkl5YUNRIiwic3RyZWV0X2FkZHJlc3MiLCIxMjMgTWFpbiBTdCJd~WyJDTGo2S0tjblA1M2taOG5kOWFueWxnIiwibG9jYWxpdHkiLCJBbnl0b3duIl0~WyI4UEVqT3FlY245cjhGY0llWThhRjh3IiwiVVMiXQ~WyJMR2hVZmV2Y0FkTGVUUEVzRnlCNi1BIiwiREUiXQ~"; + /// let presentation = Holder::presentation(sd_jwt)?; + /// Ok(()) + /// } + /// ``` + pub fn presentation(sd_jwt: &str) -> Result { + let (issuer_sd_jwt, disclosures, kb_jwt) = sd_jwt_parts(sd_jwt); + if kb_jwt.is_some() { + return Err(Error::SDJWTRejected( + ("Issuer SD JWT cannot contain key binding JWT").to_string(), + )); + } + + let issuer_jwt = get_jwt_part(issuer_sd_jwt.as_str(), JWTPart::Claims)?; + let mut issuer_jwt_claims = decode_claims_no_verification(issuer_jwt.as_str())?; + let algorithm = issuer_jwt_claims["_sd_alg"].as_str().unwrap_or(""); + let algorithm = HashAlgorithm::try_from(algorithm)?; + let mut disclosure_paths = Vec::new(); + restore_disclosures( + &mut issuer_jwt_claims, + &disclosures, + &mut disclosure_paths, + algorithm, + )?; + + Ok(Holder { + sd_jwt: issuer_sd_jwt, + redacted: Vec::new(), + disclosure_paths, + aud: None, + key: None, + algorithm: None, + }) + } + + /// Redact specific claims from the SD-JWT. + /// + /// ```rust + /// use sd_jwt::Holder; + /// use sd_jwt::Error; + /// + /// fn main() -> Result<(), Error> { + /// let sd_jwt = "eyJ0eXAiOiJzZC1qd3QiLCJhbGciOiJSUzI1NiJ9.eyJfc2QiOlsiVFhsUEt1RjM1cDQ3ZW9XTlpEcklxS0w0R0JFaDBFWXJEQnBjNmFCWjUyQSIsIkdYWlpyVUlsdnBtaDB4b0h4WURadzFOZ211WXJrd1VVS09rNG1XTHZKYUEiXSwiX3NkX2FsZyI6InNoYS0yNTYiLCJhZGRyZXNzIjp7Il9zZCI6WyJiUjVKM21ULXQ0a05pZ0V0dDJ5RVd1MU92b0hVMzBmSTZ1RVdJd2ozZWJBIiwiczhicTVKeUtJaFFwcVR1Vl9hcVNtd090UVN5UHV1TUlUU2xINXg1UWI5RSJdLCJjb3VudHJ5IjoiVVMiLCJyZWdpb24iOiJBbnlzdGF0ZSJ9LCJiaXJ0aGRhdGUiOiIxOTQwLTAxLTAxIiwiY25mIjp7ImFsZyI6IlJTMjU2IiwiZSI6IkFRQUIiLCJrdHkiOiJSU0EiLCJuIjoiNS1EZDU0WHNNQU5UWm9KMllCcHVpWmFfYXpyMzJIcEJ3MUZjanA1d1UwWFBqbW9NQTdKVllDSk4wU05maDZ0dFhyWHhhYWhFNXdmUzd4S1E0N1ZvWXhYTjlLa3kxMzdDSUx0Q0xPWUJDZkdULWFRRXJKS0FJWUVORWtzbVNpU3k0VnVWRk1yTzlMOV9KTzViZk02QjZ6X3pickJYX2MxU2s0UFRLTnBqRTcxcTJHenU4ak5GdTR0c0JaOFFSdmtJVldxNGdxVklQNTFQQmZEcmNfTm53dk1aallGN2pfc0Z5eGg2ZExTVV96QkRrZjJOVWo4VXQ0M25vcW9YMGJoaE96aGdyTlpadGpFMTlrZGFlZTJYbjBweG0td3QzRjBxUjZxd2F2TFRJT21LVHE0OFdXSGxvUk5QWXpGbEo4OHNOaVNLeW9Ta0hXMG9SVDlscUhGX3ZRIiwidXNlIjoic2lnIn0sImVtYWlsIjoiam9obmRvZUBleGFtcGxlLmNvbSIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InhnU2FMYS1CNk03OWpwVWZtaE9Hb0pkSHdNS0RNR0s3eUVKdC0tX0xScDAifSx7Ii4uLiI6Im5vNWxNSkVJSmRWdHozS3lDMVRXVkk2T2tsQnZIMjFCOExOOVEzWkxWRmMifV0sInBob25lX251bWJlciI6IisxLTIwMi01NTUtMDEwMSIsInBob25lX251bWJlcl92ZXJpZmllZCI6dHJ1ZSwic3ViIjoidXNlcl80MiIsInVwZGF0ZWRfYXQiOjE1NzAwMDAwMDB9.K2h-DNDgnq6q61tSxm1Gv-Hfo46SD8rEcP7yLFxcAlQNKBY-l1-bpXCJcqVZ7jugs2lqng0Cf9e34tM1OPkU3R6Pi5kUMGSyJ2y2ifsaZhGLCgxzNKk5W2ZxdkehzZQ6nHy6iu4flbT92Szv0eBR0hmS3hYTCtHlE4xib9G2dKWTQigB4ylPMkoRzbiKjgkucGkxSLN5ZQRXdxkez19bk5Q9BwuNLQMKG0lanq4ZJWq1C4LPt_K0WhEntyTL6SxVxGfR5HaUSxeYPCCOWSz9AVyZ46DWZGRx48PbuXGgLDH1UJYIsMej2F89CU-3QkWUrFq9b-DCYCQMxbBBekeLog~WyJoV2xxekkxY3piQzhCMnF2Mm5vN3pBIiwiZ2l2ZW5fbmFtZSIsIkpvaG4iXQ~WyJ4NXdpQVg1Qks3MFNfYzhXX2Vybm5nIiwiZmFtaWx5X25hbWUiLCJEb2UiXQ~WyI4Q1BKSmNKV2tiOGVwT09yZkl5YUNRIiwic3RyZWV0X2FkZHJlc3MiLCIxMjMgTWFpbiBTdCJd~WyJDTGo2S0tjblA1M2taOG5kOWFueWxnIiwibG9jYWxpdHkiLCJBbnl0b3duIl0~WyI4UEVqT3FlY245cjhGY0llWThhRjh3IiwiVVMiXQ~WyJMR2hVZmV2Y0FkTGVUUEVzRnlCNi1BIiwiREUiXQ~"; + /// let presentation = Holder::presentation(sd_jwt)? + /// .redact("/family_name")?; + /// Ok(()) + /// } + /// ``` + pub fn redact(&mut self, path: &str) -> Result<&mut Self, Error> { + self.redacted.push(path.to_string()); + Ok(self) + } + + /// Add key binding JWT if needed. + /// + /// ```rust + /// use sd_jwt::{Holder, Error, KeyForEncoding, Algorithm}; + /// + /// fn main() -> Result<(), Error> { + /// let sd_jwt = "eyJ0eXAiOiJzZC1qd3QiLCJhbGciOiJSUzI1NiJ9.eyJfc2QiOlsiVFhsUEt1RjM1cDQ3ZW9XTlpEcklxS0w0R0JFaDBFWXJEQnBjNmFCWjUyQSIsIkdYWlpyVUlsdnBtaDB4b0h4WURadzFOZ211WXJrd1VVS09rNG1XTHZKYUEiXSwiX3NkX2FsZyI6InNoYS0yNTYiLCJhZGRyZXNzIjp7Il9zZCI6WyJiUjVKM21ULXQ0a05pZ0V0dDJ5RVd1MU92b0hVMzBmSTZ1RVdJd2ozZWJBIiwiczhicTVKeUtJaFFwcVR1Vl9hcVNtd090UVN5UHV1TUlUU2xINXg1UWI5RSJdLCJjb3VudHJ5IjoiVVMiLCJyZWdpb24iOiJBbnlzdGF0ZSJ9LCJiaXJ0aGRhdGUiOiIxOTQwLTAxLTAxIiwiY25mIjp7ImFsZyI6IlJTMjU2IiwiZSI6IkFRQUIiLCJrdHkiOiJSU0EiLCJuIjoiNS1EZDU0WHNNQU5UWm9KMllCcHVpWmFfYXpyMzJIcEJ3MUZjanA1d1UwWFBqbW9NQTdKVllDSk4wU05maDZ0dFhyWHhhYWhFNXdmUzd4S1E0N1ZvWXhYTjlLa3kxMzdDSUx0Q0xPWUJDZkdULWFRRXJKS0FJWUVORWtzbVNpU3k0VnVWRk1yTzlMOV9KTzViZk02QjZ6X3pickJYX2MxU2s0UFRLTnBqRTcxcTJHenU4ak5GdTR0c0JaOFFSdmtJVldxNGdxVklQNTFQQmZEcmNfTm53dk1aallGN2pfc0Z5eGg2ZExTVV96QkRrZjJOVWo4VXQ0M25vcW9YMGJoaE96aGdyTlpadGpFMTlrZGFlZTJYbjBweG0td3QzRjBxUjZxd2F2TFRJT21LVHE0OFdXSGxvUk5QWXpGbEo4OHNOaVNLeW9Ta0hXMG9SVDlscUhGX3ZRIiwidXNlIjoic2lnIn0sImVtYWlsIjoiam9obmRvZUBleGFtcGxlLmNvbSIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InhnU2FMYS1CNk03OWpwVWZtaE9Hb0pkSHdNS0RNR0s3eUVKdC0tX0xScDAifSx7Ii4uLiI6Im5vNWxNSkVJSmRWdHozS3lDMVRXVkk2T2tsQnZIMjFCOExOOVEzWkxWRmMifV0sInBob25lX251bWJlciI6IisxLTIwMi01NTUtMDEwMSIsInBob25lX251bWJlcl92ZXJpZmllZCI6dHJ1ZSwic3ViIjoidXNlcl80MiIsInVwZGF0ZWRfYXQiOjE1NzAwMDAwMDB9.K2h-DNDgnq6q61tSxm1Gv-Hfo46SD8rEcP7yLFxcAlQNKBY-l1-bpXCJcqVZ7jugs2lqng0Cf9e34tM1OPkU3R6Pi5kUMGSyJ2y2ifsaZhGLCgxzNKk5W2ZxdkehzZQ6nHy6iu4flbT92Szv0eBR0hmS3hYTCtHlE4xib9G2dKWTQigB4ylPMkoRzbiKjgkucGkxSLN5ZQRXdxkez19bk5Q9BwuNLQMKG0lanq4ZJWq1C4LPt_K0WhEntyTL6SxVxGfR5HaUSxeYPCCOWSz9AVyZ46DWZGRx48PbuXGgLDH1UJYIsMej2F89CU-3QkWUrFq9b-DCYCQMxbBBekeLog~WyJoV2xxekkxY3piQzhCMnF2Mm5vN3pBIiwiZ2l2ZW5fbmFtZSIsIkpvaG4iXQ~WyJ4NXdpQVg1Qks3MFNfYzhXX2Vybm5nIiwiZmFtaWx5X25hbWUiLCJEb2UiXQ~WyI4Q1BKSmNKV2tiOGVwT09yZkl5YUNRIiwic3RyZWV0X2FkZHJlc3MiLCIxMjMgTWFpbiBTdCJd~WyJDTGo2S0tjblA1M2taOG5kOWFueWxnIiwibG9jYWxpdHkiLCJBbnl0b3duIl0~WyI4UEVqT3FlY245cjhGY0llWThhRjh3IiwiVVMiXQ~WyJMR2hVZmV2Y0FkTGVUUEVzRnlCNi1BIiwiREUiXQ~"; + /// let holder_private_key: &str = "-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDUhGTgOOW+FQwC\nQHKFGMvdV5l5P6GffWTZtmQ2QW2x2ncfXR2HCdtETl+qtoD9FQ0+ZOFzaeXEMzGU\nVdoSh8AWsq7UgWOmeQkqWR8qBaRY8rMHYnTyUL9bOWfy8mTI7vidRYwMNfg/9weD\nKSCAELhmlKyN1xsIzd3oBbVE5ma02+Q8q2phK7p3lznYguxWzn4Bykx2ZVcGdTKa\ny5MQATYRJlnoMRfTsTlHjyfp7hFlUNUmBQ5jYFNtAL+HZ6Uoa+NaQwiZLE+fD+Or\n7xrDnWl9GkZt8ZQW/bK5YZWr0Tmbm/iYoaSQKuKVun57NDvJKCgmL+njigpAIBCv\n1wwYiSGpAgMBAAECggEBAIrGWclB3mSeAdWGmEHpy1ai2Ymfz78Cd1TkEdSMLUGy\n048bkyiXeyPDuh0USG77zEYuQjrHsE7Kz1l6JolrNDiePiRuyc/vwdhxkjQysvuS\noO31kUCbEhpUBllTiBTeWGL7A1UF+TJr8e/ob1yxjnkOJRAKo5DAPmRBNfnkKrV2\noZdR4v6suy5syacBgr1whoLtLrQhfAClReQ9HOfmw0QOm7PwO807ywhfIwMYPhn8\nGLaA/3w4qGK6y3GmhFj53SnFk4wu9ifXmMroo8/T5wbXdXeGQRZGwOQk2h2TkaRr\nOHC94WYBs7wx4qIjDHDqsWqIRXTNmpTNDsXzTmUlkgECgYEA6WDy+3ELcnbG9Uvs\n0Q9Wdm8yc/P9lWZ+AiRdKHfGLOSxWz8o5Z7sdFTL9x+IGT2btrV1nDHPk2pb5muU\n7gLU9p57wTWq36NqH2OXkCT4iqP9v2mp9fi1fSLqAFsnLxwQIZtqlSRwbvnySx0f\n/oqfDRWNL5TMzYCLpbLtGhaTi5ECgYEA6R3JjTPwLQq+Xpt/iFKr21WCFf7BVwoH\nRv5GBRy4D9UibCk8XAvnJslnHxIpSDoeVfW021LZAeLlp5N/H/PCY146xNRzwsd5\npANsGlNGMkRKqGCwdtOCekpFiZN7yzvsDAlbOcwKsaQffr0oIaf3FhrLc8+SAQjx\ni9KGns8jOJkCgYEApAGlwF4pFT+zgh7hRenpcUGjyyjkRGHKm+bCMPY7JsFwghdY\nvkV5FiehTwGxu0s4aqYLCMFYhthvzPY9qyYCU238ukLk2lUU9woeMQZKQ+QLJsEy\n19D4egBXQfjNCKZID9YQiM8a1GKCi5bkLRVtwNwsZAvGAYUcnk2nonXLKoECgYEA\ngw0e4MXBEOFIUlFiqdWoDZ8NiaX1NSRLIQsTfA5AH453Uo0ABNMgOLriwSHpmVQq\n97Iw4Ve67YeMCeAuiFz1+/zeVwcEqQyRArZ10HreLKYdvnjU24hegrc8TnJeFsvy\nEHY2FdDydhlJJ2vZosoVaxTXKZ0YfIJ1oGBTE/Zo24kCgYBPyXEMr/ngR4UTLnIK\nbSJXlxgCZtkJt3dB2Usj+HQQKMGwYbp06/ILtwKeseIfSzTBMk/lsc3k4CAAoyp3\nj/XUIVc4hK4xoHK6lzI9oViagKZw8gZHs3tBoMhm1HKQbX0djl52yeeAZby83ugr\n0HEpFk7OJvra7z9Z0jjqIQwVEg==\n-----END PRIVATE KEY-----\n"; + /// let presentation = Holder::presentation(sd_jwt)? + /// .redact("/family_name")? + /// .key_binding( + /// "https://someone.example.com", + /// &KeyForEncoding::from_rsa_pem(holder_private_key.as_bytes())?, + /// Algorithm::RS256, + /// )?; + /// Ok(()) + /// } + /// ``` + pub fn key_binding( + &mut self, + aud: &str, + key: &KeyForEncoding, + algorithm: Algorithm, + ) -> Result<&mut Self, Error> { + self.aud = Some(aud.to_string()); + self.key = Some(key.clone()); + self.algorithm = Some(algorithm); + + Ok(self) + } + + /// Build the final presentation, ready for sharing or transmission. + /// + /// ```rust + /// use sd_jwt::{Holder, Error, KeyForEncoding, Algorithm}; + /// + /// fn main() -> Result<(), Error> { + /// let sd_jwt = "eyJ0eXAiOiJzZC1qd3QiLCJhbGciOiJSUzI1NiJ9.eyJfc2QiOlsiVFhsUEt1RjM1cDQ3ZW9XTlpEcklxS0w0R0JFaDBFWXJEQnBjNmFCWjUyQSIsIkdYWlpyVUlsdnBtaDB4b0h4WURadzFOZ211WXJrd1VVS09rNG1XTHZKYUEiXSwiX3NkX2FsZyI6InNoYS0yNTYiLCJhZGRyZXNzIjp7Il9zZCI6WyJiUjVKM21ULXQ0a05pZ0V0dDJ5RVd1MU92b0hVMzBmSTZ1RVdJd2ozZWJBIiwiczhicTVKeUtJaFFwcVR1Vl9hcVNtd090UVN5UHV1TUlUU2xINXg1UWI5RSJdLCJjb3VudHJ5IjoiVVMiLCJyZWdpb24iOiJBbnlzdGF0ZSJ9LCJiaXJ0aGRhdGUiOiIxOTQwLTAxLTAxIiwiY25mIjp7ImFsZyI6IlJTMjU2IiwiZSI6IkFRQUIiLCJrdHkiOiJSU0EiLCJuIjoiNS1EZDU0WHNNQU5UWm9KMllCcHVpWmFfYXpyMzJIcEJ3MUZjanA1d1UwWFBqbW9NQTdKVllDSk4wU05maDZ0dFhyWHhhYWhFNXdmUzd4S1E0N1ZvWXhYTjlLa3kxMzdDSUx0Q0xPWUJDZkdULWFRRXJKS0FJWUVORWtzbVNpU3k0VnVWRk1yTzlMOV9KTzViZk02QjZ6X3pickJYX2MxU2s0UFRLTnBqRTcxcTJHenU4ak5GdTR0c0JaOFFSdmtJVldxNGdxVklQNTFQQmZEcmNfTm53dk1aallGN2pfc0Z5eGg2ZExTVV96QkRrZjJOVWo4VXQ0M25vcW9YMGJoaE96aGdyTlpadGpFMTlrZGFlZTJYbjBweG0td3QzRjBxUjZxd2F2TFRJT21LVHE0OFdXSGxvUk5QWXpGbEo4OHNOaVNLeW9Ta0hXMG9SVDlscUhGX3ZRIiwidXNlIjoic2lnIn0sImVtYWlsIjoiam9obmRvZUBleGFtcGxlLmNvbSIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InhnU2FMYS1CNk03OWpwVWZtaE9Hb0pkSHdNS0RNR0s3eUVKdC0tX0xScDAifSx7Ii4uLiI6Im5vNWxNSkVJSmRWdHozS3lDMVRXVkk2T2tsQnZIMjFCOExOOVEzWkxWRmMifV0sInBob25lX251bWJlciI6IisxLTIwMi01NTUtMDEwMSIsInBob25lX251bWJlcl92ZXJpZmllZCI6dHJ1ZSwic3ViIjoidXNlcl80MiIsInVwZGF0ZWRfYXQiOjE1NzAwMDAwMDB9.K2h-DNDgnq6q61tSxm1Gv-Hfo46SD8rEcP7yLFxcAlQNKBY-l1-bpXCJcqVZ7jugs2lqng0Cf9e34tM1OPkU3R6Pi5kUMGSyJ2y2ifsaZhGLCgxzNKk5W2ZxdkehzZQ6nHy6iu4flbT92Szv0eBR0hmS3hYTCtHlE4xib9G2dKWTQigB4ylPMkoRzbiKjgkucGkxSLN5ZQRXdxkez19bk5Q9BwuNLQMKG0lanq4ZJWq1C4LPt_K0WhEntyTL6SxVxGfR5HaUSxeYPCCOWSz9AVyZ46DWZGRx48PbuXGgLDH1UJYIsMej2F89CU-3QkWUrFq9b-DCYCQMxbBBekeLog~WyJoV2xxekkxY3piQzhCMnF2Mm5vN3pBIiwiZ2l2ZW5fbmFtZSIsIkpvaG4iXQ~WyJ4NXdpQVg1Qks3MFNfYzhXX2Vybm5nIiwiZmFtaWx5X25hbWUiLCJEb2UiXQ~WyI4Q1BKSmNKV2tiOGVwT09yZkl5YUNRIiwic3RyZWV0X2FkZHJlc3MiLCIxMjMgTWFpbiBTdCJd~WyJDTGo2S0tjblA1M2taOG5kOWFueWxnIiwibG9jYWxpdHkiLCJBbnl0b3duIl0~WyI4UEVqT3FlY245cjhGY0llWThhRjh3IiwiVVMiXQ~WyJMR2hVZmV2Y0FkTGVUUEVzRnlCNi1BIiwiREUiXQ~"; + /// let holder_private_key: &str = "-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDUhGTgOOW+FQwC\nQHKFGMvdV5l5P6GffWTZtmQ2QW2x2ncfXR2HCdtETl+qtoD9FQ0+ZOFzaeXEMzGU\nVdoSh8AWsq7UgWOmeQkqWR8qBaRY8rMHYnTyUL9bOWfy8mTI7vidRYwMNfg/9weD\nKSCAELhmlKyN1xsIzd3oBbVE5ma02+Q8q2phK7p3lznYguxWzn4Bykx2ZVcGdTKa\ny5MQATYRJlnoMRfTsTlHjyfp7hFlUNUmBQ5jYFNtAL+HZ6Uoa+NaQwiZLE+fD+Or\n7xrDnWl9GkZt8ZQW/bK5YZWr0Tmbm/iYoaSQKuKVun57NDvJKCgmL+njigpAIBCv\n1wwYiSGpAgMBAAECggEBAIrGWclB3mSeAdWGmEHpy1ai2Ymfz78Cd1TkEdSMLUGy\n048bkyiXeyPDuh0USG77zEYuQjrHsE7Kz1l6JolrNDiePiRuyc/vwdhxkjQysvuS\noO31kUCbEhpUBllTiBTeWGL7A1UF+TJr8e/ob1yxjnkOJRAKo5DAPmRBNfnkKrV2\noZdR4v6suy5syacBgr1whoLtLrQhfAClReQ9HOfmw0QOm7PwO807ywhfIwMYPhn8\nGLaA/3w4qGK6y3GmhFj53SnFk4wu9ifXmMroo8/T5wbXdXeGQRZGwOQk2h2TkaRr\nOHC94WYBs7wx4qIjDHDqsWqIRXTNmpTNDsXzTmUlkgECgYEA6WDy+3ELcnbG9Uvs\n0Q9Wdm8yc/P9lWZ+AiRdKHfGLOSxWz8o5Z7sdFTL9x+IGT2btrV1nDHPk2pb5muU\n7gLU9p57wTWq36NqH2OXkCT4iqP9v2mp9fi1fSLqAFsnLxwQIZtqlSRwbvnySx0f\n/oqfDRWNL5TMzYCLpbLtGhaTi5ECgYEA6R3JjTPwLQq+Xpt/iFKr21WCFf7BVwoH\nRv5GBRy4D9UibCk8XAvnJslnHxIpSDoeVfW021LZAeLlp5N/H/PCY146xNRzwsd5\npANsGlNGMkRKqGCwdtOCekpFiZN7yzvsDAlbOcwKsaQffr0oIaf3FhrLc8+SAQjx\ni9KGns8jOJkCgYEApAGlwF4pFT+zgh7hRenpcUGjyyjkRGHKm+bCMPY7JsFwghdY\nvkV5FiehTwGxu0s4aqYLCMFYhthvzPY9qyYCU238ukLk2lUU9woeMQZKQ+QLJsEy\n19D4egBXQfjNCKZID9YQiM8a1GKCi5bkLRVtwNwsZAvGAYUcnk2nonXLKoECgYEA\ngw0e4MXBEOFIUlFiqdWoDZ8NiaX1NSRLIQsTfA5AH453Uo0ABNMgOLriwSHpmVQq\n97Iw4Ve67YeMCeAuiFz1+/zeVwcEqQyRArZ10HreLKYdvnjU24hegrc8TnJeFsvy\nEHY2FdDydhlJJ2vZosoVaxTXKZ0YfIJ1oGBTE/Zo24kCgYBPyXEMr/ngR4UTLnIK\nbSJXlxgCZtkJt3dB2Usj+HQQKMGwYbp06/ILtwKeseIfSzTBMk/lsc3k4CAAoyp3\nj/XUIVc4hK4xoHK6lzI9oViagKZw8gZHs3tBoMhm1HKQbX0djl52yeeAZby83ugr\n0HEpFk7OJvra7z9Z0jjqIQwVEg==\n-----END PRIVATE KEY-----\n"; + /// let presentation = Holder::presentation(sd_jwt)? + /// .redact("/family_name")? + /// .key_binding( + /// "https://someone.example.com", + /// &KeyForEncoding::from_rsa_pem(holder_private_key.as_bytes())?, + /// Algorithm::RS256, + /// )? + /// .build()?; + /// println!("{:?}", presentation); + /// Ok(()) + /// } + /// ``` + pub fn build(&self) -> Result { + // issuer jwt contains cnf claim then Key Binding JWT is required + let issuer_claims_part = get_jwt_part(self.sd_jwt.as_str(), JWTPart::Claims)?; + let issuer_jwt_claims = decode_claims_no_verification(issuer_claims_part.as_str())?; + if issuer_jwt_claims.get("cnf").is_some() + && (self.key.is_none() || self.algorithm.is_none() || self.aud.is_none()) + { + return Err(Error::KeyBindingJWTRequired); + } + + let presentation_disclosures = self + .disclosure_paths + .iter() + .filter(|disclosure_path| !self.redacted.contains(&disclosure_path.path)) + .map(|disclosure_path| disclosure_path.disclosure.disclosure()) + .collect::>(); + + let mut presentation = presentation_disclosures.iter().fold( + self.sd_jwt.clone(), + |mut presentation, disclosure| { + presentation.push('~'); + presentation.push_str(disclosure); + presentation + }, + ); + presentation.push('~'); + + if issuer_jwt_claims.get("cnf").is_some() { + // build kb-jwt + let sd_alg = + HashAlgorithm::try_from(issuer_jwt_claims["_sd_alg"].as_str().unwrap_or(""))?; + let nonce = generate_nonce(32); + let iat = Utc::now().timestamp(); + let sd_hash = base64_hash(sd_alg, &presentation); + let mut header = Header::new(self.algorithm.clone().ok_or( + Error::KeyBindingJWTParameterMissing("algorithm".to_string()), + )?); + header.typ = Some("kb+jwt".to_string()); + let claims = serde_json::json!({ + "aud": self.aud.clone().ok_or(Error::KeyBindingJWTParameterMissing("aud".to_string()))?, + "nonce": nonce, + "iat": iat, + "sd_hash": sd_hash, + }); + let kb_jwt = encode( + &header, + &claims, + self.key + .as_ref() + .ok_or(Error::KeyBindingJWTParameterMissing( + "encoding key".to_string(), + ))?, + )?; + presentation.push_str(&kb_jwt); + } + + Ok(presentation) + } + + pub fn verify_raw( + issuer_token: &str, + key: &KeyForDecoding, + validation: &Validation, + ) -> Result<(Value, Value, Vec), Error> { + let (issuer_sd_jwt, disclosures, kb_jwt) = sd_jwt_parts(issuer_token); + if kb_jwt.is_some() { + return Err(Error::SDJWTRejected( + ("Issuer SD JWT cannot contain key binding JWT").to_string(), + )); + } + + let (header, claims) = decode(&issuer_sd_jwt, key, validation)?; + + match HashAlgorithm::try_from(claims["_sd_alg"].as_str().ok_or(Error::SDJWTRejected( + ("Issuer SD JWT must contain _sd_alg claim").to_string(), + ))?) { + Ok(_) => {} + Err(e) => { + return Err(Error::InvalidHashAlgorithm(e.to_string())); + } + } + + Ok((header, claims, disclosures)) + } + + /// Verify SD-JWT from Issuer for authenticity and integrity. + /// + /// ``` + /// use sd_jwt::{Holder, Error, KeyForDecoding, Validation}; + /// + /// const ISSUER_PUBKEY: &str = "-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEA2a7Pz5WA1AmtGfIxSKwB8vU9OL1ti7udYhvC6048l74loAlmJGps\n0hb4u64jv8sAmdGjYeya2Oza1dydtSmlLArMkbeAiSV/n+KKmK0mpA7D7R8ARLKK\n/BZG7Z/QaxEORJl1KspliBQ2mUJJbcFH+EUko9bAdWEWx9GLkRH2pDm9nMO2lTtE\nqzO+JBjnuEoTn/NZ9Ur4dQDf3nWLBwEFyyJfJ90Ga2f6LFeHL2cOcAbHiofW5NAa\nGqh/JWxf6dSClyOUG0Bpe+RV8t0hnFhIC7RFV0aVbp50sqTM4mwYtOPk/2qWVVMF\nBOaswXYbi0ADUc9CqIaGDCAWnmHrHL/J4wIDAQAB\n-----END RSA PUBLIC KEY-----\n"; + /// const ISSUER_SD_JWT: &str = "eyJ0eXAiOiJzZC1qd3QiLCJhbGciOiJSUzI1NiJ9.eyJfc2QiOlsiVFhsUEt1RjM1cDQ3ZW9XTlpEcklxS0w0R0JFaDBFWXJEQnBjNmFCWjUyQSIsIkdYWlpyVUlsdnBtaDB4b0h4WURadzFOZ211WXJrd1VVS09rNG1XTHZKYUEiXSwiX3NkX2FsZyI6InNoYS0yNTYiLCJhZGRyZXNzIjp7Il9zZCI6WyJiUjVKM21ULXQ0a05pZ0V0dDJ5RVd1MU92b0hVMzBmSTZ1RVdJd2ozZWJBIiwiczhicTVKeUtJaFFwcVR1Vl9hcVNtd090UVN5UHV1TUlUU2xINXg1UWI5RSJdLCJjb3VudHJ5IjoiVVMiLCJyZWdpb24iOiJBbnlzdGF0ZSJ9LCJiaXJ0aGRhdGUiOiIxOTQwLTAxLTAxIiwiY25mIjp7ImFsZyI6IlJTMjU2IiwiZSI6IkFRQUIiLCJrdHkiOiJSU0EiLCJuIjoiNS1EZDU0WHNNQU5UWm9KMllCcHVpWmFfYXpyMzJIcEJ3MUZjanA1d1UwWFBqbW9NQTdKVllDSk4wU05maDZ0dFhyWHhhYWhFNXdmUzd4S1E0N1ZvWXhYTjlLa3kxMzdDSUx0Q0xPWUJDZkdULWFRRXJKS0FJWUVORWtzbVNpU3k0VnVWRk1yTzlMOV9KTzViZk02QjZ6X3pickJYX2MxU2s0UFRLTnBqRTcxcTJHenU4ak5GdTR0c0JaOFFSdmtJVldxNGdxVklQNTFQQmZEcmNfTm53dk1aallGN2pfc0Z5eGg2ZExTVV96QkRrZjJOVWo4VXQ0M25vcW9YMGJoaE96aGdyTlpadGpFMTlrZGFlZTJYbjBweG0td3QzRjBxUjZxd2F2TFRJT21LVHE0OFdXSGxvUk5QWXpGbEo4OHNOaVNLeW9Ta0hXMG9SVDlscUhGX3ZRIiwidXNlIjoic2lnIn0sImVtYWlsIjoiam9obmRvZUBleGFtcGxlLmNvbSIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InhnU2FMYS1CNk03OWpwVWZtaE9Hb0pkSHdNS0RNR0s3eUVKdC0tX0xScDAifSx7Ii4uLiI6Im5vNWxNSkVJSmRWdHozS3lDMVRXVkk2T2tsQnZIMjFCOExOOVEzWkxWRmMifV0sInBob25lX251bWJlciI6IisxLTIwMi01NTUtMDEwMSIsInBob25lX251bWJlcl92ZXJpZmllZCI6dHJ1ZSwic3ViIjoidXNlcl80MiIsInVwZGF0ZWRfYXQiOjE1NzAwMDAwMDB9.K2h-DNDgnq6q61tSxm1Gv-Hfo46SD8rEcP7yLFxcAlQNKBY-l1-bpXCJcqVZ7jugs2lqng0Cf9e34tM1OPkU3R6Pi5kUMGSyJ2y2ifsaZhGLCgxzNKk5W2ZxdkehzZQ6nHy6iu4flbT92Szv0eBR0hmS3hYTCtHlE4xib9G2dKWTQigB4ylPMkoRzbiKjgkucGkxSLN5ZQRXdxkez19bk5Q9BwuNLQMKG0lanq4ZJWq1C4LPt_K0WhEntyTL6SxVxGfR5HaUSxeYPCCOWSz9AVyZ46DWZGRx48PbuXGgLDH1UJYIsMej2F89CU-3QkWUrFq9b-DCYCQMxbBBekeLog~WyJoV2xxekkxY3piQzhCMnF2Mm5vN3pBIiwiZ2l2ZW5fbmFtZSIsIkpvaG4iXQ~WyJ4NXdpQVg1Qks3MFNfYzhXX2Vybm5nIiwiZmFtaWx5X25hbWUiLCJEb2UiXQ~WyI4Q1BKSmNKV2tiOGVwT09yZkl5YUNRIiwic3RyZWV0X2FkZHJlc3MiLCIxMjMgTWFpbiBTdCJd~WyJDTGo2S0tjblA1M2taOG5kOWFueWxnIiwibG9jYWxpdHkiLCJBbnl0b3duIl0~WyI4UEVqT3FlY245cjhGY0llWThhRjh3IiwiVVMiXQ~WyJMR2hVZmV2Y0FkTGVUUEVzRnlCNi1BIiwiREUiXQ~"; + /// + /// fn main() -> Result<(), Error> { + /// let mut validation = Validation::default().no_exp(); + /// let decoding_key = KeyForDecoding::from_rsa_pem(ISSUER_PUBKEY.as_bytes())?; + /// let (header, decoded_claims, disclosure_paths) = + /// Holder::verify(ISSUER_SD_JWT, &decoding_key, &validation)?; + /// println!("header: {:?}", header); + /// println!("claims: {:?}", decoded_claims); + /// + /// Ok(()) + /// } + /// ``` + pub fn verify( + issuer_token: &str, + key: &KeyForDecoding, + validation: &Validation, + ) -> Result<(Value, Value, Vec), Error> { + let (header, claims, disclosures) = Holder::verify_raw(issuer_token, key, 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, disclosure_paths)) + } +} + +#[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::{Disclosure, Issuer, Jwk, KeyForEncoding}; + + 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" + ] + }"#; + + fn verify_path_to_disclosure( + expected_path: &str, + expected_key: &Option<&str>, + expected_value: &Value, + disclosure_path: &DisclosurePath, + ) -> bool { + if disclosure_path.path != expected_path { + return false; + } + + match (expected_key, &disclosure_path.disclosure.key()) { + (Some(exp_key), Some(disc_key)) if exp_key == disc_key => {} + (None, None) => {} + _ => return false, + } + + if !compare_json_values(expected_value, disclosure_path.disclosure.value()) { + return false; + } + + true + } + + #[test] + fn test_verify_sd_jwt_with_sd_objects() -> Result<(), Error> { + let (priv_key, pub_key) = keys(); + let (issuer_private_key, issuer_public_key) = convert_to_pem(priv_key, pub_key); + let claims: Value = serde_json::from_str(TEST_CLAIMS).unwrap(); + let mut issuer = Issuer::new(claims)?; + let encoded = issuer + .expires_in_seconds(60) + .disclosable("/given_name") + .disclosable("/family_name") + .disclosable("/address/street_address") + .disclosable("/address/locality") + .encode(&KeyForEncoding::from_rsa_pem( + issuer_private_key.as_bytes(), + )?)?; + println!("encoded: {:?}", encoded); + let test_claims = issuer.claims_copy(); + let dot_segments = encoded.split('.').count(); + let disclosure_segments = encoded.split('~').count() - 2; + + assert_eq!(dot_segments, 3); + assert_eq!(disclosure_segments, 4); + + let validation = Validation::default(); + let decoding_key = KeyForDecoding::from_rsa_pem(issuer_public_key.as_bytes())?; + let (header, decoded_claims, disclosure_paths) = + Holder::verify(&encoded, &decoding_key, &validation)?; + println!("header: {:?}", header); + println!("claims: {:?}", decoded_claims); + println!("disclosure_paths: {:?}", disclosure_paths); + assert!(compare_json_values(&test_claims, &decoded_claims)); + assert!(verify_path_to_disclosure( + "/given_name", + &Some("given_name"), + &serde_json::json!("John"), + &disclosure_paths[0] + )); + assert!(verify_path_to_disclosure( + "/given_name", + &Some("given_name"), + &serde_json::json!("John"), + &disclosure_paths[0] + )); + assert!(verify_path_to_disclosure( + "/family_name", + &Some("family_name"), + &serde_json::json!("Doe"), + &disclosure_paths[1] + )); + assert!(verify_path_to_disclosure( + "/address/street_address", + &Some("street_address"), + &serde_json::json!("123 Main St"), + &disclosure_paths[2] + )); + assert!(verify_path_to_disclosure( + "/address/locality", + &Some("locality"), + &serde_json::json!("Anytown"), + &disclosure_paths[3] + )); + Ok(()) + } + + #[test] + fn test_verify_sd_jwt_with_sd_objects_array() -> Result<(), Error> { + let (priv_key, pub_key) = keys(); + let (issuer_private_key, issuer_public_key) = convert_to_pem(priv_key, pub_key); + let claims: Value = serde_json::from_str(TEST_CLAIMS).unwrap(); + let mut issuer = Issuer::new(claims)?; + let encoded = issuer + .expires_in_seconds(60) + .disclosable("/given_name") + .disclosable("/family_name") + .disclosable("/address/street_address") + .disclosable("/address/locality") + .disclosable("/nationalities/0") + .disclosable("/nationalities/1") + .encode(&KeyForEncoding::from_rsa_pem( + issuer_private_key.as_bytes(), + )?)?; + println!("encoded: {:?}", encoded); + let test_claims = issuer.claims_copy(); + let dot_segments = encoded.split('.').count(); + let disclosure_segments = encoded.split('~').count() - 2; + + assert_eq!(dot_segments, 3); + assert_eq!(disclosure_segments, 6); + + let validation = Validation::default(); + let decoding_key = KeyForDecoding::from_rsa_pem(issuer_public_key.as_bytes())?; + let (header, decoded_claims, disclosure_paths) = + Holder::verify(&encoded, &decoding_key, &validation)?; + println!("header: {:?}", header); + println!("claims: {:?}", decoded_claims); + println!("disclosure_paths: {:?}", disclosure_paths); + assert!(compare_json_values(&test_claims, &decoded_claims)); + assert!(verify_path_to_disclosure( + "/given_name", + &Some("given_name"), + &serde_json::json!("John"), + &disclosure_paths[0] + )); + assert!(verify_path_to_disclosure( + "/given_name", + &Some("given_name"), + &serde_json::json!("John"), + &disclosure_paths[0] + )); + assert!(verify_path_to_disclosure( + "/family_name", + &Some("family_name"), + &serde_json::json!("Doe"), + &disclosure_paths[1] + )); + assert!(verify_path_to_disclosure( + "/address/street_address", + &Some("street_address"), + &serde_json::json!("123 Main St"), + &disclosure_paths[2] + )); + assert!(verify_path_to_disclosure( + "/address/locality", + &Some("locality"), + &serde_json::json!("Anytown"), + &disclosure_paths[3] + )); + assert!(verify_path_to_disclosure( + "/nationalities/0", + &None, + &serde_json::json!("US"), + &disclosure_paths[4] + )); + assert!(verify_path_to_disclosure( + "/nationalities/1", + &None, + &serde_json::json!("DE"), + &disclosure_paths[5] + )); + Ok(()) + } + + #[test] + fn test_presentation() -> 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 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") + .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")? + .build()?; + println!("presentation: {:?}", presentation); + + let dot_segments = presentation.split('.').count(); + let disclosure_segments = presentation.split('~').count() - 2; + + assert_eq!(dot_segments, 3); + assert_eq!(disclosure_segments, 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")); + + Ok(()) + } + + #[test] + fn test_presentation_with_kb_holder_no_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_public_key) = keys(); + let holder_jwk = publickey_to_jwk(&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 result = Holder::presentation(&issuer_sd_jwt)? + .redact("/family_name")? + .redact("/address/street_address")? + .redact("/nationalities/0")? + .build(); + assert!(result.is_err()); + + Ok(()) + } + + #[test] + fn test_presentation_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")); + + Ok(()) + } +} diff --git a/src/issuer.rs b/src/issuer.rs new file mode 100644 index 0000000..cf758b9 --- /dev/null +++ b/src/issuer.rs @@ -0,0 +1,487 @@ +use crate::Disclosure; +use crate::Error; +use crate::Header; +use crate::Jwk; +use crate::{encode, KeyForEncoding}; +use chrono::{Duration, Utc}; +use serde::Serialize; +use serde_json::Value; +use std::ops::Deref; +use std::vec; + +/// # Issuer Module +/// +/// Represents an issuer of claims. Issues SD-JWT with all disclosures. +/// +/// ## Features +/// +/// - Creating new issuers with custom claims. +/// - Marking claims as disclosable. +/// - Configuring JWT headers. +/// - Setting expiration time for the token. +/// - Optionally requiring a key binding. +/// - Encoding the issuer's claims into a SD-JWT. +/// +/// Example: +/// ``` +/// use sd_jwt::{Issuer, Jwk, Error, KeyForEncoding}; +/// use serde_json::Value; +/// +/// const ISSUER_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 ISSUER_SIGNING_KEY_PEM: &str = "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDSwzyVZp2AIxS3\n802n0AfwKsMUcMYATMM6kK5VVS21ku3d6QC8kfhvJ0Pcb24dmGUWAJ95H9m19qDF\nbLrVZ9b4iobOsNlXNhKn4TRrsVFa8EaGXAJjGNRPPcL+gFwfV9y3tfR00tkokhR5\nZhhMifwKJf55QlEzY96yyk8ISzhagwO6Kf/E980Eoby1tvhX8q8HIwLG4GjFnmXx\nbKqxVQR1T07vFKHsF1MK8/d6a7+samHPWjoSlLvKSE4rdK8gouRpN/5Who4iS2s7\nlhfS2DcnxCnxj9S9BBm4GIQNk0Tc+lR20btBm+JiehAyEV9vX222BVSLUC9z9HGD\nk39b9ezbAgMBAAECggEBAIXuRxtxX/jDUjEqzVgsXD8EDX95wnkCTrVypzXWsPtH\naRyxKiSqZcLMotT7gnAQHXyD3NMtqD13geazF27xU6wQ62WBADvpQqWn+JXO0jIF\nqetLoMC0UIYiaz0q+F96h+m+GJ/8NL8RRS138U0CCkWwqysHN25+sk/PO7W7hw4M\nOAN/97rBkXqyzJJSvNwl2A66ga+9WC8G/9YgweqkS6re6WAyo4z1KyZAE1r655JR\nEaiIR6GYvahNsy/dNjVtGR189o8bf6xnTPbDUXQ/D61nO3Kg3B7Ca/uQWiDbI9VJ\nMXZxgip9Q7Qil9WuK1vVCUSf6WK38NV6r9fubw/DgsECgYEA70drCiGrC3pvIJF0\nLJL46H6x6SFClR876BZEnN51udJGXRstWV+Ya6NULSTykwusaTYUnr2BC6r3tT4S\nrRLfnXTaI0Tr6Bws6kBSJJC0CS0lLqK2tlKbcypQXv0T6Ulv2NXDq0VqQB3txED6\n8m5GieppHNueqLQqGqM1V4JYw5ECgYEA4X2s7ccLB8MX01j4T6Fnj4BGaZsyc1kV\nn6VHsuAsUxA9ZuwV+lk5k6xaWxDYmQR3xZ4XcQEntRUtGFu4TMLVpCcK26Vqafrp\nymbGjJGFagIaP9YOhQ+5ZMfO0obYUEaDGhPjXH3G9O/dTXoRg5nP5JvdcAnf853y\nm1BaYBHbG6sCgYAfVkQffI9RHoTFSCdl2w28LTORq6hzrTaES75KqRvT7UUH1pJW\n3R0yI57XlroqJeI7mTiUHY9z/r0YQHvjrNAaZ/5VliYrLN15BFl9rnHVrdLry6WQ\nNTtklssV1aEw8UwzorNQj/O9V+4WwMfczjJwx4FipSSfRZEqEevffROw8QKBgGNK\nba0+KjM+yuz7jkuyLOHZgCfcePilz4m+w7WWVK42xnLdnkfgpiPKjvbukhG/D+Zq\n2LOf6JYqPvMs4Bic6mof7v4M9rC4Fd5UJzWaln65ckmNvlMFO4OPIBk/21xt0CjZ\nfRIrKEKOpIoLKE8kmZB2uakuD/k8IaoWVdVbx3mFAoGAMFFWZAAHpB18WaATQRR6\n86JnudPD3TlOw+8Zw4tlOoGv4VXCPVsyAH8CWNSONyTRxeSJpe8Pn6ZvPJ7YBt6c\nchNSaqFIl9UnkMJ1ckE7EX2zKFCg3k8VzqYRLC9TcqqwKTJcNdRu1SbWkAds6Sd8\nKKRrCm+L44uQ01gUYvYYv5c=\n-----END PRIVATE KEY-----\n"; +/// +/// fn main() -> Result<(), Error> { +/// // holder's public key required for key binding +/// let holder_jwk = Jwk::from_value(serde_json::json!({ +/// "kty": "RSA", +/// "n": "...", +/// "e": "...", +/// "alg": "RS256", +/// "use": "sig", +/// }))?; +/// +/// // create issuer's claims +/// let claims: Value = serde_json::from_str(ISSUER_CLAIMS).unwrap(); +/// let issuer = Issuer::new(claims)? +/// .disclosable("/given_name") +/// .disclosable("/family_name") +/// .disclosable("/address/street_address") +/// .disclosable("/address/locality") +/// .disclosable("/nationalities/0") +/// .disclosable("/nationalities/1") +/// .require_key_binding(holder_jwk) +/// .encode(&KeyForEncoding::from_rsa_pem( +/// ISSUER_SIGNING_KEY_PEM.as_bytes(), +/// )?)?; +/// +/// Ok(()) +/// } +/// ``` +#[derive(Debug, Clone)] +pub struct Issuer { + claims: Value, + disclosable_claim_paths: Vec, + header: Header, + key_binding_pubkey: Option, +} + +impl Issuer { + /// Creates a new `Issuer` with the given claims. + /// + /// # Arguments + /// + /// * `claims` - A serializable object that represents the claims to be included in the SD-JWT. + /// + /// # Returns + /// + /// A result containing the new `Issuer` instance, or an error if the claims cannot be serialized. + /// + /// # Examples + /// + /// ``` + /// use sd_jwt::Issuer; + /// + /// let claims = serde_json::json!({ + /// "sub": "user_42", + /// "given_name": "John", + /// "family_name": "Doe", + /// "email": "johndoe@example", + /// }); + /// let issuer = Issuer::new(claims).unwrap(); + /// ``` + pub fn new(claims: T) -> Result { + Ok(Issuer { + claims: serde_json::to_value(claims)?, + disclosable_claim_paths: Vec::new(), + header: Header::default(), + key_binding_pubkey: None, + }) + } + + /// Marks claim as disclosable. + /// + /// # Arguments + /// + /// * `path` - A string slice representing the path to a claim that can be disclosed. + /// + /// # Returns + /// + /// A mutable reference to the issuer for method chaining. + /// + /// # Examples + /// + /// ``` + /// use sd_jwt::Issuer; + /// + /// let claims = serde_json::json!({ + /// "sub": "user_42", + /// "given_name": "John", + /// "family_name": "Doe", + /// "email": "johndoe@example", + /// "address": { + /// "street_address": "123 Main St", + /// "locality": "Anytown", + /// "region": "Anystate", + /// "country": "US" + /// }, + /// "nationalities": [ + /// "US", + /// "DE" + /// ] + /// }); + /// + /// let issuer = Issuer::new(claims).unwrap() + /// .disclosable("/given_name") + /// .disclosable("/family_name") + /// .disclosable("/address/street_address") + /// .disclosable("/address/locality") + /// .disclosable("/nationalities/0") + /// .disclosable("/nationalities/1"); + /// ``` + pub fn disclosable(&mut self, path: &str) -> &mut Self { + self.disclosable_claim_paths.push(path.to_string()); + self + } + + /// Sets the header for the issuer's SD-JWT. + /// + /// # Arguments + /// + /// * `header` - The `Header` struct representing the JWT header. + /// + /// # Returns + /// + /// A mutable reference to the issuer for method chaining. + /// + /// # Examples + /// + /// ``` + /// use sd_jwt::{Issuer, Header}; + /// + /// let mut header = Header::default(); + /// header.typ = Some("application/example+sd-jwt".to_string()); + /// let claims = serde_json::json!({ + /// "sub": "user_42", + /// "given_name": "John", + /// "family_name": "Doe", + /// "email": "johndoe@example", + /// }); + /// let issuer = Issuer::new(claims).unwrap() + /// .header(header); + /// ``` + pub fn header(&mut self, header: Header) -> &mut Self { + self.header = header; + self + } + + /// Sets the expiration time of the issuer's SD-JWT. + /// + /// # Arguments + /// + /// * `seconds` - The number of seconds from now until the token expires. + /// + /// # Returns + /// + /// A mutable reference to the issuer for method chaining. + /// + /// # Examples + /// + /// ``` + /// use sd_jwt::Issuer; + /// + /// let claims = serde_json::json!({ + /// "sub": "user_42", + /// "given_name": "John", + /// "family_name": "Doe", + /// "email": "johndoe@example", + /// }); + /// let issuer = Issuer::new(claims).unwrap() + /// .expires_in_seconds(3600); // Expires in one hour + /// ``` + pub fn expires_in_seconds(&mut self, seconds: i64) -> &mut Self { + let now = Utc::now(); + let expiration = now + Duration::seconds(seconds); + let exp = expiration.timestamp(); + self.claims["exp"] = serde_json::json!(exp); + self + } + + /// Requires a key binding for Holder. + /// + /// # Arguments + /// + /// * `key_binding_pubkey` - A `Jwk` representing the public key to bind to the JWT. + /// + /// # Returns + /// + /// A mutable reference to the issuer for method chaining. + /// + /// # Examples + /// + /// ``` + /// use sd_jwt::{Issuer, Jwk}; + /// + /// let claims = serde_json::json!({ + /// "sub": "user_42", + /// "given_name": "John", + /// "family_name": "Doe", + /// "email": "johndoe@example", + /// }); + /// + /// let holder_jwk = Jwk::from_value(serde_json::json!({ + /// "kty": "RSA", + /// "n": "...", + /// "e": "...", + /// "alg": "RS256", + /// "use": "sig", + /// })).unwrap(); + /// let issuer = Issuer::new(claims).unwrap() + /// .require_key_binding(holder_jwk); + /// ``` + pub fn require_key_binding(&mut self, key_binding_pubkey: Jwk) -> &mut Self { + self.key_binding_pubkey = Some(key_binding_pubkey); + self + } + + /// Encodes the issuer into a SD-JWT. + /// + /// # Arguments + /// + /// * `signer_key` - A reference to a `KeyForEncoding` used for signing the issuer's SD-JWT. + /// + /// # Returns + /// + /// Serialized SD-JWT in format: + /// ~~~...~~ + /// + /// # Examples + /// + /// ``` + /// use sd_jwt::{Issuer, KeyForEncoding}; + /// + /// const ISSUER_SIGNING_KEY_PEM: &str = "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDSwzyVZp2AIxS3\n802n0AfwKsMUcMYATMM6kK5VVS21ku3d6QC8kfhvJ0Pcb24dmGUWAJ95H9m19qDF\nbLrVZ9b4iobOsNlXNhKn4TRrsVFa8EaGXAJjGNRPPcL+gFwfV9y3tfR00tkokhR5\nZhhMifwKJf55QlEzY96yyk8ISzhagwO6Kf/E980Eoby1tvhX8q8HIwLG4GjFnmXx\nbKqxVQR1T07vFKHsF1MK8/d6a7+samHPWjoSlLvKSE4rdK8gouRpN/5Who4iS2s7\nlhfS2DcnxCnxj9S9BBm4GIQNk0Tc+lR20btBm+JiehAyEV9vX222BVSLUC9z9HGD\nk39b9ezbAgMBAAECggEBAIXuRxtxX/jDUjEqzVgsXD8EDX95wnkCTrVypzXWsPtH\naRyxKiSqZcLMotT7gnAQHXyD3NMtqD13geazF27xU6wQ62WBADvpQqWn+JXO0jIF\nqetLoMC0UIYiaz0q+F96h+m+GJ/8NL8RRS138U0CCkWwqysHN25+sk/PO7W7hw4M\nOAN/97rBkXqyzJJSvNwl2A66ga+9WC8G/9YgweqkS6re6WAyo4z1KyZAE1r655JR\nEaiIR6GYvahNsy/dNjVtGR189o8bf6xnTPbDUXQ/D61nO3Kg3B7Ca/uQWiDbI9VJ\nMXZxgip9Q7Qil9WuK1vVCUSf6WK38NV6r9fubw/DgsECgYEA70drCiGrC3pvIJF0\nLJL46H6x6SFClR876BZEnN51udJGXRstWV+Ya6NULSTykwusaTYUnr2BC6r3tT4S\nrRLfnXTaI0Tr6Bws6kBSJJC0CS0lLqK2tlKbcypQXv0T6Ulv2NXDq0VqQB3txED6\n8m5GieppHNueqLQqGqM1V4JYw5ECgYEA4X2s7ccLB8MX01j4T6Fnj4BGaZsyc1kV\nn6VHsuAsUxA9ZuwV+lk5k6xaWxDYmQR3xZ4XcQEntRUtGFu4TMLVpCcK26Vqafrp\nymbGjJGFagIaP9YOhQ+5ZMfO0obYUEaDGhPjXH3G9O/dTXoRg5nP5JvdcAnf853y\nm1BaYBHbG6sCgYAfVkQffI9RHoTFSCdl2w28LTORq6hzrTaES75KqRvT7UUH1pJW\n3R0yI57XlroqJeI7mTiUHY9z/r0YQHvjrNAaZ/5VliYrLN15BFl9rnHVrdLry6WQ\nNTtklssV1aEw8UwzorNQj/O9V+4WwMfczjJwx4FipSSfRZEqEevffROw8QKBgGNK\nba0+KjM+yuz7jkuyLOHZgCfcePilz4m+w7WWVK42xnLdnkfgpiPKjvbukhG/D+Zq\n2LOf6JYqPvMs4Bic6mof7v4M9rC4Fd5UJzWaln65ckmNvlMFO4OPIBk/21xt0CjZ\nfRIrKEKOpIoLKE8kmZB2uakuD/k8IaoWVdVbx3mFAoGAMFFWZAAHpB18WaATQRR6\n86JnudPD3TlOw+8Zw4tlOoGv4VXCPVsyAH8CWNSONyTRxeSJpe8Pn6ZvPJ7YBt6c\nchNSaqFIl9UnkMJ1ckE7EX2zKFCg3k8VzqYRLC9TcqqwKTJcNdRu1SbWkAds6Sd8\nKKRrCm+L44uQ01gUYvYYv5c=\n-----END PRIVATE KEY-----\n"; + /// + /// let claims = serde_json::json!({ + /// "sub": "user_42", + /// "given_name": "John", + /// "family_name": "Doe", + /// "email": "johndoe@example", + /// "address": { + /// "street_address": "123 Main St", + /// "locality": "Anytown", + /// "region": "Anystate", + /// "country": "US" + /// }, + /// "nationalities": [ + /// "US", + /// "DE" + /// ] + /// }); + /// + /// let encoded_jwt = Issuer::new(claims).unwrap() + /// .disclosable("/given_name") + /// .disclosable("/family_name") + /// .disclosable("/address/street_address") + /// .disclosable("/address/locality") + /// .disclosable("/nationalities/0") + /// .disclosable("/nationalities/1") + /// .encode(&KeyForEncoding::from_rsa_pem( + /// ISSUER_SIGNING_KEY_PEM.as_bytes(), + /// ).unwrap()).unwrap(); + /// println!("Encoded JWT: {}", encoded_jwt); + /// ``` + pub fn encode(&mut self, signer_key: &KeyForEncoding) -> Result { + let mut updated_claims = self.claims.clone(); + let disclosures: Result, Error> = self + .disclosable_claim_paths + .iter() + .map(|disclosable_claim| build_disclosure(&mut updated_claims, disclosable_claim)) + .collect(); + let disclosures = disclosures?; + + if !disclosures.is_empty() { + let algorithm = disclosures[0].get_algorithm().to_string(); + updated_claims["_sd_alg"] = algorithm.into(); + } + + if self.key_binding_pubkey.is_some() { + let key_binding_pubkey = self.key_binding_pubkey.as_ref().unwrap(); + updated_claims["cnf"] = serde_json::json!(key_binding_pubkey.deref()); + } + + let issuer_jwt = encode(&self.header, &updated_claims, signer_key)?; + let mut serialized_sd_jwt = issuer_jwt; + disclosures.iter().for_each(|disclosure| { + serialized_sd_jwt = format!("{}~{}", serialized_sd_jwt, disclosure.disclosure()); + }); + serialized_sd_jwt = format!("{}~", serialized_sd_jwt); + + Ok(serialized_sd_jwt) + } + + #[cfg(test)] + pub fn claims_copy(&self) -> Value { + self.claims.clone() + } +} + +fn parent_elem_from_path(path: &str) -> Result<(&str, &str), Error> { + let last_slash_index = path.rfind('/').ok_or(Error::InvalidPathPointer)?; + let (parent_path, element_path) = path.split_at(last_slash_index); + let element_path = element_path.trim_start_matches('/'); + + Ok((parent_path, element_path)) +} + +fn build_disclosure(claims: &mut Value, disclosable_claim: &str) -> Result { + let (parent_ptr, elem_ptr) = parent_elem_from_path(disclosable_claim)?; + let key = elem_ptr.trim_start_matches('/'); + + let parent = claims + .pointer_mut(parent_ptr) + .ok_or(Error::InvalidPathPointer)?; + if parent.is_array() { + let parent = parent.as_array_mut().ok_or(Error::InvalidPathPointer)?; + let key_index = key.parse()?; + let value = parent.remove(key_index); + let disclosure = Disclosure::new(None, value.clone()).build()?; + parent.insert(key_index, serde_json::json!({ "...": disclosure.digest() })); + return Ok(disclosure); + } + let parent = parent.as_object_mut().ok_or(Error::InvalidPathPointer)?; + + let value = parent.remove(key).ok_or(Error::InvalidPathPointer)?; + + let disclosure = Disclosure::new(Some(key.to_owned()), value.clone()).build()?; + + match parent.get_mut("_sd") { + Some(sd) => { + if let Some(sd_array) = sd.as_array_mut() { + sd_array.push(Value::from(disclosure.digest().as_str())); + } else { + return Err(Error::InvalidSDType); + } + } + None => { + let sd_array = vec![Value::from(disclosure.digest().as_str())]; + parent.insert("_sd".to_string(), sd_array.into()); + } + } + + Ok(disclosure) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::common_test_utils::{convert_to_pem, keys, publickey_to_jwk}; + use serde_json::Value; + + 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" + ] + }"#; + + fn setup_common() -> (Issuer, String) { + let (priv_key, pub_key) = keys(); + let (issuer_private_key, _) = convert_to_pem(priv_key, pub_key); + let claims: Value = serde_json::from_str(TEST_CLAIMS).unwrap(); + let issuer = Issuer::new(claims).unwrap(); + (issuer, issuer_private_key) + } + + fn encode_and_test( + issuer: &mut Issuer, + issuer_private_key: &str, + expected_disclosures: usize, + ) -> Result<(), Error> { + let encoded = issuer.encode(&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, expected_disclosures); + Ok(()) + } + + #[test] + fn test_encode_objects() -> Result<(), Error> { + let (mut issuer, issuer_private_key) = setup_common(); + issuer + .disclosable("/given_name") + .disclosable("/family_name") + .disclosable("/address/street_address") + .disclosable("/address/locality"); + encode_and_test(&mut issuer, &issuer_private_key, 4) + } + + #[test] + fn test_encode_objects_and_array() -> Result<(), Error> { + let (mut issuer, issuer_private_key) = setup_common(); + issuer + .disclosable("/given_name") + .disclosable("/family_name") + .disclosable("/address/street_address") + .disclosable("/address/locality") + .disclosable("/nationalities/0") + .disclosable("/nationalities/1"); + encode_and_test(&mut issuer, &issuer_private_key, 6) + } + + #[test] + fn test_encode_objects_and_array_kb_required() -> Result<(), Error> { + let (mut issuer, issuer_private_key) = setup_common(); + let (_, holder_public_key) = keys(); + let holder_jwk = publickey_to_jwk(&holder_public_key); + println!("holder_jwk: {:?}", holder_jwk); + + issuer + .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_and_test(&mut issuer, &issuer_private_key, 6) + } +} diff --git a/src/jwk.rs b/src/jwk.rs new file mode 100644 index 0000000..c0bb91f --- /dev/null +++ b/src/jwk.rs @@ -0,0 +1,29 @@ +use crate::Error; +use std::ops::{Deref, DerefMut}; + +#[derive(Debug, Clone)] +pub struct Jwk { + jwk: jsonwebtoken::jwk::Jwk, +} + +impl Jwk { + pub fn from_value(value: serde_json::Value) -> Result { + Ok(Jwk { + jwk: serde_json::from_value(value)?, + }) + } +} + +impl Deref for Jwk { + type Target = jsonwebtoken::jwk::Jwk; + + fn deref(&self) -> &Self::Target { + &self.jwk + } +} + +impl DerefMut for Jwk { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.jwk + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..d552fe7 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,29 @@ +pub mod algorithm; +pub mod decoding; +pub mod disclosure; +pub mod disclosure_path; +pub mod encoding; +pub mod error; +pub mod header; +pub mod holder; +pub mod issuer; +pub mod jwk; +mod utils; +pub mod validation; +pub mod verifier; + +#[cfg(test)] +mod test_utils; + +pub use algorithm::*; +pub use decoding::*; +pub use disclosure::Disclosure; +pub use disclosure_path::*; +pub use encoding::*; +pub use error::*; +pub use header::*; +pub use holder::*; +pub use issuer::*; +pub use jwk::*; +pub use validation::*; +pub use verifier::*; diff --git a/src/test_utils.rs b/src/test_utils.rs new file mode 100644 index 0000000..52ab68a --- /dev/null +++ b/src/test_utils.rs @@ -0,0 +1,106 @@ +#[cfg(test)] +pub mod common_test_utils { + use base64::Engine; + use rand::rngs::OsRng; + use rsa::PublicKeyParts; + use rsa::{pkcs1::ToRsaPublicKey, pkcs8::ToPrivateKey, RsaPrivateKey, RsaPublicKey}; + use serde_json::value::{Map, Value}; + use std::collections::HashSet; + + pub fn keys() -> (RsaPrivateKey, RsaPublicKey) { + let mut rng = OsRng; + let bits = 2048; + let private_key = RsaPrivateKey::new(&mut rng, bits).unwrap(); + let public_key = RsaPublicKey::from(&private_key); + + (private_key, public_key) + } + + pub fn convert_to_pem( + private_key: RsaPrivateKey, + public_key: RsaPublicKey, + ) -> (String, String) { + ( + private_key.to_pkcs8_pem().unwrap().to_string(), + public_key.to_pkcs1_pem().unwrap(), + ) + } + + pub fn publickey_to_jwk(public_key: &RsaPublicKey) -> serde_json::Value { + let n = public_key.n().to_bytes_be(); + let e = public_key.e().to_bytes_be(); + + serde_json::json!({ + "kty": "RSA", + "n": base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(n), + "e": base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(e), + "alg": "RS256", + "use": "sig", + }) + } + + pub fn compare_json_values(json1: &Value, json2: &Value) -> bool { + match (json1, json2) { + (Value::Object(map1), Value::Object(map2)) => compare_json_maps(map1, map2), + (Value::Array(arr1), Value::Array(arr2)) => compare_json_arrays(arr1, arr2), + _ => json1 == json2, + } + } + + pub fn compare_json_maps(map1: &Map, map2: &Map) -> bool { + if map1.len() != map2.len() { + return false; + } + + map1.iter().all(|(key, val1)| { + map2.get(key) + .map_or(false, |val2| compare_json_values(val1, val2)) + }) + } + + pub fn compare_json_arrays(arr1: &[Value], arr2: &[Value]) -> bool { + if arr1.len() != arr2.len() { + return false; + } + + let mut matched_indices = HashSet::new(); + + for val1 in arr1 { + let mut is_matched = false; + + for (index, val2) in arr2.iter().enumerate() { + if !matched_indices.contains(&index) && compare_json_values(val1, val2) { + is_matched = true; + matched_indices.insert(index); + break; + } + } + + if !is_matched { + return false; + } + } + + true + } + + pub fn separate_jwt_and_disclosures(input: &str) -> (String, String) { + let parts: Vec<&str> = input.splitn(2, '~').collect(); + if parts.len() == 2 { + (parts[0].to_string(), parts[1].to_string()) + } else { + (input.to_string(), "".to_string()) + } + } + + pub fn disclosures2vec(disclosures: &str) -> Vec { + let parts: Vec<&str> = disclosures.split('~').collect(); + if parts.is_empty() { + return Vec::new(); + } + parts[..parts.len() - 1] + .iter() + .map(|&s| s.to_string()) + .collect() + } +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..0a93201 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,183 @@ +use crate::{Disclosure, DisclosurePath, Error, HashAlgorithm}; +use base64::Engine; +use rand::{distributions::Alphanumeric, Rng}; +use serde_json::Value; + +#[allow(dead_code)] +pub(crate) enum JWTPart { + Header, + Claims, + Signature, +} + +pub(crate) fn get_jwt_part(jwt: &str, part: JWTPart) -> Result { + let parts: Vec<&str> = jwt.split('.').collect(); + + if parts.len() != 3 { + return Err(Error::JwtMustHaveThreeParts); + } + + match part { + JWTPart::Header => Ok(parts[0].to_string()), + JWTPart::Claims => Ok(parts[1].to_string()), + JWTPart::Signature => Ok(parts[2].to_string()), + } +} + +pub(crate) fn decode_claims_no_verification(claims: &str) -> Result { + let decoded_claims = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(claims)?; + let decoded_claims = String::from_utf8(decoded_claims)?; + let claims: Value = serde_json::from_str(decoded_claims.as_str())?; + Ok(claims) +} + +pub(crate) fn drop_kb(input: &str) -> String { + let parts = input.split('~').collect::>(); + + if parts.len() < 2 { + return String::from(input); + } + + let filtered_parts = &parts[..parts.len() - 1]; + + format!("{}~", filtered_parts.join("~")) +} + +pub(crate) fn restore_disclosures( + claims: &mut Value, + disclosures: &[String], + disclosure_paths: &mut Vec, + algorithm: HashAlgorithm, +) -> Result<(), Error> { + for disclosure in disclosures { + let decoded_disclosure = Disclosure::from_base64(disclosure, algorithm)?; + restore_disclosure(claims, &decoded_disclosure, String::new(), disclosure_paths)?; + } + + Ok(()) +} + +pub(crate) fn sd_contains_digest(sd: &Value, digest: &str) -> Result { + let sd_array = sd + .as_array() + .ok_or_else(|| Error::SDJWTRejected("_sd element must be array".to_string()))?; + Ok(sd_array.iter().any(|item| item.as_str() == Some(digest))) +} + +pub(crate) fn remove_digests(claims: &mut Value) -> Result<(), Error> { + if let Value::Object(ref mut map) = claims { + map.remove("_sd_alg"); + } + remove_all_digests(claims) +} + +pub(crate) fn remove_all_digests(claims: &mut Value) -> Result<(), Error> { + match claims { + Value::Object(map) => { + let keys_to_remove: Vec<_> = map.keys().filter(|&k| k == "_sd").cloned().collect(); + for k in keys_to_remove { + map.remove(&k); + } + + for value in map.values_mut() { + remove_all_digests(value)?; + } + } + Value::Array(array) => { + array.retain(|item| { + !(item.is_object() && item.get("...").map_or(false, |v| v.is_string())) + }); + + for item in array.iter_mut() { + remove_all_digests(item)?; + } + } + _ => {} + } + + Ok(()) +} +pub(crate) fn format_path(parent_path: &str, key: &str) -> String { + if parent_path.is_empty() { + format!("/{}", key) + } else { + format!("{}/{}", parent_path, key) + } +} + +pub(crate) fn restore_disclosure( + claims: &mut Value, + disclosure: &Disclosure, + current_path: String, + disclosure_paths: &mut Vec, +) -> Result { + let mut array_changes = Vec::new(); + let mut is_restored = false; + + match claims { + Value::Object(map) => { + if let Some(sd) = map.get_mut("_sd") { + if sd_contains_digest(sd, disclosure.digest())? { + if let Some(key) = disclosure.key() { + let path = format_path(¤t_path, key); + disclosure_paths.push(DisclosurePath::new(&path, disclosure)); + map.insert(key.to_string(), disclosure.value().clone()); + is_restored = true; + } else { + return Err(Error::SDJWTRejected( + "Disclosure key is missing".to_string(), + )); + } + } + } + + for (key, value) in map.iter_mut() { + let path = format_path(¤t_path, key); + if restore_disclosure(value, disclosure, path, disclosure_paths)? { + is_restored = true; + } + } + } + Value::Array(array) => { + for (idx, item) in array.iter_mut().enumerate() { + if item.is_object() { + let value = item.as_object().unwrap().get("..."); + if value.is_some() && item.as_object().unwrap().len() != 1 { + return Err(Error::SDJWTRejected( + ("... key must be only key in object").to_string(), + )); + } + + if let Some(v) = value { + if v == disclosure.digest() { + if !disclosure.key().is_none() { + return Err(Error::SDJWTRejected(format!( + "disclosure key must be empty in {} for array elements", + disclosure.disclosure(), + ))); + } + let path = format_path(¤t_path, &idx.to_string()); + disclosure_paths.push(DisclosurePath::new(&path, disclosure)); + array_changes.push(disclosure.value().clone()); + is_restored = true; + } + } + } + } + for elem in array_changes { + array.push(elem); + } + } + _ => {} + } + + Ok(is_restored) +} + +pub(crate) fn generate_nonce(length: usize) -> String { + rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(length) + .map(char::from) + .collect() +} diff --git a/src/validation.rs b/src/validation.rs new file mode 100644 index 0000000..aa0f6bd --- /dev/null +++ b/src/validation.rs @@ -0,0 +1,51 @@ +// This is based on https://github.com/Keats/jsonwebtoken/blob/master/src/validation.rs and is used +// to provide facade for underlying JWT library set the validation parameters for the JWT. + +use crate::Algorithm; +use std::collections::HashSet; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Validation { + pub required_spec_claims: HashSet, + pub leeway: u64, + pub validate_exp: bool, + pub validate_nbf: bool, + pub validate_aud: bool, + pub aud: Option>, + pub iss: Option>, + pub sub: Option, + pub algorithms: Algorithm, +} + +impl Validation { + pub fn new(alg: Algorithm) -> Validation { + let mut required_claims = HashSet::with_capacity(1); + required_claims.insert("exp".to_owned()); + + Validation { + required_spec_claims: required_claims, + algorithms: alg, + leeway: 60, + + validate_exp: true, + validate_nbf: false, + validate_aud: true, + + iss: None, + sub: None, + aud: None, + } + } + + pub fn no_exp(mut self) -> Self { + self.validate_exp = false; + self.required_spec_claims.remove("exp"); + self + } +} + +impl Default for Validation { + fn default() -> Self { + Self::new(Algorithm::RS256) + } +} diff --git a/src/verifier.rs b/src/verifier.rs new file mode 100644 index 0000000..6856327 --- /dev/null +++ b/src/verifier.rs @@ -0,0 +1,365 @@ +use crate::{ + base64_hash, decode, sd_jwt_parts, + utils::{drop_kb, remove_digests, restore_disclosures}, + Error, HashAlgorithm, Jwk, KeyForDecoding, Validation, +}; +use serde_json::Value; + +/// # Verifier Module +/// +/// Represents a Verifier. Verifies SD-JWT presentations. +/// +/// ## Features +/// +/// - Verifying SD-JWT presentations. +/// +/// Example +/// +/// ``` +/// use sd_jwt::{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().no_exp(); +/// let mut kb_validation = Validation::default().no_exp(); +/// 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), 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 _ = Jwk::from_value(claims["cnf"].clone())?; + + 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 sd_jwt::{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().no_exp(); + /// let mut kb_validation = Validation::default().no_exp(); + /// 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 = kb_jwk["e"].as_str().ok_or(Error::SDJWTRejected( + "Issuer SD JWT cnf claim must contain RSA key, invalid exponent".to_string(), + ))?; + let n = kb_jwk["n"].as_str().ok_or(Error::SDJWTRejected( + "Issuer SD JWT cnf claim must contain RSA key, invalid modulus".to_string(), + ))?; + 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().no_exp(); + 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(()) + } +}