├── .github ├── settings.yml └── workflows │ └── rust.yml ├── .gitignore ├── CODEOWNERS ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── MAINTAINERS.md ├── README.md ├── generate ├── Cargo.toml ├── README.md ├── sd_jwt_python.patch └── src │ ├── error.rs │ ├── main.rs │ ├── types │ ├── cli.rs │ ├── mod.rs │ ├── settings.rs │ └── specification.rs │ └── utils │ ├── funcs.rs │ ├── generate.rs │ └── mod.rs ├── src ├── disclosure.rs ├── error.rs ├── holder.rs ├── issuer.rs ├── lib.rs ├── utils.rs └── verifier.rs └── tests ├── demos.rs └── utils ├── fixtures.rs └── mod.rs /.github/settings.yml: -------------------------------------------------------------------------------- 1 | repository: 2 | private: false 3 | has_issues: true 4 | has_projects: true 5 | has_wiki: true 6 | has_downloads: true 7 | default_branch: main 8 | allow_squash_merge: true 9 | allow_merge_commit: false 10 | allow_rebase_merge: true 11 | 12 | branches: 13 | - name: main 14 | protection: 15 | required_status_checks: 16 | enforcement_level: everyone 17 | strict: true 18 | contexts: ["DCO"] 19 | enforce_admins: true -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | .idea/ 16 | 17 | # Ignore Mac DS_Store files 18 | .DS_Store 19 | **/.DS_Store -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @openwallet-foundation-labs/sd-jwt-rust-maintainers 2 | * @jovfer 3 | * @Abdulbois 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to SD-JWT 2 | 3 | We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's: 4 | 5 | - Reporting a bug 6 | - Discussing the current state of the code 7 | - Submitting a fix 8 | - Proposing new features 9 | - Becoming a maintainer 10 | 11 | ## We Develop with GitHub 12 | 13 | We use GitHub to host code, to track issues and feature requests, as well as accept pull requests. 14 | 15 | ## We Use [GitHub Flow](https://guides.github.com/introduction/flow/index.html), So All Code Changes Happen Through Pull Requests 16 | 17 | Pull requests are the best way to propose changes to the codebase (we use [GitHub Flow](https://guides.github.com/introduction/flow/index.html)). We actively welcome your pull requests: 18 | 19 | 1. Fork the repo and create your branch from `main`. 20 | 2. If you've added code that should be tested, add tests. 21 | 3. If you've changed APIs, update the documentation. 22 | 4. Ensure the test suite passes. 23 | 5. Make sure your code lints. 24 | 6. Issue that pull request! 25 | 26 | ## Any contributions you make will be under the Apache 2.0 Software License 27 | 28 | In short, when you submit code changes, your submissions are understood to be under the same [Apache 2.0 License](http://www.apache.org/licenses/) that covers the project. Feel free to contact the maintainers if that's a concern. 29 | 30 | ## Report bugs using GitHub's [issues](https://github.com/openwallet-foundation-labs/sd-jwt-rust/issues) 31 | 32 | We use GitHub issues to track public bugs. Report a bug by opening a new issue it's that easy! 33 | 34 | ## Write bug reports with detail, background, and sample code 35 | 36 | **Great Bug Reports** tend to have: 37 | 38 | - A quick summary and/or background 39 | - Steps to reproduce 40 | - Be specific! 41 | - Give sample code if you can. 42 | - What you expected would happen 43 | - What actually happens 44 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) 45 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sd-jwt-rs" 3 | version = "0.7.1" 4 | edition = "2021" 5 | license = "Apache-2.0 OR MIT" 6 | description = "Rust reference implementation of the IETF SD-JWT specification (v7)." 7 | rust-version = "1.67.0" 8 | authors = ["Abdulbois Tursunov , Sergey Minaev "] 9 | repository = "https://github.com/openwallet-foundation-labs/sd-jwt-rust" 10 | documentation = "https://docs.rs/sd-jwt-rs" 11 | homepage = "https://github.com/openwallet-foundation-labs/sd-jwt-rust" 12 | 13 | [features] 14 | mock_salts = ["lazy_static"] 15 | 16 | [dependencies] 17 | base64 = "0.21" 18 | hmac = "0.12" 19 | jsonwebtoken = "9.2" 20 | lazy_static = { version = "1.4", optional = true } 21 | log = "0.4" 22 | rand = "0.8" 23 | serde = { version = "1.0.193", features = ["derive"] } 24 | serde_json = { version = "1.0.113", features = ["preserve_order"] } 25 | sha2 = "0.10" 26 | thiserror = "1.0.51" 27 | strum = { version = "0.25", default-features = false, features = ["std", "derive"] } 28 | 29 | [dev-dependencies] 30 | rstest = "0.18.2" 31 | regex = "1.10" 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | ## Active Maintainers 2 | 3 | | Maintainer | GitHub ID | LFID | Email | Chat ID | Company Affiliation | Scope | 4 | |---------------|-----------|---------------|-----------------------------------|---------|---------------------|------------| 5 | | Sergey Minaev | jovfer | sergey.minaev | sergey.minaev.dev@gmail.com | | | repository | 6 | | Abdulbois Tursunov | Abdulbois | | abdulbois.tursunov@dsr-corporation.com | | DSR Corporation | repository | 7 | 8 | ## What Does Being a Maintainer Entail 9 | 10 | - Reviewing code contributions. 11 | - Managing issues and bugs. 12 | - Maintaining documentation. 13 | - Communicating with the community. 14 | - Managing version control. 15 | - Participating in project decisions. 16 | - Building and sustaining a contributor community. 17 | 18 | ## How to Become a Maintainer 19 | 20 | Before being considered as a maintainer, contributors should meet the following requirements: 21 | 22 | - A history of substantial and consistent contributions to the project. 23 | - A deep understanding of the project's goals, codebase, and best practices. 24 | - Active involvement in the community, including helping others and engaging in discussions. 25 | - Ultimately, the maintainers decide who will become the new maintainer through a majority vote. 26 | 27 | ## How Maintainers are Removed or Moved to Emeritus Status 28 | 29 | - Inactivity or consensus decision can lead to removal. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SD-JWT Rust Reference Implementation 2 | 3 | This is the reference implementation of the [IETF SD-JWT specification](https://datatracker.ietf.org/doc/draft-ietf-oauth-selective-disclosure-jwt/) written in Rust. 4 | Supported version: 7. 5 | 6 | Note: while the project is started as a reference implementation, it is intended to be evolved to a production-ready, high-performance implementations in the long-run. 7 | 8 | ## API 9 | Note: the current version of the crate is 0.0.x, so the API should be considered as experimental. 10 | Proposals about API improvements are highly appreciated. 11 | 12 | ```rust 13 | fn demo() { 14 | let mut issuer = SDJWTIssuer::new(issuer_key, None); 15 | let sd_jwt = issuer.issue_sd_jwt(claims, ClaimsForSelectiveDisclosureStrategy::AllLevels, holder_key, add_decoy, SDJWTSerializationFormat::Compact).unwrap(); 16 | 17 | let mut holder = SDJWTHolder::new(sd_jwt, SDJWTSerializationFormat::Compact).unwrap(); 18 | let presentation = holder.create_presentation(claims_to_disclosure, None, None, None, None).unwrap(); 19 | 20 | let verified_claims = SDJWTVerifier::new(presentation, cb_to_resolve_issuer_key, None, None, SDJWTSerializationFormat::Compact).unwrap() 21 | .verified_claims; 22 | } 23 | ``` 24 | 25 | See `tests/demos.rs` for more details; 26 | 27 | ## Repository structure 28 | 29 | ### SD-JWT Rust crate 30 | SD-JWT crate is the root of the repository. 31 | 32 | To build the project simply perform: 33 | ```shell 34 | cargo build 35 | ``` 36 | 37 | To run tests: 38 | ```shell 39 | cargo test 40 | ``` 41 | 42 | ### Interoperability testing tool 43 | See [Generate tool README](./generate/README.md) document. 44 | 45 | ## External Dependencies 46 | 47 | Dual license (MIT/Apache 2.0) dependencies: [base64](https://crates.io/crates/base64), [lazy_static](https://crates.io/crates/lazy_static) [log](https://crates.io/crates/log), [serde](https://crates.io/crates/serde), [serde_json](https://crates.io/crates/serde_json), [sha2](https://crates.io/crates/sha2), [rand](https://crates.io/crates/rand), [hmac](https://crates.io/crates/hmac), [thiserror](https://crates.io/crates/thiserror). 48 | MIT license dependencies: [jsonwebtoken](https://crates.io/crates/jsonwebtoken), [strum](https://crates.io/crates/strum) 49 | 50 | Note: the list of dependencies may be changed in the future. 51 | 52 | ## Initial Maintainers 53 | 54 | - Sergey Minaev ([Github](https://github.com/jovfer)) 55 | - DSR Corporation Decentralized Systems Team ([Github](https://github.com/orgs/DSRCorporation/teams/decentralized-systems)) 56 | -------------------------------------------------------------------------------- /generate/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sd-jwt-generate" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Abdulbois Tursunov ", "Alexander Sukhachev "] 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | clap = { version = "4.4.10", features = ["derive"] } 11 | serde = { version = "1.0.193", features = ["derive"] } 12 | serde_yaml = "0.9.27" 13 | serde_json = { version = "1.0.113", features = ["preserve_order"] } 14 | jsonwebtoken = "9.1" 15 | sd-jwt-rs = {path = "./..", features = ["mock_salts"]} -------------------------------------------------------------------------------- /generate/README.md: -------------------------------------------------------------------------------- 1 | # SD-JWT Interop tool 2 | 3 | This tool is used to verify interoperability between the `sd-jwt-rust` and `sd-jwt-python` implementations of the [IETF SD-JWT specification](https://datatracker.ietf.org/doc/draft-ietf-oauth-selective-disclosure-jwt/). 4 | 5 | ## How does the Interop tool work? 6 | 7 | The main idea is to generate data structures (SDJWT/presentation/verified claims) using both implementations and compare them. 8 | 9 | The `sd-jwt-python` is used to generate artifacts based on input data (`specification.yml`) and store them as files. 10 | The interop tool (based on `sd-jwt-rust`) is used to generate artifacts using the same specification file, load artifacts stored in files by `sd-jwt-python` and compare them. The interop tool doesn't store any files on filesystem. 11 | 12 | There are some factors that make impossible to compare data due to non-equivalence data generated by different implementations: 13 | 14 | - Using random 'salt' in each run that make results different even though they are generated by the same implementation. 15 | - Not equivalent json-serialized strings (different number of spaces) generated under the hood of the different implementations. 16 | - Using 'decoy' digests in the SD-JWT payload. 17 | 18 | In order to reach reproducibility and equivalence of the values generated by both implementations it is required to use the same input data (issuer private key, user claims, etc.) and to get rid of some non-deterministic values during data generating (values of 'salt', for example). 19 | 20 | ### Deterministic 'salt' 21 | 22 | In order to make it possible to get reproducible result each run it's required to use deterministic values of 'salt' used in internal algorithms. The `sd-jwt-python` project implements such behavior for test purposes. 23 | 24 | In order to use the same set of 'salt' values by the `sd-jwt-rust` project Python-implementation stores values in the `claims_vs_salts.json` file as artifact. The Interop tool loads values from the file and use it instead of random generated values (see the `mock_salts` feature). 25 | 26 | 27 | ### Similar json serialization 28 | 29 | In order to have the same json-strings used under the hood of the both implementations there is some code that gets rid of different number of spaces: 30 | 31 | ```rust 32 | value_str = value_str 33 | .replace(":[", ": [") 34 | .replace(',', ", ") 35 | .replace("\":", "\": ") 36 | .replace("\": ", "\": "); 37 | ``` 38 | 39 | ### 'Decoy' SD items 40 | 41 | In order to make it possible to compare `SD-JWT` payloads that contains [decoy](https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-07.html#name-decoy-digests) it was decided to detect and remove all `decoy` items from payloads and then compare them. 42 | 43 | 44 | ## How to use the interop tool? 45 | 46 | 1. Install the prerequisites 47 | 2. Clone and build the `sd-jwt-rust` project 48 | 3. Clone and build the `sd-jwt-python` project 49 | 4. Generate artifacts using the `sd-jwt-python` project 50 | 5. Run the interop tool 51 | 52 | ### Install the prerequisites 53 | 54 | In order to be able to build both implementations it is required to setup following tools: 55 | 56 | - `Rust`/`cargo` 57 | - `poetry` 58 | 59 | 60 | ### Clone and build the `sd-jwt-rust` project 61 | 62 | ```shell 63 | git clone git@github.com:openwallet-foundation-labs/sd-jwt-rust.git 64 | cd sd-jwt-rust/generate 65 | cargo build 66 | ``` 67 | 68 | 69 | ### Clone and build the `sd-jwt-python` project 70 | 71 | Once the project repo is cloned to local directory it is necessary to apply special patch. 72 | This patch is required to have some additional files as artifacts generated by the `sd-jwt-python` project. 73 | 74 | Files: 75 | 76 | - `claims_vs_salts.json` file contains values of so called 'salt' that have been used during `SDJWT` issuance. 77 | - `issuer_key.pem` file contains the issuer's private key. 78 | - `issuer_public_key.pem` file contains the issuer's public key. 79 | - `holder_key.pem` file contains the holder's private key. 80 | 81 | The files are used to make it possible for this tool to generate the same values of artifacts (SDJWT payload/SDJWT claims/presentation/verified claims) that are generated by `sd-jwt-python`. 82 | 83 | 84 | ```shell 85 | git clone git@github.com:openwallet-foundation-labs/sd-jwt-python.git 86 | cd sd-jwt-python 87 | 88 | # apply the patch 89 | git apply ../sd-jwt-rust/generate/sd_jwt_python.patch 90 | 91 | # build 92 | poetry install && poetry build 93 | ``` 94 | 95 | 96 | 97 | ### Generate artifacts using the `sd-jwt-python` project 98 | 99 | ```shell 100 | pushd sd-jwt-python/tests/testcases && poetry run ../../src/sd_jwt/bin/generate.py -- example && popd 101 | pushd sd-jwt-python/examples && poetry run ../src/sd_jwt/bin/generate.py -- example && popd 102 | ``` 103 | 104 | 105 | ### Run the interop tool 106 | 107 | ```shell 108 | cd sd-jwt-rust/generate 109 | sd_jwt_py="../../sd-jwt-python" 110 | for cases_dir in $sd_jwt_py/examples $sd_jwt_py/tests/testcases; do 111 | for test_case_dir in $(ls $cases_dir); do 112 | if [[ -d $cases_dir/$test_case_dir ]]; then 113 | ./target/debug/sd-jwt-generate -p $cases_dir/$test_case_dir 114 | fi 115 | done 116 | done 117 | ``` 118 | -------------------------------------------------------------------------------- /generate/sd_jwt_python.patch: -------------------------------------------------------------------------------- 1 | diff --git a/.gitignore b/.gitignore 2 | index 1874e26..72ff453 100644 3 | --- a/.gitignore 4 | +++ b/.gitignore 5 | @@ -157,7 +157,7 @@ cython_debug/ 6 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 7 | # and can be added to the global gitignore or merged into this file. For a more nuclear 8 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 9 | -#.idea/ 10 | +.idea/ 11 | 12 | 13 | # Ignore output of test cases except for specification.yml 14 | diff --git a/pyproject.toml b/pyproject.toml 15 | index 4294e64..47c9281 100644 16 | --- a/pyproject.toml 17 | +++ b/pyproject.toml 18 | @@ -12,7 +12,7 @@ jwcrypto = ">=1.3.1" 19 | pyyaml = ">=5.4" 20 | 21 | [tool.poetry.group.dev.dependencies] 22 | -flake8 = "^6.0.0" 23 | +# flake8 = "^6.0.0" 24 | black = "^23.3.0" 25 | 26 | [build-system] 27 | diff --git a/src/sd_jwt/bin/generate.py b/src/sd_jwt/bin/generate.py 28 | index ad00641..d0299ea 100755 29 | --- a/src/sd_jwt/bin/generate.py 30 | +++ b/src/sd_jwt/bin/generate.py 31 | @@ -105,12 +105,36 @@ def generate_test_case_data(settings: Dict, testcase_path: Path, type: str): 32 | 33 | # Write the test case data to the directory of the test case 34 | 35 | + claims_vs_salts = [] 36 | + for disclosure in sdjwt_at_issuer.ii_disclosures: 37 | + claims_vs_salts.append(disclosure.salt) 38 | + 39 | _artifacts = { 40 | "user_claims": ( 41 | remove_sdobj_wrappers(testcase["user_claims"]), 42 | "User Claims", 43 | "json", 44 | ), 45 | + "issuer_key": ( 46 | + demo_keys["issuer_key"].export_to_pem(True, None).decode("utf-8"), 47 | + "Issuer private key", 48 | + "pem", 49 | + ), 50 | + "issuer_public_key": ( 51 | + demo_keys["issuer_public_key"].export_to_pem(False, None).decode("utf-8"), 52 | + "Issuer public key", 53 | + "pem", 54 | + ), 55 | + "holder_key": ( 56 | + demo_keys["holder_key"].export_to_pem(True, None).decode("utf-8"), 57 | + "Issuer private key", 58 | + "pem", 59 | + ), 60 | + "claims_vs_salts": ( 61 | + claims_vs_salts, 62 | + "Claims with Salts", 63 | + "json", 64 | + ), 65 | "sd_jwt_payload": ( 66 | sdjwt_at_issuer.sd_jwt_payload, 67 | "Payload of the SD-JWT", 68 | diff --git a/src/sd_jwt/disclosure.py b/src/sd_jwt/disclosure.py 69 | index a9727c4..d1f983a 100644 70 | --- a/src/sd_jwt/disclosure.py 71 | +++ b/src/sd_jwt/disclosure.py 72 | @@ -15,11 +15,11 @@ class SDJWTDisclosure: 73 | self._hash() 74 | 75 | def _hash(self): 76 | - salt = self.issuer._generate_salt() 77 | + self._salt = self.issuer._generate_salt() 78 | if self.key is None: 79 | - data = [salt, self.value] 80 | + data = [self._salt, self.value] 81 | else: 82 | - data = [salt, self.key, self.value] 83 | + data = [self._salt, self.key, self.value] 84 | 85 | self._json = dumps(data).encode("utf-8") 86 | 87 | @@ -30,6 +30,10 @@ class SDJWTDisclosure: 88 | def hash(self): 89 | return self._hash 90 | 91 | + @property 92 | + def salt(self): 93 | + return self._salt 94 | + 95 | @property 96 | def b64(self): 97 | return self._raw_b64 98 | -------------------------------------------------------------------------------- /generate/src/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 DSR Corporation, Denver, Colorado. 2 | // https://www.dsr-corporation.com 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | #![allow(unused)] 6 | 7 | use std::error::Error as StdError; 8 | use std::fmt::{self, Display, Formatter}; 9 | use std::result::Result as StdResult; 10 | 11 | pub type Result = std::result::Result; 12 | 13 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 14 | pub enum ErrorKind { 15 | Input, 16 | IOError, 17 | DataNotEqual, 18 | } 19 | 20 | impl ErrorKind { 21 | #[must_use] 22 | pub const fn as_str(&self) -> &'static str { 23 | match self { 24 | Self::Input => "Input error", 25 | Self::IOError => "IO error", 26 | Self::DataNotEqual => "Data not equal error", 27 | } 28 | } 29 | } 30 | 31 | impl Display for ErrorKind { 32 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 33 | f.write_str(self.as_str()) 34 | } 35 | } 36 | 37 | /// The standard crate error type 38 | #[derive(Debug)] 39 | pub struct Error { 40 | kind: ErrorKind, 41 | pub cause: Option>, 42 | pub message: Option, 43 | // backtrace (when supported) 44 | } 45 | 46 | impl Error { 47 | pub fn from_msg>(kind: ErrorKind, msg: T) -> Self { 48 | Self { 49 | kind, 50 | cause: None, 51 | message: Some(msg.into()), 52 | } 53 | } 54 | 55 | pub fn from_opt_msg>(kind: ErrorKind, msg: Option) -> Self { 56 | Self { 57 | kind, 58 | cause: None, 59 | message: msg.map(Into::into), 60 | } 61 | } 62 | 63 | #[must_use] 64 | #[inline] 65 | pub const fn kind(&self) -> ErrorKind { 66 | self.kind 67 | } 68 | 69 | #[must_use] 70 | pub fn with_cause>>(mut self, err: T) -> Self { 71 | self.cause = Some(err.into()); 72 | self 73 | } 74 | } 75 | 76 | impl fmt::Display for Error { 77 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 78 | match (self.kind, &self.message) { 79 | (ErrorKind::Input, None) => write!(f, "{:?}", self.kind), 80 | (ErrorKind::Input, Some(msg)) => f.write_str(msg), 81 | (kind, None) => write!(f, "{kind}"), 82 | (kind, Some(msg)) => write!(f, "{kind}: {msg}"), 83 | }?; 84 | if let Some(ref source) = self.cause { 85 | write!(f, " [{source}]")?; 86 | } 87 | Ok(()) 88 | } 89 | } 90 | 91 | impl StdError for Error { 92 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 93 | self.cause 94 | .as_ref() 95 | .map(|err| unsafe { std::mem::transmute(&**err) }) 96 | } 97 | } 98 | 99 | impl PartialEq for Error { 100 | fn eq(&self, other: &Self) -> bool { 101 | self.kind == other.kind && self.message == other.message 102 | } 103 | } 104 | 105 | impl From for Error { 106 | fn from(kind: ErrorKind) -> Self { 107 | Self { 108 | kind, 109 | cause: None, 110 | message: None, 111 | } 112 | } 113 | } 114 | 115 | impl From for Error { 116 | fn from(err: std::io::Error) -> Self { 117 | Self::from(ErrorKind::IOError).with_cause(err) 118 | } 119 | } 120 | 121 | impl From for Error { 122 | fn from(err: serde_json::Error) -> Self { 123 | Self::from(ErrorKind::Input).with_cause(err) 124 | } 125 | } 126 | 127 | impl From for Error { 128 | fn from(err: serde_yaml::Error) -> Self { 129 | Self::from(ErrorKind::Input).with_cause(err) 130 | } 131 | } 132 | 133 | impl From<(ErrorKind, M)> for Error 134 | where 135 | M: fmt::Display + Send + Sync + 'static, 136 | { 137 | fn from((kind, msg): (ErrorKind, M)) -> Self { 138 | Self::from_msg(kind, msg.to_string()) 139 | } 140 | } 141 | 142 | macro_rules! err_msg { 143 | () => { 144 | $crate::error::Error::from($crate::error::ErrorKind::Input) 145 | }; 146 | ($kind:ident) => { 147 | $crate::error::Error::from($crate::error::ErrorKind::$kind) 148 | }; 149 | ($kind:ident, $($args:tt)+) => { 150 | $crate::error::Error::from_msg($crate::error::ErrorKind::$kind, format!($($args)+)) 151 | }; 152 | ($($args:tt)+) => { 153 | $crate::error::Error::from_msg($crate::error::ErrorKind::Input, format!($($args)+)) 154 | }; 155 | } 156 | 157 | macro_rules! err_map { 158 | ($($params:tt)*) => { 159 | |err| err_msg!($($params)*).with_cause(err) 160 | }; 161 | } 162 | 163 | pub trait ResultExt { 164 | fn map_err_string(self) -> StdResult; 165 | fn map_input_err(self, mapfn: F) -> Result 166 | where 167 | F: FnOnce() -> M, 168 | M: fmt::Display + Send + Sync + 'static; 169 | fn with_err_msg(self, kind: ErrorKind, msg: M) -> Result 170 | where 171 | M: fmt::Display + Send + Sync + 'static; 172 | fn with_input_err(self, msg: M) -> Result 173 | where 174 | M: fmt::Display + Send + Sync + 'static; 175 | } 176 | 177 | impl ResultExt for StdResult 178 | where 179 | E: std::error::Error + Send + Sync + 'static, 180 | { 181 | fn map_err_string(self) -> StdResult { 182 | self.map_err(|err| err.to_string()) 183 | } 184 | 185 | fn map_input_err(self, mapfn: F) -> Result 186 | where 187 | F: FnOnce() -> M, 188 | M: fmt::Display + Send + Sync + 'static, 189 | { 190 | self.map_err(|err| Error::from_msg(ErrorKind::Input, mapfn().to_string()).with_cause(err)) 191 | } 192 | 193 | fn with_err_msg(self, kind: ErrorKind, msg: M) -> Result 194 | where 195 | M: fmt::Display + Send + Sync + 'static, 196 | { 197 | self.map_err(|err| Error::from_msg(kind, msg.to_string()).with_cause(err)) 198 | } 199 | 200 | #[inline] 201 | fn with_input_err(self, msg: M) -> Result 202 | where 203 | M: fmt::Display + Send + Sync + 'static, 204 | { 205 | self.map_err(|err| Error::from_msg(ErrorKind::Input, msg.to_string()).with_cause(err)) 206 | } 207 | } 208 | 209 | type DynError = Box; 210 | 211 | macro_rules! define_error { 212 | ($name:tt, $short:expr, $doc:tt) => { 213 | #[derive(Debug, Error)] 214 | #[doc=$doc] 215 | pub struct $name { 216 | pub context: Option, 217 | pub source: Option, 218 | } 219 | 220 | impl $name { 221 | pub fn from_msg>(msg: T) -> Self { 222 | Self::from(msg.into()) 223 | } 224 | 225 | pub fn from_err(err: E) -> Self 226 | where 227 | E: StdError + Send + Sync + 'static, 228 | { 229 | Self { 230 | context: None, 231 | source: Some(Box::new(err) as DynError), 232 | } 233 | } 234 | 235 | pub fn from_msg_err(msg: M, err: E) -> Self 236 | where 237 | M: Into, 238 | E: StdError + Send + Sync + 'static, 239 | { 240 | Self { 241 | context: Some(msg.into()), 242 | source: Some(Box::new(err) as DynError), 243 | } 244 | } 245 | } 246 | 247 | impl From<&str> for $name { 248 | fn from(context: &str) -> Self { 249 | Self { 250 | context: Some(context.to_owned()), 251 | source: None, 252 | } 253 | } 254 | } 255 | 256 | impl From for $name { 257 | fn from(context: String) -> Self { 258 | Self { 259 | context: Some(context), 260 | source: None, 261 | } 262 | } 263 | } 264 | 265 | impl From> for $name { 266 | fn from(context: Option) -> Self { 267 | Self { 268 | context, 269 | source: None, 270 | } 271 | } 272 | } 273 | 274 | impl From<(M, E)> for $name 275 | where 276 | M: Into, 277 | E: StdError + Send + Sync + 'static, 278 | { 279 | fn from((context, err): (M, E)) -> Self { 280 | Self::from_msg_err(context, err) 281 | } 282 | } 283 | 284 | impl From<$name> for String { 285 | fn from(s: $name) -> Self { 286 | s.to_string() 287 | } 288 | } 289 | 290 | impl std::fmt::Display for $name { 291 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 292 | write!(f, $short)?; 293 | match self.context { 294 | Some(ref context) => write!(f, ": {}", context), 295 | None => Ok(()), 296 | } 297 | } 298 | } 299 | }; 300 | } -------------------------------------------------------------------------------- /generate/src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 DSR Corporation, Denver, Colorado. 2 | // https://www.dsr-corporation.com 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | mod error; 6 | mod types; 7 | mod utils; 8 | 9 | use jsonwebtoken::jwk::Jwk; 10 | 11 | use crate::error::{Error, ErrorKind, Result}; 12 | use crate::utils::funcs::{parse_sdjwt_paylod, load_salts}; 13 | use clap::Parser; 14 | use jsonwebtoken::{EncodingKey, DecodingKey}; 15 | use sd_jwt_rs::issuer::{ClaimsForSelectiveDisclosureStrategy, SDJWTIssuer}; 16 | use sd_jwt_rs::holder::SDJWTHolder; 17 | use sd_jwt_rs::verifier::SDJWTVerifier; 18 | use sd_jwt_rs::SDJWTSerializationFormat; 19 | use serde_json::{Number, Value}; 20 | use std::path::PathBuf; 21 | use types::cli::{Cli, GenerateType}; 22 | use types::settings::Settings; 23 | use types::specification::Specification; 24 | 25 | const ISSUER_KEY_PEM_FILE_NAME: &str = "issuer_key.pem"; 26 | const ISSUER_PUBLIC_KEY_PEM_FILE_NAME: &str = "issuer_public_key.pem"; 27 | // const HOLDER_KEY_PEM_FILE_NAME: &str = "holder_key.pem"; 28 | const SETTINGS_FILE_NAME: &str = "settings.yml"; 29 | const SPECIFICATION_FILE_NAME: &str = "specification.yml"; 30 | const SALTS_FILE_NAME: &str = "claims_vs_salts.json"; 31 | const SD_JWT_FILE_NAME_TEMPLATE: &str = "sd_jwt_issuance"; 32 | const VERIFIED_CLAIMS_FILE_NAME: &str = "verified_contents.json"; 33 | 34 | fn main() { 35 | let args = Cli::parse(); 36 | 37 | println!("type_: {:?}, paths: {:?}", args.type_.clone(), args.paths); 38 | 39 | let basedir = std::env::current_dir().expect("Unable to get current directory"); 40 | let spec_directories = get_specification_paths(&args, basedir).unwrap(); 41 | 42 | for mut directory in spec_directories { 43 | println!("Generating data for '{:?}'", directory); 44 | let settings = get_settings(&directory.parent().unwrap().join("..").join(SETTINGS_FILE_NAME)); 45 | let specs = Specification::from(&directory); 46 | 47 | // Remove specification.yaml from path 48 | directory.pop(); 49 | 50 | generate_and_check(&directory, &settings, specs, args.type_.clone()).unwrap(); 51 | } 52 | } 53 | 54 | fn generate_and_check( 55 | directory: &PathBuf, 56 | settings: &Settings, 57 | specs: Specification, 58 | _: GenerateType, 59 | ) -> Result<()> { 60 | let decoy = specs.add_decoy_claims.unwrap_or(false); 61 | let serialization_format; 62 | let stored_sd_jwt_file_path; 63 | 64 | match &specs.serialization_format { 65 | Some(format) if format == "json" => { 66 | serialization_format = SDJWTSerializationFormat::JSON; 67 | stored_sd_jwt_file_path = directory.join(format!("{SD_JWT_FILE_NAME_TEMPLATE}.json")); 68 | }, 69 | Some(format) if format == "compact" => { 70 | serialization_format = SDJWTSerializationFormat::Compact; 71 | stored_sd_jwt_file_path = directory.join(format!("{SD_JWT_FILE_NAME_TEMPLATE}.txt")); 72 | }, 73 | None => { 74 | println!("using default serialization format: Compact"); 75 | serialization_format = SDJWTSerializationFormat::Compact; 76 | stored_sd_jwt_file_path = directory.join(format!("{SD_JWT_FILE_NAME_TEMPLATE}.txt")); 77 | }, 78 | Some(format) => { 79 | panic!("unsupported format: {format}"); 80 | }, 81 | }; 82 | 83 | let sd_jwt = issue_sd_jwt(directory, &specs, settings, serialization_format.clone(), decoy)?; 84 | let presentation = create_presentation(&sd_jwt, serialization_format.clone(), &specs.holder_disclosed_claims)?; 85 | 86 | // Verify presentation 87 | let verified_claims = verify_presentation(directory, &presentation, serialization_format.clone())?; 88 | 89 | let loaded_sd_jwt = load_sd_jwt(&stored_sd_jwt_file_path)?; 90 | 91 | let loaded_sdjwt_paylod = parse_sdjwt_paylod(&loaded_sd_jwt.replace('\n', ""), &serialization_format, decoy)?; 92 | let issued_sdjwt_paylod = parse_sdjwt_paylod(&sd_jwt, &serialization_format, decoy)?; 93 | 94 | compare_jwt_payloads(&loaded_sdjwt_paylod, &issued_sdjwt_paylod)?; 95 | 96 | let loaded_verified_claims_content = load_sd_jwt(&directory.join(VERIFIED_CLAIMS_FILE_NAME))?; 97 | let loaded_verified_claims = parse_verified_claims(&loaded_verified_claims_content)?; 98 | 99 | compare_verified_claims(&loaded_verified_claims, &verified_claims)?; 100 | 101 | Ok(()) 102 | } 103 | 104 | fn issue_sd_jwt( 105 | directory: &PathBuf, 106 | specs: &Specification, 107 | settings: &Settings, 108 | serialization_format: SDJWTSerializationFormat, 109 | decoy: bool 110 | ) -> Result { 111 | let issuer_key = get_key(&directory.join(ISSUER_KEY_PEM_FILE_NAME)); 112 | 113 | let mut user_claims = specs.user_claims.claims_to_json_value()?; 114 | let claims_obj = user_claims.as_object_mut().expect("must be an object"); 115 | 116 | if !claims_obj.contains_key("iss") { 117 | claims_obj.insert(String::from("iss"), Value::String(settings.identifiers.issuer.clone())); 118 | } 119 | 120 | if !claims_obj.contains_key("iat") { 121 | let iat = settings.iat.expect("'iat' value must be provided by settings.yml"); 122 | claims_obj.insert(String::from("iat"), Value::Number(Number::from(iat))); 123 | } 124 | 125 | if !claims_obj.contains_key("exp") { 126 | let exp = settings.exp.expect("'expt' value must be provided by settings.yml"); 127 | claims_obj.insert(String::from("exp"), Value::Number(Number::from(exp))); 128 | } 129 | 130 | let sd_claims_jsonpaths = specs.user_claims.sd_claims_to_jsonpath()?; 131 | 132 | let strategy = 133 | ClaimsForSelectiveDisclosureStrategy::Custom(sd_claims_jsonpaths.iter().map(String::as_str).collect()); 134 | 135 | let jwk: Option = if specs.key_binding.unwrap_or(false) { 136 | let jwk: Jwk = serde_yaml::from_value(settings.key_settings.holder_key.clone()).unwrap(); 137 | Some(jwk) 138 | } else { 139 | None 140 | }; 141 | 142 | let mut issuer = SDJWTIssuer::new(issuer_key, Some(String::from("ES256"))); 143 | let sd_jwt = issuer.issue_sd_jwt( 144 | user_claims, 145 | strategy, 146 | jwk, 147 | decoy, 148 | serialization_format) 149 | .unwrap(); 150 | 151 | Ok(sd_jwt) 152 | } 153 | 154 | fn create_presentation( 155 | sd_jwt: &str, 156 | serialization_format: SDJWTSerializationFormat, 157 | disclosed_claims: &serde_json::Map 158 | ) -> Result { 159 | let mut holder = SDJWTHolder::new(sd_jwt.to_string(), serialization_format).unwrap(); 160 | 161 | let presentation = holder 162 | .create_presentation( 163 | disclosed_claims.clone(), 164 | None, 165 | None, 166 | None, 167 | None 168 | ).unwrap(); 169 | 170 | Ok(presentation) 171 | } 172 | 173 | fn verify_presentation( 174 | directory: &PathBuf, 175 | presentation: &str, 176 | serialization_format: SDJWTSerializationFormat 177 | ) -> Result { 178 | let pub_key_path = directory.clone().join(ISSUER_PUBLIC_KEY_PEM_FILE_NAME); 179 | 180 | let _verified = SDJWTVerifier::new( 181 | presentation.to_string(), 182 | Box::new(move |_, _| { 183 | let key = std::fs::read(&pub_key_path).expect("Failed to read file"); 184 | DecodingKey::from_ec_pem(&key).expect("Unable to create EncodingKey") 185 | }), 186 | None, 187 | None, 188 | serialization_format, 189 | ).unwrap(); 190 | 191 | Ok(_verified.verified_claims) 192 | } 193 | 194 | fn parse_verified_claims(content: &str) -> Result { 195 | let json_value: Value = serde_json::from_str(content)?; 196 | 197 | // TODO: check if the json_value is json object 198 | Ok(json_value) 199 | } 200 | 201 | fn load_sd_jwt(path: &PathBuf) -> Result { 202 | let content = std::fs::read_to_string(path)?; 203 | Ok(content) 204 | } 205 | 206 | fn compare_jwt_payloads(loaded_payload: &Value, issued_payload: &Value) -> Result<()> { 207 | if issued_payload.eq(loaded_payload) { 208 | println!("\nJWT payloads are equal"); 209 | } else { 210 | eprintln!("\nJWT payloads are NOT equal"); 211 | 212 | println!("Issued SD-JWT \n {:#?}", issued_payload); 213 | println!("Loaded SD-JWT \n {:#?}", loaded_payload); 214 | 215 | return Err(Error::from_msg(ErrorKind::DataNotEqual, "JWT payloads are different")); 216 | } 217 | 218 | Ok(()) 219 | } 220 | 221 | fn compare_verified_claims(loaded_claims: &Value, verified_claims: &Value) -> Result<()> { 222 | if loaded_claims.eq(verified_claims) { 223 | println!("Verified claims are equal",); 224 | } else { 225 | eprintln!("Verified claims are NOT equal"); 226 | 227 | println!("Issued verified claims \n {:#?}", verified_claims); 228 | println!("Loaded verified claims \n {:#?}", loaded_claims); 229 | 230 | return Err(Error::from_msg(ErrorKind::DataNotEqual, "verified claims are different")); 231 | } 232 | 233 | Ok(()) 234 | } 235 | 236 | fn get_key(path: &PathBuf) -> EncodingKey { 237 | let key = std::fs::read(path).expect("Failed to read file"); 238 | 239 | EncodingKey::from_ec_pem(&key).expect("Unable to create EncodingKey") 240 | } 241 | 242 | fn get_settings(path: &PathBuf) -> Settings { 243 | println!("settings.yaml - {:?}", path); 244 | 245 | Settings::from(path) 246 | } 247 | 248 | fn get_specification_paths(args: &Cli, basedir: PathBuf) -> Result> { 249 | let glob: Vec; 250 | if args.paths.is_empty() { 251 | glob = basedir 252 | .read_dir()? 253 | .filter_map(|entry| { 254 | if let Ok(entry) = entry { 255 | let path = entry.path(); 256 | if path.is_dir() && path.join(SPECIFICATION_FILE_NAME).exists() { 257 | // load_salts(&path).map_err(|err| Error::from_msg(ErrorKind::IOError, err.to_string()))?; 258 | load_salts(&path.join(SALTS_FILE_NAME)).unwrap(); 259 | return Some(path.join(SPECIFICATION_FILE_NAME)); 260 | } 261 | } 262 | None 263 | }) 264 | .collect(); 265 | } else { 266 | glob = args 267 | .paths 268 | .iter() 269 | .map(|d| { 270 | // load_salts(&path).map_err(|err| Error::from_msg(ErrorKind::IOError, err.to_string()))?; 271 | load_salts(&d.join(SALTS_FILE_NAME)).unwrap(); 272 | basedir.join(d).join(SPECIFICATION_FILE_NAME) 273 | }) 274 | .collect(); 275 | } 276 | 277 | println!("specification.yaml files - {:?}", glob); 278 | 279 | Ok(glob) 280 | } 281 | -------------------------------------------------------------------------------- /generate/src/types/cli.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 DSR Corporation, Denver, Colorado. 2 | // https://www.dsr-corporation.com 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | use clap::Parser; 6 | use serde::Serialize; 7 | 8 | #[derive(Parser)] 9 | pub struct Cli { 10 | /// The type to generate 11 | #[arg(short, value_enum, default_value_t = GenerateType::Example)] 12 | pub type_: GenerateType, 13 | /// The paths to the directories where specification.yaml file is located 14 | #[arg(short, value_delimiter = ' ', num_args = 0.., require_equals = false)] 15 | pub paths: Vec, 16 | } 17 | 18 | 19 | #[derive(clap::ValueEnum, Clone, Debug, Serialize)] 20 | #[serde(rename_all = "kebab-case")] 21 | pub enum GenerateType { 22 | Example, 23 | TestCase, 24 | } -------------------------------------------------------------------------------- /generate/src/types/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 DSR Corporation, Denver, Colorado. 2 | // https://www.dsr-corporation.com 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | pub mod settings; 6 | pub mod specification; 7 | pub mod cli; -------------------------------------------------------------------------------- /generate/src/types/settings.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 DSR Corporation, Denver, Colorado. 2 | // https://www.dsr-corporation.com 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | use std::path::PathBuf; 6 | use serde::{Deserialize, Serialize}; 7 | use serde_yaml::Value; 8 | 9 | #[derive(Serialize, Deserialize, PartialEq, Debug)] 10 | pub struct KeySettings { 11 | pub key_size: i32, 12 | pub kty: String, 13 | pub issuer_key: Key, 14 | pub holder_key: Value, 15 | } 16 | 17 | #[derive(Serialize, Deserialize, PartialEq, Debug)] 18 | pub struct Key { 19 | pub kty: String, 20 | pub d: String, 21 | pub crv: String, 22 | pub x: String, 23 | pub y: String, 24 | } 25 | 26 | #[derive(Serialize, Deserialize, PartialEq, Debug)] 27 | pub struct Identifiers { 28 | pub issuer: String, 29 | pub verifier: String, 30 | } 31 | 32 | #[derive(Serialize, Deserialize, PartialEq, Debug)] 33 | pub struct Settings { 34 | pub identifiers: Identifiers, 35 | pub key_settings: KeySettings, 36 | pub key_binding_nonce: String, 37 | pub expiry_seconds: Option, 38 | pub random_seed: Option, 39 | pub iat: Option, 40 | pub exp: Option, 41 | } 42 | 43 | impl From<&PathBuf> for Settings { 44 | fn from(path: &PathBuf) -> Self { 45 | let contents = std::fs::read_to_string(path) 46 | .expect("Failed to read settings file"); 47 | 48 | let settings: Settings = serde_yaml::from_str(&contents) 49 | .expect("Failed to parse YAML"); 50 | 51 | settings 52 | } 53 | } 54 | 55 | #[cfg(test)] 56 | mod tests { 57 | use crate::types::settings::Settings; 58 | 59 | #[test] 60 | fn test_test_settings() { 61 | let yaml_str = r#" 62 | identifiers: 63 | issuer: "https://example.com/issuer" 64 | verifier: "https://example.com/verifier" 65 | 66 | key_settings: 67 | key_size: 256 68 | kty: "EC" 69 | issuer_key: 70 | kty: "EC" 71 | d: "Ur2bNKuBPOrAaxsRnbSH6hIhmNTxSGXshDSUD1a1y7g" 72 | crv: "P-256" 73 | x: "b28d4MwZMjw8-00CG4xfnn9SLMVMM19SlqZpVb_uNtQ" 74 | y: "Xv5zWwuoaTgdS6hV43yI6gBwTnjukmFQQnJ_kCxzqk8" 75 | holder_key: 76 | kty: "EC" 77 | d: "5K5SCos8zf9zRemGGUl6yfok-_NiiryNZsvANWMhF-I" 78 | crv: "P-256" 79 | x: "TCAER19Zvu3OHF4j4W4vfSVoHIP1ILilDls7vCeGemc" 80 | y: "ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ" 81 | 82 | key_binding_nonce: "1234567890" 83 | 84 | expiry_seconds: 86400000 85 | random_seed: 0 86 | iat: 1683000000 87 | exp: 1883000000 88 | "#; 89 | 90 | let settings: Settings = serde_yaml::from_str(yaml_str).unwrap(); 91 | println!("{:#?}", settings); 92 | assert_eq!(settings.identifiers.issuer, "https://example.com/issuer"); 93 | } 94 | } 95 | 96 | -------------------------------------------------------------------------------- /generate/src/types/specification.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 DSR Corporation, Denver, Colorado. 2 | // https://www.dsr-corporation.com 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | use crate::utils::generate::generate_jsonpath_from_tagged_values; 6 | use serde::{Deserialize, Serialize}; 7 | use serde_yaml::Value; 8 | use std::path::PathBuf; 9 | use crate::error::Result; 10 | 11 | const SD_TAG: &str = "!sd"; 12 | 13 | #[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Default)] 14 | pub struct Specification { 15 | pub user_claims: UserClaims, 16 | pub holder_disclosed_claims: serde_json::Map, 17 | pub add_decoy_claims: Option, 18 | pub key_binding: Option, 19 | pub serialization_format: Option, 20 | } 21 | 22 | impl Specification { 23 | fn update_disclosed_claims(&mut self) { 24 | // not to transform top-level empty object 25 | if self.holder_disclosed_claims.is_empty() { 26 | return; 27 | } 28 | 29 | let res = replace_empty_items(&serde_json::Value::Object(self.holder_disclosed_claims.clone())); 30 | self.holder_disclosed_claims = res.as_object().unwrap().clone(); 31 | } 32 | } 33 | 34 | fn replace_empty_items(m: &serde_json::Value) -> serde_json::Value { 35 | match m { 36 | serde_json::Value::Array(arr) if (arr.is_empty()) => { 37 | serde_json::Value::Bool(false) 38 | } 39 | serde_json::Value::Object(obj) if (obj.is_empty()) => { 40 | serde_json::Value::Bool(false) 41 | } 42 | serde_json::Value::Array(arr) => { 43 | let mut result = Vec::new(); 44 | 45 | for value in arr { 46 | result.push(replace_empty_items(value)); 47 | } 48 | 49 | serde_json::Value::Array(result) 50 | } 51 | serde_json::Value::Object(obj) => { 52 | let mut result = serde_json::Map::new(); 53 | 54 | for (key, value) in obj { 55 | result.insert(key.clone(), replace_empty_items(value)); 56 | } 57 | 58 | serde_json::Value::Object(result) 59 | } 60 | _ => { 61 | m.clone() 62 | } 63 | } 64 | } 65 | 66 | impl From<&str> for Specification { 67 | fn from(value: &str) -> Self { 68 | let mut result = serde_yaml::from_str(value).unwrap_or(Specification::default()); 69 | result.update_disclosed_claims(); 70 | result 71 | } 72 | } 73 | 74 | impl From<&PathBuf> for Specification { 75 | fn from(path: &PathBuf) -> Self { 76 | let contents = std::fs::read_to_string(path).expect("Failed to read specification file"); 77 | 78 | let mut spec: Specification = serde_yaml::from_str(&contents).expect("Failed to parse YAML"); 79 | 80 | spec.update_disclosed_claims(); 81 | 82 | spec 83 | } 84 | } 85 | 86 | #[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Default)] 87 | pub struct UserClaims(Value); 88 | 89 | impl UserClaims { 90 | pub fn claims_to_json_value(&self) -> Result { 91 | let filtered_value = _remove_tags(&self.0); 92 | let json_value: serde_json::Value = 93 | serde_yaml::from_value(filtered_value).expect("Failed to convert serde_json::Value"); 94 | 95 | Ok(json_value) 96 | } 97 | 98 | pub fn sd_claims_to_jsonpath(&self) -> Result> { 99 | let path = "".to_string(); 100 | let mut paths = Vec::new(); 101 | let mut claims = self.0.clone(); 102 | 103 | let _ = generate_jsonpath_from_tagged_values(&mut claims, path, &mut paths); 104 | 105 | Ok(paths) 106 | } 107 | } 108 | 109 | fn _validate(value: &Value) -> Result<()> { 110 | match value { 111 | Value::String(_) | Value::Bool(_) | Value::Number(_) => Ok(()), 112 | Value::Tagged(tag) => { 113 | if tag.tag == SD_TAG { 114 | _validate(&tag.value) 115 | } else { 116 | panic!( 117 | "Unsupported tag {:?} in claim-name, only !sd tag is supported", 118 | tag.tag 119 | ); 120 | } 121 | } 122 | Value::Sequence(list) => { 123 | for v in list { 124 | _validate(v)?; 125 | } 126 | 127 | Ok(()) 128 | } 129 | Value::Mapping(map) => { 130 | for (key, value) in map { 131 | _validate(key)?; 132 | _validate(value)?; 133 | } 134 | 135 | Ok(()) 136 | } 137 | 138 | _ => { 139 | panic!("Unsupported type for claim-name, it can be only string or tagged"); 140 | } 141 | } 142 | } 143 | 144 | fn _remove_tags(original: &Value) -> Value { 145 | match original { 146 | Value::Tagged(tag) => _remove_tags(&tag.value), 147 | Value::Mapping(map) => { 148 | let mut filtered_map = serde_yaml::Mapping::new(); 149 | 150 | for (key, value) in map.iter() { 151 | match key { 152 | Value::Tagged(tag) => { 153 | let filtered_value = _remove_tags(value); 154 | 155 | filtered_map.insert(tag.value.clone(), filtered_value); 156 | } 157 | Value::Null => {} 158 | _ => { 159 | let filtered_value = _remove_tags(value); 160 | filtered_map.insert(key.clone(), filtered_value); 161 | } 162 | } 163 | } 164 | 165 | Value::Mapping(filtered_map) 166 | } 167 | Value::Sequence(seq) => { 168 | let filtered_seq: Vec = seq.iter().map(_remove_tags).collect(); 169 | 170 | Value::Sequence(filtered_seq) 171 | } 172 | other => other.clone(), 173 | } 174 | } 175 | #[cfg(test)] 176 | mod tests { 177 | use crate::types::specification::Specification; 178 | 179 | #[test] 180 | fn test_specification() { 181 | let yaml_str = r#" 182 | user_claims: 183 | sub: 6c5c0a49-b589-431d-bae7-219122a9ec2c 184 | !sd address: 185 | street_address: Schulstr. 12 186 | !sd street_address1: Schulstr. 12 187 | 188 | holder_disclosed_claims: {} 189 | "#; 190 | 191 | let spec = Specification::from(yaml_str); 192 | println!("{:?}", spec.user_claims.claims_to_json_value().unwrap()) 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /generate/src/utils/funcs.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 DSR Corporation, Denver, Colorado. 2 | // https://www.dsr-corporation.com 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | use std::collections::HashSet; 6 | use std::path::PathBuf; 7 | 8 | use serde_json::Value; 9 | use sd_jwt_rs::SDJWTSerializationFormat; 10 | use sd_jwt_rs::utils::{base64_hash, base64url_decode}; 11 | use sd_jwt_rs::utils::SALTS; 12 | use crate::error::{Error, ErrorKind, Result}; 13 | 14 | 15 | pub fn parse_sdjwt_paylod( 16 | sd_jwt: &str, 17 | serialization_format: &SDJWTSerializationFormat, 18 | remove_decoy: bool 19 | ) -> Result { 20 | 21 | match serialization_format { 22 | SDJWTSerializationFormat::JSON => { 23 | parse_payload_json(sd_jwt, remove_decoy) 24 | }, 25 | SDJWTSerializationFormat::Compact => { 26 | parse_payload_compact(sd_jwt, remove_decoy) 27 | } 28 | } 29 | } 30 | 31 | fn parse_payload_json(sd_jwt: &str, remove_decoy: bool) -> Result { 32 | let v: serde_json::Value = serde_json::from_str(sd_jwt).unwrap(); 33 | 34 | let disclosures = v.as_object().unwrap().get("disclosures").unwrap(); 35 | 36 | let mut hashes: HashSet = HashSet::new(); 37 | 38 | for disclosure in disclosures.as_array().unwrap() { 39 | let hash = base64_hash(disclosure.as_str().unwrap().replace(' ', "").as_bytes()); 40 | hashes.insert(hash.clone()); 41 | } 42 | 43 | let ddd = v.as_object().unwrap().get("payload").unwrap().as_str().unwrap().replace(' ', ""); 44 | let payload = base64url_decode(&ddd).unwrap(); 45 | 46 | let payload: serde_json::Value = serde_json::from_slice(&payload).unwrap(); 47 | 48 | if remove_decoy { 49 | return Ok(remove_decoy_items(&payload, &hashes)); 50 | } 51 | 52 | Ok(payload) 53 | } 54 | 55 | fn parse_payload_compact(sd_jwt: &str, remove_decoy: bool) -> Result { 56 | let mut disclosures: Vec = sd_jwt 57 | .split('~') 58 | .filter(|s| !s.is_empty()) 59 | .map(String::from) 60 | .collect(); 61 | 62 | let payload = disclosures.remove(0); 63 | 64 | let payload: Vec<_> = payload.split('.').collect(); 65 | let payload = String::from(payload[1]); 66 | 67 | let mut hashes: HashSet = HashSet::new(); 68 | 69 | for disclosure in disclosures { 70 | let hash = base64_hash(disclosure.as_bytes()); 71 | hashes.insert(hash.clone()); 72 | } 73 | 74 | let payload = base64url_decode(&payload).unwrap(); 75 | 76 | let payload: serde_json::Value = serde_json::from_slice(&payload).unwrap(); 77 | 78 | if remove_decoy { 79 | return Ok(remove_decoy_items(&payload, &hashes)); 80 | } 81 | 82 | Ok(payload) 83 | } 84 | 85 | fn remove_decoy_items(payload: &Value, hashes: &HashSet) -> Value { 86 | let mut map: serde_json::Map = serde_json::Map::new(); 87 | 88 | for (key, val) in payload.as_object().unwrap() { 89 | if key == "_sd" { 90 | let v1: Vec<_> = val.as_array().unwrap().iter() 91 | .filter(|item| hashes.contains(item.as_str().unwrap())).cloned() 92 | .collect(); 93 | 94 | let filtered_array = serde_json::Value::Array(v1); 95 | map.insert(key.clone(), filtered_array); 96 | } else if val.is_object() { 97 | let filtered_object = remove_decoy_items(val, hashes); 98 | map.insert(key.clone(), filtered_object); 99 | } else { 100 | map.insert(key.clone(), val.clone()); 101 | } 102 | } 103 | 104 | Value::Object(map) 105 | } 106 | 107 | pub fn load_salts(path: &PathBuf) -> Result<()> { 108 | let json_data = std::fs::read_to_string(path) 109 | .map_err(|e| Error::from_msg(ErrorKind::IOError, e.to_string()))?; 110 | let salts: Vec = serde_json::from_str(&json_data)?; 111 | 112 | { 113 | let mut s = SALTS.lock().unwrap(); 114 | 115 | for salt in salts.iter() { 116 | s.push_back(salt.clone()); 117 | } 118 | } 119 | 120 | Ok(()) 121 | } 122 | -------------------------------------------------------------------------------- /generate/src/utils/generate.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 DSR Corporation, Denver, Colorado. 2 | // https://www.dsr-corporation.com 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | use serde_yaml::Value; 6 | use crate::error::Result; 7 | 8 | #[allow(unused)] 9 | pub fn generate_jsonpath_from_tagged_values( 10 | yaml: &Value, 11 | mut path: String, 12 | paths: &mut Vec, 13 | ) -> Result<()> { 14 | 15 | if path.is_empty() { 16 | path.push('$'); 17 | } 18 | 19 | match yaml { 20 | Value::Mapping(map) => { 21 | for (key, value) in map { 22 | // Handle nested 23 | 24 | let mut subpath: String; 25 | 26 | match key { 27 | Value::Tagged(tagged) => { 28 | subpath = format!("{}.{}", &path, tagged.value.as_str().unwrap()); 29 | paths.push(subpath.clone()); 30 | generate_jsonpath_from_tagged_values(value, subpath, paths); 31 | } 32 | Value::String(s) => { 33 | subpath = format!("{}.{}", &path, &s); 34 | generate_jsonpath_from_tagged_values(value, subpath, paths); 35 | } 36 | _ => {} 37 | } 38 | } 39 | } 40 | Value::Sequence(seq) => { 41 | for (idx, value) in seq.iter().enumerate() { 42 | 43 | let mut subpath = format!("{}.[{}]", &path, idx); 44 | generate_jsonpath_from_tagged_values(value, subpath, paths); 45 | } 46 | } 47 | Value::Tagged(tagged) => { 48 | // TODO: handle other value types (int/bool/etc) 49 | 50 | match &tagged.value { 51 | Value::Mapping(m) => { 52 | paths.push(path.clone()); 53 | generate_jsonpath_from_tagged_values(&tagged.value, path.clone(), paths); 54 | } 55 | Value::Sequence(s) => { 56 | paths.push(path.clone()); 57 | generate_jsonpath_from_tagged_values(&tagged.value, path.clone(), paths); 58 | } 59 | _ => { 60 | paths.push(path.clone()); 61 | } 62 | } 63 | 64 | } 65 | _ => {} 66 | } 67 | 68 | Ok(()) 69 | } 70 | -------------------------------------------------------------------------------- /generate/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 DSR Corporation, Denver, Colorado. 2 | // https://www.dsr-corporation.com 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | pub mod generate; 6 | pub mod funcs; -------------------------------------------------------------------------------- /src/disclosure.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 DSR Corporation, Denver, Colorado. 2 | // https://www.dsr-corporation.com 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | use crate::utils::{base64_hash, base64url_encode}; 6 | #[cfg(not(feature = "mock_salts"))] 7 | use crate::utils::generate_salt; 8 | #[cfg(feature = "mock_salts")] 9 | use crate::utils::generate_salt_mock; 10 | use serde_json::Value; 11 | 12 | 13 | #[derive(Debug)] 14 | pub(crate) struct SDJWTDisclosure { 15 | pub raw_b64: String, 16 | pub hash: String, 17 | } 18 | 19 | impl SDJWTDisclosure { 20 | pub(crate) fn new(key: Option, value: V) -> Self where V: ToString { 21 | #[cfg(not(feature = "mock_salts"))] 22 | let salt = generate_salt(); 23 | let mut value_str = value.to_string(); 24 | 25 | #[cfg(feature = "mock_salts")] 26 | let salt = { 27 | value_str = value_str 28 | .replace(":[", ": [") 29 | .replace(',', ", ") 30 | .replace("\":", "\": ") 31 | .replace("\": ", "\": "); 32 | generate_salt_mock() 33 | }; 34 | 35 | if !value_str.is_ascii() { 36 | value_str = escape_unicode_chars(&value_str); 37 | } 38 | 39 | let data = if let Some(key) = &key { 40 | format!(r#"["{}", {}, {}]"#, salt, escape_json(key), value_str) 41 | } else { 42 | format!(r#"["{}", {}]"#, salt, value_str) 43 | }; 44 | 45 | let raw_b64 = base64url_encode(data.as_bytes()); 46 | let hash = base64_hash(raw_b64.as_bytes()); 47 | 48 | Self { 49 | raw_b64, 50 | hash, 51 | } 52 | } 53 | } 54 | 55 | fn escape_unicode_chars(s: &str) -> String { 56 | let mut result = String::new(); 57 | 58 | for c in s.chars() { 59 | if c.is_ascii() { 60 | result.push(c); 61 | } else { 62 | let esc_c = c.escape_unicode().to_string(); 63 | 64 | let esc_c_new = match esc_c.chars().count() { 65 | 6 => esc_c.replace("\\u{", "\\u00").replace('}', ""), // example: \u{de} 66 | 7 => esc_c.replace("\\u{", "\\u0").replace('}', ""), // example: \u{980} 67 | 8 => esc_c.replace("\\u{", "\\u").replace('}', ""), // example: \u{23f0} 68 | _ => {panic!("unexpected value")} 69 | }; 70 | 71 | result.push_str(&esc_c_new); 72 | } 73 | } 74 | 75 | result 76 | } 77 | 78 | fn escape_json(s: &str) -> String { 79 | Value::String(String::from(s)).to_string() 80 | } 81 | 82 | #[cfg(test)] 83 | mod tests { 84 | use super::*; 85 | use crate::utils::base64url_decode; 86 | use regex::Regex; 87 | 88 | 89 | #[test] 90 | fn test_sdjwt_disclosure_when_key_is_none() { 91 | let sdjwt_disclosure = SDJWTDisclosure::new(None, "test"); 92 | let decoded_disclosure: String = String::from_utf8(base64url_decode(&sdjwt_disclosure.raw_b64).unwrap()).unwrap(); 93 | 94 | let re = Regex::new(r#"\[".*", test]"#).unwrap(); 95 | assert!(re.is_match(&decoded_disclosure)); 96 | } 97 | 98 | #[test] 99 | fn test_sdjwt_disclosure_when_key_is_present() { 100 | let sdjwt_disclosure = SDJWTDisclosure::new(Some("key".to_string()), "test"); 101 | let decoded = String::from_utf8(base64url_decode(&sdjwt_disclosure.raw_b64).unwrap()).unwrap(); 102 | 103 | let re = Regex::new(r#"\[".*", "key", test]"#).unwrap(); 104 | assert!(re.is_match(&decoded)); } 105 | } 106 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 DSR Corporation, Denver, Colorado. 2 | // https://www.dsr-corporation.com 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | pub type Result = ::core::result::Result; 6 | 7 | #[derive(Debug, thiserror::Error, strum::IntoStaticStr)] 8 | #[non_exhaustive] 9 | pub enum Error { 10 | #[error("conversion error: Cannot convert to {0}")] 11 | ConversionError(String), 12 | 13 | #[error("invalid input: {0}")] 14 | DeserializationError(String), 15 | 16 | #[error("data field is not expected: {0}")] 17 | DataFieldMismatch(String), 18 | 19 | #[error("Digest {0} appears multiple times")] 20 | DuplicateDigestError(String), 21 | 22 | #[error("Key {0} appears multiple times")] 23 | DuplicateKeyError(String), 24 | 25 | #[error("invalid disclosure: {0}")] 26 | InvalidDisclosure(String), 27 | 28 | #[error("invalid array disclosure: {0}")] 29 | InvalidArrayDisclosureObject(String), 30 | 31 | #[error("invalid path: {0}")] 32 | InvalidPath(String), 33 | 34 | #[error("index {idx} is out of bounds for the provided array with length {length}: {msg}")] 35 | IndexOutOfBounds { 36 | idx: usize, 37 | length: usize, 38 | msg: String, 39 | }, 40 | 41 | #[error("invalid state: {0}")] 42 | InvalidState(String), 43 | 44 | #[error("invalid input: {0}")] 45 | InvalidInput(String), 46 | 47 | #[error("key not found: {0}")] 48 | KeyNotFound(String), 49 | 50 | #[error("{0}")] 51 | Unspecified(String), 52 | } 53 | -------------------------------------------------------------------------------- /src/holder.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 DSR Corporation, Denver, Colorado. 2 | // https://www.dsr-corporation.com 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | use crate::{error, SDJWTJson, SDJWTSerializationFormat}; 6 | use error::{Error, Result}; 7 | use jsonwebtoken::{Algorithm, EncodingKey, Header}; 8 | use serde_json::{Map, Value}; 9 | use std::collections::HashMap; 10 | use std::ops::Add; 11 | use std::str::FromStr; 12 | use std::time; 13 | 14 | use crate::utils::base64_hash; 15 | use crate::SDJWTCommon; 16 | use crate::{ 17 | COMBINED_SERIALIZATION_FORMAT_SEPARATOR, DEFAULT_SIGNING_ALG, KB_DIGEST_KEY, SD_DIGESTS_KEY, 18 | SD_LIST_PREFIX, 19 | }; 20 | 21 | pub struct SDJWTHolder { 22 | sd_jwt_engine: SDJWTCommon, 23 | hs_disclosures: Vec, 24 | key_binding_jwt_header: HashMap, 25 | key_binding_jwt_payload: HashMap, 26 | serialized_key_binding_jwt: String, 27 | sd_jwt_payload: Map, 28 | serialized_sd_jwt: String, 29 | sd_jwt_json: Option, 30 | } 31 | 32 | impl SDJWTHolder { 33 | /// Build an instance of holder to create one or more presentations based on SD JWT provided by issuer. 34 | /// 35 | /// # Arguments 36 | /// * `sd_jwt_with_disclosures` - SD JWT with disclosures in the format specified by `serialization_format` 37 | /// * `serialization_format` - Serialization format of the SD JWT, see [SDJWTSerializationFormat]. 38 | /// 39 | /// # Returns 40 | /// * `SDJWTHolder` - Instance of SDJWTHolder 41 | /// 42 | /// # Errors 43 | /// * `InvalidInput` - If the serialization format is not supported 44 | /// * `InvalidState` - If the SD JWT data is not valid 45 | /// * `DeserializationError` - If the SD JWT serialization is not valid 46 | pub fn new(sd_jwt_with_disclosures: String, serialization_format: SDJWTSerializationFormat) -> Result { 47 | let mut holder = SDJWTHolder { 48 | sd_jwt_engine: SDJWTCommon { 49 | serialization_format, 50 | ..Default::default() 51 | }, 52 | hs_disclosures: Vec::new(), 53 | key_binding_jwt_header: HashMap::new(), 54 | key_binding_jwt_payload: HashMap::new(), 55 | serialized_key_binding_jwt: "".to_string(), 56 | sd_jwt_payload: Map::new(), 57 | serialized_sd_jwt: "".to_string(), 58 | sd_jwt_json: None, 59 | }; 60 | 61 | holder 62 | .sd_jwt_engine 63 | .parse_sd_jwt(sd_jwt_with_disclosures.clone())?; 64 | 65 | //TODO Verify signature before accepting the JWT 66 | holder.sd_jwt_payload = holder 67 | .sd_jwt_engine 68 | .unverified_input_sd_jwt_payload 69 | .take() 70 | .ok_or(Error::InvalidState("Cannot take payload".to_string()))?; 71 | holder.serialized_sd_jwt = holder 72 | .sd_jwt_engine 73 | .unverified_sd_jwt 74 | .take() 75 | .ok_or(Error::InvalidState("Cannot take jwt".to_string()))?; 76 | holder.sd_jwt_json = holder.sd_jwt_engine.unverified_sd_jwt_json.clone(); 77 | 78 | holder.sd_jwt_engine.create_hash_mappings()?; 79 | 80 | Ok(holder) 81 | } 82 | 83 | /// Create a presentation based on the SD JWT provided by issuer. 84 | /// 85 | /// # Arguments 86 | /// * `claims_to_disclose` - Claims to disclose in the presentation 87 | /// * `nonce` - Nonce to be used in the key-binding JWT 88 | /// * `aud` - Audience to be used in the key-binding JWT 89 | /// * `holder_key` - Key to sign the key-binding JWT 90 | /// * `sign_alg` - Signing algorithm to be used in the key-binding JWT 91 | /// 92 | /// # Returns 93 | /// * `String` - Presentation in the format specified by `serialization_format` in the constructor. It can be either compact or json. 94 | pub fn create_presentation( 95 | &mut self, 96 | claims_to_disclose: Map, 97 | nonce: Option, 98 | aud: Option, 99 | holder_key: Option, 100 | sign_alg: Option, 101 | ) -> Result { 102 | self.key_binding_jwt_header = Default::default(); 103 | self.key_binding_jwt_payload = Default::default(); 104 | self.serialized_key_binding_jwt = Default::default(); 105 | self.hs_disclosures = self.select_disclosures(&self.sd_jwt_payload, claims_to_disclose)?; 106 | 107 | match (nonce, aud, holder_key) { 108 | (Some(nonce), Some(aud), Some(holder_key)) => { 109 | self.create_key_binding_jwt(nonce, aud, &holder_key, sign_alg)? 110 | } 111 | (None, None, None) => {} 112 | _ => { 113 | return Err(Error::InvalidInput( 114 | "Inconsistency in parameters to determine JWT KB by holder".to_string(), 115 | )); 116 | } 117 | } 118 | 119 | let sd_jwt_presentation = if self.sd_jwt_engine.serialization_format == SDJWTSerializationFormat::Compact { 120 | let mut combined: Vec<&str> = Vec::with_capacity(self.hs_disclosures.len() + 2); 121 | combined.push(&self.serialized_sd_jwt); 122 | combined.extend(self.hs_disclosures.iter().map(|s| s.as_str())); 123 | combined.push(&self.serialized_key_binding_jwt); 124 | let joined = combined.join(COMBINED_SERIALIZATION_FORMAT_SEPARATOR); 125 | joined.to_string() 126 | } else { 127 | let mut sd_jwt_json = self 128 | .sd_jwt_json 129 | .take() 130 | .ok_or(Error::InvalidState("Cannot take SDJWTJson".to_string()))?; 131 | sd_jwt_json.disclosures = self.hs_disclosures.clone(); 132 | if !self.serialized_key_binding_jwt.is_empty() { 133 | sd_jwt_json.kb_jwt = Some(self.serialized_key_binding_jwt.clone()); 134 | } 135 | serde_json::to_string(&sd_jwt_json) 136 | .map_err(|e| Error::DeserializationError(e.to_string()))? 137 | }; 138 | 139 | Ok(sd_jwt_presentation) 140 | } 141 | 142 | fn select_disclosures( 143 | &self, 144 | sd_jwt_claims: &Map, 145 | claims_to_disclose: Map, 146 | ) -> Result> { 147 | let mut hash_to_disclosure = Vec::new(); 148 | let default_list = Vec::new(); 149 | let sd_map: HashMap<&str, (&Value, &str)> = sd_jwt_claims 150 | .get(SD_DIGESTS_KEY) 151 | .and_then(Value::as_array) 152 | .unwrap_or(&default_list) 153 | .iter() 154 | .filter_map(|digest| { 155 | let digest = digest.as_str()?; 156 | let disclosure = self.sd_jwt_engine.hash_to_decoded_disclosure.get(digest)?; 157 | let key = disclosure[1].as_str()?; 158 | Some((key, (&disclosure[2], digest))) 159 | }) 160 | .collect(); //TODO split to 2 maps 161 | for (key_to_disclose, value_to_disclose) in claims_to_disclose { 162 | match value_to_disclose { 163 | Value::Bool(true) | Value::Number(_) | Value::String(_) => { 164 | /* disclose without children */ 165 | } 166 | Value::Array(claims_to_disclose) => { 167 | if let Some(sd_jwt_claims) = sd_jwt_claims 168 | .get(&key_to_disclose) 169 | .and_then(Value::as_array) 170 | { 171 | hash_to_disclosure.append( 172 | &mut self.select_disclosures_from_disclosed_list( 173 | sd_jwt_claims, 174 | &claims_to_disclose, 175 | )?, 176 | ) 177 | } else if let Some(sd_jwt_claims) = sd_map 178 | .get(key_to_disclose.as_str()) 179 | .and_then(|(sd, _)| sd.as_array()) 180 | { 181 | hash_to_disclosure.append( 182 | &mut self.select_disclosures_from_disclosed_list( 183 | sd_jwt_claims, 184 | &claims_to_disclose, 185 | )?, 186 | ) 187 | } 188 | } 189 | Value::Object(claims_to_disclose) if (!claims_to_disclose.is_empty()) => { 190 | let sd_jwt_claims = if let Some(next) = sd_jwt_claims 191 | .get(&key_to_disclose) 192 | .and_then(Value::as_object) 193 | { 194 | next 195 | } else { 196 | sd_map[key_to_disclose.as_str()] 197 | .0 198 | .as_object() 199 | .ok_or(Error::ConversionError("json object".to_string()))? 200 | }; 201 | hash_to_disclosure 202 | .append(&mut self.select_disclosures(sd_jwt_claims, claims_to_disclose)?); 203 | } 204 | Value::Object(_) => { /* disclose without children */ } 205 | Value::Bool(false) | Value::Null => { 206 | // skip unrevealed 207 | continue; 208 | } 209 | } 210 | if sd_jwt_claims.contains_key(&key_to_disclose) { 211 | continue; 212 | } else if let Some((_, digest)) = sd_map.get(key_to_disclose.as_str()) { 213 | hash_to_disclosure.push(self.sd_jwt_engine.hash_to_disclosure[*digest].to_owned()); 214 | } else { 215 | return Err(Error::InvalidState( 216 | "Requested claim doesn't exist".to_string(), 217 | )); 218 | } 219 | } 220 | 221 | Ok(hash_to_disclosure) 222 | } 223 | 224 | fn select_disclosures_from_disclosed_list( 225 | &self, 226 | sd_jwt_claims: &[Value], 227 | claims_to_disclose: &[Value], 228 | ) -> Result> { 229 | let mut hash_to_disclosure: Vec = Vec::new(); 230 | for (claim_to_disclose, sd_jwt_claims) in claims_to_disclose.iter().zip(sd_jwt_claims) { 231 | match (claim_to_disclose, sd_jwt_claims) { 232 | (Value::Bool(true), Value::Object(sd_jwt_claims)) => { 233 | if let Some(Value::String(digest)) = sd_jwt_claims.get(SD_LIST_PREFIX) { 234 | hash_to_disclosure 235 | .push(self.sd_jwt_engine.hash_to_disclosure[digest].to_owned()); 236 | } 237 | } 238 | (claim_to_disclose, Value::Object(sd_jwt_claims)) => { 239 | if let Some(Value::String(digest)) = sd_jwt_claims.get(SD_LIST_PREFIX) { 240 | let disclosure = self.sd_jwt_engine.hash_to_decoded_disclosure[digest] 241 | .as_array() 242 | .ok_or(Error::ConversionError("json array".to_string()))?; 243 | match (claim_to_disclose, disclosure.get(1)) { 244 | ( 245 | Value::Array(claim_to_disclose), 246 | Some(Value::Array(sd_jwt_claims)), 247 | ) => { 248 | hash_to_disclosure.push( 249 | self.sd_jwt_engine.hash_to_disclosure[digest].clone() 250 | ); 251 | hash_to_disclosure.append( 252 | &mut self.select_disclosures_from_disclosed_list( 253 | sd_jwt_claims, 254 | claim_to_disclose, 255 | )?, 256 | ); 257 | } 258 | ( 259 | Value::Object(claim_to_disclose), 260 | Some(Value::Object(sd_jwt_claims)), 261 | ) => { 262 | hash_to_disclosure 263 | .push(self.sd_jwt_engine.hash_to_disclosure[digest].to_owned()); 264 | hash_to_disclosure.append(&mut self.select_disclosures( 265 | sd_jwt_claims, 266 | claim_to_disclose.to_owned(), 267 | )?); 268 | } 269 | _ => {} 270 | } 271 | } else if let Some(claim_to_disclose) = claim_to_disclose.as_object() { 272 | hash_to_disclosure.append( 273 | &mut self 274 | .select_disclosures(sd_jwt_claims, claim_to_disclose.to_owned())?, 275 | ); 276 | } 277 | } 278 | (Value::Array(claim_to_disclose), Value::Array(sd_jwt_claims)) => { 279 | hash_to_disclosure.append(&mut self.select_disclosures_from_disclosed_list( 280 | sd_jwt_claims, 281 | claim_to_disclose, 282 | )?); 283 | } 284 | _ => {} 285 | } 286 | } 287 | 288 | Ok(hash_to_disclosure) 289 | } 290 | fn create_key_binding_jwt( 291 | &mut self, 292 | nonce: String, 293 | aud: String, 294 | holder_key: &EncodingKey, 295 | sign_alg: Option, 296 | ) -> Result<()> { 297 | let alg = sign_alg.unwrap_or_else(|| DEFAULT_SIGNING_ALG.to_string()); 298 | // Set key-binding fields 299 | self.key_binding_jwt_header 300 | .insert("alg".to_string(), alg.clone().into()); 301 | self.key_binding_jwt_header 302 | .insert("typ".to_string(), crate::KB_JWT_TYP_HEADER.into()); 303 | self.key_binding_jwt_payload 304 | .insert("nonce".to_string(), nonce.into()); 305 | self.key_binding_jwt_payload 306 | .insert("aud".to_string(), aud.into()); 307 | let timestamp = time::SystemTime::now() 308 | .duration_since(time::UNIX_EPOCH) 309 | .map_err(|e| Error::ConversionError(format!("timestamp: {}", e)))? 310 | .as_secs(); 311 | self.key_binding_jwt_payload 312 | .insert("iat".to_string(), timestamp.into()); 313 | self.set_key_binding_digest_key()?; 314 | // Create key-binding jwt 315 | let mut header = Header::new( 316 | Algorithm::from_str(&alg) 317 | .map_err(|e| Error::DeserializationError(e.to_string()))?, 318 | ); 319 | header.typ = Some(crate::KB_JWT_TYP_HEADER.into()); 320 | self.serialized_key_binding_jwt = 321 | jsonwebtoken::encode(&header, &self.key_binding_jwt_payload, holder_key) 322 | .map_err(|e| Error::DeserializationError(e.to_string()))?; 323 | Ok(()) 324 | } 325 | 326 | fn set_key_binding_digest_key(&mut self) -> Result<()> { 327 | let mut combined: Vec<&str> = Vec::with_capacity(self.hs_disclosures.len() + 1); 328 | combined.push(&self.serialized_sd_jwt); 329 | combined.extend(self.hs_disclosures.iter().map(|s| s.as_str())); 330 | let combined = combined 331 | .join(COMBINED_SERIALIZATION_FORMAT_SEPARATOR) 332 | .add(COMBINED_SERIALIZATION_FORMAT_SEPARATOR); 333 | 334 | let sd_hash = base64_hash(combined.as_bytes()); 335 | self.key_binding_jwt_payload 336 | .insert(KB_DIGEST_KEY.to_owned(), Value::String(sd_hash)); 337 | 338 | Ok(()) 339 | } 340 | } 341 | 342 | #[cfg(test)] 343 | mod tests { 344 | use crate::issuer::ClaimsForSelectiveDisclosureStrategy; 345 | use crate::{SDJWTHolder, SDJWTIssuer, COMBINED_SERIALIZATION_FORMAT_SEPARATOR, SDJWTSerializationFormat}; 346 | use jsonwebtoken::EncodingKey; 347 | use serde_json::{json, Map, Value}; 348 | use std::collections::HashSet; 349 | 350 | const PRIVATE_ISSUER_PEM: &str = "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgUr2bNKuBPOrAaxsR\nnbSH6hIhmNTxSGXshDSUD1a1y7ihRANCAARvbx3gzBkyPDz7TQIbjF+ef1IsxUwz\nX1KWpmlVv+421F7+c1sLqGk4HUuoVeN8iOoAcE547pJhUEJyf5Asc6pP\n-----END PRIVATE KEY-----\n"; 351 | 352 | #[test] 353 | fn create_full_presentation() { 354 | let user_claims = json!({ 355 | "sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c", 356 | "iss": "https://example.com/issuer", 357 | "iat": "1683000000", 358 | "exp": "1883000000", 359 | "address": { 360 | "street_address": "Schulstr. 12", 361 | "locality": "Schulpforta", 362 | "region": "Sachsen-Anhalt", 363 | "country": "DE" 364 | } 365 | }); 366 | let private_issuer_bytes = PRIVATE_ISSUER_PEM.as_bytes(); 367 | let issuer_key = EncodingKey::from_ec_pem(private_issuer_bytes).unwrap(); 368 | let sd_jwt = SDJWTIssuer::new(issuer_key, None).issue_sd_jwt( 369 | user_claims.clone(), 370 | ClaimsForSelectiveDisclosureStrategy::AllLevels, 371 | None, 372 | false, 373 | SDJWTSerializationFormat::Compact, 374 | ) 375 | .unwrap(); 376 | let presentation = SDJWTHolder::new( 377 | sd_jwt.clone(), 378 | SDJWTSerializationFormat::Compact, 379 | ) 380 | .unwrap() 381 | .create_presentation( 382 | user_claims.as_object().unwrap().clone(), 383 | None, 384 | None, 385 | None, 386 | None, 387 | ) 388 | .unwrap(); 389 | assert_eq!(sd_jwt, presentation); 390 | } 391 | #[test] 392 | fn create_presentation_empty_object_as_disclosure_value() { 393 | let mut user_claims = json!({ 394 | "sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c", 395 | "iss": "https://example.com/issuer", 396 | "iat": 1683000000, 397 | "exp": 1883000000, 398 | "address": { 399 | "street_address": "Schulstr. 12", 400 | "locality": "Schulpforta", 401 | "region": "Sachsen-Anhalt", 402 | "country": "DE" 403 | } 404 | }); 405 | let private_issuer_bytes = PRIVATE_ISSUER_PEM.as_bytes(); 406 | let issuer_key = EncodingKey::from_ec_pem(private_issuer_bytes).unwrap(); 407 | 408 | let sd_jwt = SDJWTIssuer::new(issuer_key, None).issue_sd_jwt( 409 | user_claims.clone(), 410 | ClaimsForSelectiveDisclosureStrategy::AllLevels, 411 | None, 412 | false, 413 | SDJWTSerializationFormat::Compact, 414 | ) 415 | .unwrap(); 416 | let issued = sd_jwt.clone(); 417 | user_claims["address"] = Value::Object(Map::new()); 418 | let presentation = 419 | SDJWTHolder::new(sd_jwt, SDJWTSerializationFormat::Compact) 420 | .unwrap() 421 | .create_presentation( 422 | user_claims.as_object().unwrap().clone(), 423 | None, 424 | None, 425 | None, 426 | None, 427 | ) 428 | .unwrap(); 429 | 430 | let mut parts: Vec<&str> = issued 431 | .split(COMBINED_SERIALIZATION_FORMAT_SEPARATOR) 432 | .collect(); 433 | parts.remove(5); 434 | parts.remove(4); 435 | parts.remove(3); 436 | parts.remove(2); 437 | let expected = parts.join(COMBINED_SERIALIZATION_FORMAT_SEPARATOR); 438 | assert_eq!(expected, presentation); 439 | } 440 | 441 | #[test] 442 | fn create_presentation_for_arrayed_disclosures() { 443 | let mut user_claims = json!( 444 | { 445 | "sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c", 446 | "name": "Bois", 447 | "addresses": [ 448 | { 449 | "street_address": "Schulstr. 12", 450 | "locality": "Schulpforta", 451 | "region": "Sachsen-Anhalt", 452 | "country": "DE" 453 | }, 454 | { 455 | "street_address": "456 Main St", 456 | "locality": "Anytown", 457 | "region": "NY", 458 | "country": "US" 459 | } 460 | ], 461 | "nationalities": [ 462 | "US", 463 | "CA" 464 | ] 465 | } 466 | ); 467 | let strategy = ClaimsForSelectiveDisclosureStrategy::Custom(vec![ 468 | "$.name", 469 | "$.addresses[1]", 470 | "$.addresses[1].country", 471 | "$.nationalities[0]", 472 | ]); 473 | 474 | let private_issuer_bytes = PRIVATE_ISSUER_PEM.as_bytes(); 475 | let issuer_key = EncodingKey::from_ec_pem(private_issuer_bytes).unwrap(); 476 | let sd_jwt = SDJWTIssuer::new(issuer_key, None).issue_sd_jwt( 477 | user_claims.clone(), 478 | strategy, 479 | None, 480 | false, 481 | SDJWTSerializationFormat::Compact, 482 | ) 483 | .unwrap(); 484 | // Choose what to reveal 485 | user_claims["addresses"] = Value::Array(vec![Value::Bool(true), Value::Bool(false)]); 486 | user_claims["nationalities"] = Value::Array(vec![Value::Bool(true), Value::Bool(true)]); 487 | 488 | let issued = sd_jwt.clone(); 489 | println!("{}", issued); 490 | let presentation = 491 | SDJWTHolder::new(sd_jwt, SDJWTSerializationFormat::Compact) 492 | .unwrap() 493 | .create_presentation( 494 | user_claims.as_object().unwrap().clone(), 495 | None, 496 | None, 497 | None, 498 | None, 499 | ) 500 | .unwrap(); 501 | println!("{}", presentation); 502 | let mut issued_parts: HashSet<&str> = issued 503 | .split(COMBINED_SERIALIZATION_FORMAT_SEPARATOR) 504 | .collect(); 505 | issued_parts.remove(""); 506 | 507 | let mut revealed_parts: HashSet<&str> = presentation 508 | .split(COMBINED_SERIALIZATION_FORMAT_SEPARATOR) 509 | .collect(); 510 | revealed_parts.remove(""); 511 | 512 | let union: HashSet<_> = issued_parts.intersection(&revealed_parts).collect(); 513 | assert_eq!(union.len(), 3); 514 | } 515 | 516 | #[test] 517 | fn create_presentation_for_recursive_disclosures() { 518 | // Input data used to create the SD-JWT and presentation fixtures, 519 | // can be used to debug in case the test fails: 520 | 521 | // let mut user_claims = json!( 522 | // { 523 | // "foo": ["one", "two"], 524 | // "bar": { 525 | // "red": 1, 526 | // "green": 2 527 | // }, 528 | // "qux": [ 529 | // ["blue", "yellow"] 530 | // ], 531 | // "baz": [ 532 | // ["orange", "purple"], 533 | // ["black", "white"] 534 | // ], 535 | // "animals": { 536 | // "snake": { 537 | // "name": "python", 538 | // "age": 10 539 | // }, 540 | // "bird": { 541 | // "name": "eagle", 542 | // "age": 20 543 | // } 544 | // } 545 | // } 546 | // ); 547 | // let strategy = ClaimsForSelectiveDisclosureStrategy::Custom(vec![ 548 | // "$.foo[0]", 549 | // "$.foo[1]", 550 | // "$.bar.red", 551 | // "$.bar.green", 552 | // "$.qux[0]", 553 | // "$.qux[0][0]", 554 | // "$.qux[0][1]", 555 | // "$.baz[0]", 556 | // "$.baz[0][0]", 557 | // "$.baz[0][1]", 558 | // "$.baz[1]", 559 | // "$.baz[1][0]", 560 | // "$.baz[1][1]", 561 | // "$.animals.snake", 562 | // "$.animals.snake.name", 563 | // "$.animals.snake.age", 564 | // "$.animals.bird", 565 | // "$.animals.bird.name", 566 | // "$.animals.bird.age", 567 | // ]); 568 | 569 | // let private_issuer_bytes = PRIVATE_ISSUER_PEM.as_bytes(); 570 | // let issuer_key = EncodingKey::from_ec_pem(private_issuer_bytes).unwrap(); 571 | // let sd_jwt = SDJWTIssuer::new(issuer_key, None).issue_sd_jwt( 572 | // user_claims.clone(), 573 | // strategy, 574 | // None, 575 | // false, 576 | // SDJWTSerializationFormat::Compact, 577 | // ) 578 | // .unwrap(); 579 | 580 | let sd_jwt = String::from("eyJhbGciOiJFUzI1NiJ9.eyJmb28iOlt7Ii4uLiI6Ii1XMWROTk0tNUI3WlpxR3R4MkF6RTA3X0hpRUpOZVJtNGtEQ1VORTVDNFUifSx7Ii4uLiI6ImpuUURqUEFoclY1bjMtRW5PVEZHWTcwMkd0T3FhN3hua3pVM0E4aElSX3cifV0sImJhciI6eyJfc2QiOlsiX25yZUxad2xVYlp1SmtqS1RVdHR5YkhqUTNrY2J4cnZab1dxUmVBbG4tcyIsImhGcjdBRElQbjZvQ3lSckNBN0VtNldLaGk1UjdXMWJjYWFZUFFrelpGMXciXX0sInF1eCI6W3siLi4uIjoieHl6MkRSSDRTSkpjdFFtMDEtSzROVVllMTMzMWh6U3VkTXd3MENDODEyUSJ9XSwiYmF6IjpbeyIuLi4iOiJRMGcyVmYzNnl6TnNvUkdNb0dsODZnZ2QyWGFVTmg5bGN6STFfbmFZYUhnIn0seyIuLi4iOiJZcGpMNTJKd1BfYmFFS21OaHFLazE3TWFrMl9fSWJCNmctY0haSHd6dmwwIn1dLCJhbmltYWxzIjp7Il9zZCI6WyJyQ19LNzlObG95SkFPWXRCOW9ITFlsTVJSS1V4UTNnaTZ0Wld0Zm90TWRjIiwidjUyd3d6bzB5Ymw2U2V1MjZWYklUODh5bHk1LXVMZkdlYTdkWnMxSHBwMCJdfSwiX3NkX2FsZyI6InNoYS0yNTYifQ.piidRp0pHJYmtExCJnLExaaWMTBX50mLwM6gFVYnD72DszyjpKbAoZhyAXT-I4CqqSpiHZg-2w8s26XBraqX6w~WyJCQ2k5UXlsWVVqVEpXWWVfbzRzOWxnIiwgIm9uZSJd~WyJSLXZ0bDBmbWF6N01zR1ZWRFh3T3BnIiwgInR3byJd~WyJXNWlOQ1Z1Qlo4OW9aV2dIUkxzRWJBIiwgInJlZCIsIDFd~WyJTQW5hNUJnaHJxUXJ0amR5SGxiejJBIiwgImdyZWVuIiwgMl0~WyJENVJrNVlIUkdJVXM5enp6OFUtOTVnIiwgImJsdWUiXQ~WyI0Y2tnSjJuWVhhV21jM3pVQ253d3N3IiwgInllbGxvdyJd~WyJ2Ml8tRG5JN0lEZ1loYVMzTG9Kb013IiwgW3siLi4uIjogIkhqMUQtZE1SNXR0YmpLcl9DUENETzRuVGlkTWR1YVNpMnlnYlhtcmR4MGcifSwgeyIuLi4iOiAiUl9Sb253SFY3bzR6Y0o3TV9jcTlobVpLZ2o2RkMtdmNXTko4bzNkeTg2MCJ9XV0~WyJwRHQtcEtfaklUYXhCVENJRFNvUnhBIiwgIm9yYW5nZSJd~WyJ1b3FDS0lpZGJzQmxhczhUaU5Kakh3IiwgInB1cnBsZSJd~WyJJWWFXMzVPNzBoUWg4OGlqWVBUVXZRIiwgW3siLi4uIjogIjlWdnFSbjk1ZUN6QnVkNkhYOG1faVRNMERZSVVxN0ZheFFtanowV1llbUkifSwgeyIuLi4iOiAiTmFOeXozWEJRZVc4Z1JRd28ySlN5NmhtbnJZT1JxRjMxeUhfWkhqbkRxNCJ9XV0~WyI4cFNkNHl0TWlPdnVGaFhQSXBPbW5BIiwgImJsYWNrIl0~WyJxLWR0QXhtZzY5cWZLMFpvS1BSbWFnIiwgIndoaXRlIl0~WyIzTzY0WmVYSjF6XzJWMXdrMGhJdUdBIiwgW3siLi4uIjogImRmVnVjbkwwMC1FVFh0RGpHaDlpRHYtSE5PZmRyZ1VuTlNYRk01VUlIRVkifSwgeyIuLi4iOiAiUlRnVmxQb25RTVZJNkEzNUJic21KTThDeDVTVTN1ZXJBMENyYmpvRW02USJ9XV0~WyJNQTlSbGMwUlAxNnVJWER6blRqOWJ3IiwgIm5hbWUiLCAicHl0aG9uIl0~WyJrblRLb0lKVzZuQ1VzeW1sN3lKWTNBIiwgImFnZSIsIDEwXQ~WyJlUEdwazZjdEhOSS1HS2JKbjZrR3lBIiwgInNuYWtlIiwgeyJfc2QiOiBbIjREU0s5REpJVEhROElITFFESld6SV9yM0lheXBIek5Ma19tc3BUa2xDVzQiLCAiYy11UFhEQkZJX2FDV1BUUHlYNFV0OWdDWW1DQ1FqUEw5TnRFZGotdWZtMCJdfV0~WyJOMzgyX2xTU1dpSzZsbGdPNFFhbUdnIiwgIm5hbWUiLCAiZWFnbGUiXQ~WyJjVWZVRVBrX0pDZm1KQzhWQUp1V1pBIiwgImFnZSIsIDIwXQ~WyJSVDh5My1Odmh6QXo4Q2ctS1NDRGh3IiwgImJpcmQiLCB7Il9zZCI6IFsiUVhINU9mSF8tMGtFYkEwWDBnd0RLenphc05ZYWRWekNWRGFrYlZfWnNxNCIsICJoUmtPNjRIVXZuaEFPbDBRS1NlZDFUWUhtb0VpRW9zb0R0WmsyRVl4ejdNIl19XQ~"); 581 | let expected_presentation = String::from("eyJhbGciOiJFUzI1NiJ9.eyJmb28iOlt7Ii4uLiI6Ii1XMWROTk0tNUI3WlpxR3R4MkF6RTA3X0hpRUpOZVJtNGtEQ1VORTVDNFUifSx7Ii4uLiI6ImpuUURqUEFoclY1bjMtRW5PVEZHWTcwMkd0T3FhN3hua3pVM0E4aElSX3cifV0sImJhciI6eyJfc2QiOlsiX25yZUxad2xVYlp1SmtqS1RVdHR5YkhqUTNrY2J4cnZab1dxUmVBbG4tcyIsImhGcjdBRElQbjZvQ3lSckNBN0VtNldLaGk1UjdXMWJjYWFZUFFrelpGMXciXX0sInF1eCI6W3siLi4uIjoieHl6MkRSSDRTSkpjdFFtMDEtSzROVVllMTMzMWh6U3VkTXd3MENDODEyUSJ9XSwiYmF6IjpbeyIuLi4iOiJRMGcyVmYzNnl6TnNvUkdNb0dsODZnZ2QyWGFVTmg5bGN6STFfbmFZYUhnIn0seyIuLi4iOiJZcGpMNTJKd1BfYmFFS21OaHFLazE3TWFrMl9fSWJCNmctY0haSHd6dmwwIn1dLCJhbmltYWxzIjp7Il9zZCI6WyJyQ19LNzlObG95SkFPWXRCOW9ITFlsTVJSS1V4UTNnaTZ0Wld0Zm90TWRjIiwidjUyd3d6bzB5Ymw2U2V1MjZWYklUODh5bHk1LXVMZkdlYTdkWnMxSHBwMCJdfSwiX3NkX2FsZyI6InNoYS0yNTYifQ.piidRp0pHJYmtExCJnLExaaWMTBX50mLwM6gFVYnD72DszyjpKbAoZhyAXT-I4CqqSpiHZg-2w8s26XBraqX6w~WyJSLXZ0bDBmbWF6N01zR1ZWRFh3T3BnIiwgInR3byJd~WyJTQW5hNUJnaHJxUXJ0amR5SGxiejJBIiwgImdyZWVuIiwgMl0~WyI0Y2tnSjJuWVhhV21jM3pVQ253d3N3IiwgInllbGxvdyJd~WyJ2Ml8tRG5JN0lEZ1loYVMzTG9Kb013IiwgW3siLi4uIjogIkhqMUQtZE1SNXR0YmpLcl9DUENETzRuVGlkTWR1YVNpMnlnYlhtcmR4MGcifSwgeyIuLi4iOiAiUl9Sb253SFY3bzR6Y0o3TV9jcTlobVpLZ2o2RkMtdmNXTko4bzNkeTg2MCJ9XV0~WyJ1b3FDS0lpZGJzQmxhczhUaU5Kakh3IiwgInB1cnBsZSJd~WyJJWWFXMzVPNzBoUWg4OGlqWVBUVXZRIiwgW3siLi4uIjogIjlWdnFSbjk1ZUN6QnVkNkhYOG1faVRNMERZSVVxN0ZheFFtanowV1llbUkifSwgeyIuLi4iOiAiTmFOeXozWEJRZVc4Z1JRd28ySlN5NmhtbnJZT1JxRjMxeUhfWkhqbkRxNCJ9XV0~WyI4cFNkNHl0TWlPdnVGaFhQSXBPbW5BIiwgImJsYWNrIl0~WyJxLWR0QXhtZzY5cWZLMFpvS1BSbWFnIiwgIndoaXRlIl0~WyIzTzY0WmVYSjF6XzJWMXdrMGhJdUdBIiwgW3siLi4uIjogImRmVnVjbkwwMC1FVFh0RGpHaDlpRHYtSE5PZmRyZ1VuTlNYRk01VUlIRVkifSwgeyIuLi4iOiAiUlRnVmxQb25RTVZJNkEzNUJic21KTThDeDVTVTN1ZXJBMENyYmpvRW02USJ9XV0~WyJrblRLb0lKVzZuQ1VzeW1sN3lKWTNBIiwgImFnZSIsIDEwXQ~WyJlUEdwazZjdEhOSS1HS2JKbjZrR3lBIiwgInNuYWtlIiwgeyJfc2QiOiBbIjREU0s5REpJVEhROElITFFESld6SV9yM0lheXBIek5Ma19tc3BUa2xDVzQiLCAiYy11UFhEQkZJX2FDV1BUUHlYNFV0OWdDWW1DQ1FqUEw5TnRFZGotdWZtMCJdfV0~WyJjVWZVRVBrX0pDZm1KQzhWQUp1V1pBIiwgImFnZSIsIDIwXQ~WyJSVDh5My1Odmh6QXo4Q2ctS1NDRGh3IiwgImJpcmQiLCB7Il9zZCI6IFsiUVhINU9mSF8tMGtFYkEwWDBnd0RLenphc05ZYWRWekNWRGFrYlZfWnNxNCIsICJoUmtPNjRIVXZuaEFPbDBRS1NlZDFUWUhtb0VpRW9zb0R0WmsyRVl4ejdNIl19XQ~"); 582 | 583 | // Choose what to reveal 584 | let revealed = json!( 585 | { 586 | "foo": [false, true], 587 | "bar": { 588 | "red": false, 589 | "green": true 590 | }, 591 | "qux": [ 592 | [false, true] 593 | ], 594 | "baz": [ 595 | [false, true], 596 | [true, true] 597 | ], 598 | "animals": { 599 | "snake": { 600 | "name": false, 601 | "age": true 602 | }, 603 | "bird": { 604 | "name": false, 605 | "age": true 606 | } 607 | } 608 | } 609 | ); 610 | 611 | let presentation = 612 | SDJWTHolder::new(sd_jwt, SDJWTSerializationFormat::Compact) 613 | .unwrap() 614 | .create_presentation( 615 | revealed.as_object().unwrap().clone(), 616 | None, 617 | None, 618 | None, 619 | None, 620 | ) 621 | .unwrap(); 622 | 623 | let presentation: HashSet<_> = presentation 624 | .split(COMBINED_SERIALIZATION_FORMAT_SEPARATOR).map(String::from) 625 | .collect(); 626 | 627 | let expected: HashSet<_> = expected_presentation 628 | .split(COMBINED_SERIALIZATION_FORMAT_SEPARATOR) 629 | .map(String::from).collect(); 630 | 631 | assert_eq!(presentation, expected); 632 | } 633 | } 634 | -------------------------------------------------------------------------------- /src/issuer.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 DSR Corporation, Denver, Colorado. 2 | // https://www.dsr-corporation.com 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | use crate::{error, SDJWTJson}; 6 | use error::Result; 7 | use std::collections::{HashMap, VecDeque}; 8 | use std::str::FromStr; 9 | use std::vec::Vec; 10 | 11 | use jsonwebtoken::jwk::Jwk; 12 | use jsonwebtoken::{Algorithm, EncodingKey, Header}; 13 | use rand::Rng; 14 | use serde_json::Value; 15 | use serde_json::{json, Map as SJMap, Map}; 16 | 17 | use crate::disclosure::SDJWTDisclosure; 18 | use crate::error::Error; 19 | use crate::utils::{base64_hash, generate_salt}; 20 | use crate::{ 21 | SDJWTCommon, CNF_KEY, COMBINED_SERIALIZATION_FORMAT_SEPARATOR, DEFAULT_DIGEST_ALG, 22 | DEFAULT_SIGNING_ALG, DIGEST_ALG_KEY, JWK_KEY, SD_DIGESTS_KEY, SD_LIST_PREFIX, 23 | SDJWTSerializationFormat, 24 | }; 25 | 26 | pub struct SDJWTIssuer { 27 | // parameters 28 | sign_alg: String, 29 | add_decoy_claims: bool, 30 | extra_header_parameters: Option>, 31 | 32 | // input data 33 | issuer_key: EncodingKey, 34 | holder_key: Option, 35 | 36 | // internal fields 37 | inner: SDJWTCommon, 38 | all_disclosures: Vec, 39 | sd_jwt_payload: SJMap, 40 | signed_sd_jwt: String, 41 | serialized_sd_jwt: String, 42 | } 43 | 44 | /// ClaimsForSelectiveDisclosureStrategy is used to determine which claims can be selectively disclosed later by the holder. 45 | #[derive(PartialEq, Debug)] 46 | pub enum ClaimsForSelectiveDisclosureStrategy<'a> { 47 | /// No claims can be selectively disclosed, so all claims are always disclosed in presentations generated by the holder. 48 | NoSDClaims, 49 | /// Top-level claims can be selectively disclosed, nested objects are fully disclosed, if a parent claim is disclosed. 50 | TopLevel, 51 | /// All claims can be selectively disclosed (recursively including nested objects). 52 | AllLevels, 53 | /// Claims can be selectively disclosed based on the provided JSONPaths. 54 | /// Other claims are always disclosed in presentation generated by the holder. 55 | /// # Examples 56 | /// ``` 57 | /// use sd_jwt_rs::issuer::ClaimsForSelectiveDisclosureStrategy; 58 | /// 59 | /// let strategy = ClaimsForSelectiveDisclosureStrategy::Custom(vec!["$.address", "$.address.street_address"]); 60 | /// ``` 61 | Custom(Vec<&'a str>), 62 | } 63 | 64 | impl<'a> ClaimsForSelectiveDisclosureStrategy<'a> { 65 | fn finalize_input(&mut self) -> Result<()> { 66 | match self { 67 | ClaimsForSelectiveDisclosureStrategy::Custom(keys) => { 68 | for key in keys.iter_mut() { 69 | if let Some(new_key) = key.strip_prefix("$.") { 70 | *key = new_key; 71 | } else { 72 | return Err(Error::InvalidPath("Invalid JSONPath".to_owned())); 73 | } 74 | } 75 | Ok(()) 76 | } 77 | _ => Ok(()), 78 | } 79 | } 80 | 81 | fn next_level(&self, key: &str) -> Self { 82 | match self { 83 | Self::NoSDClaims => Self::NoSDClaims, 84 | Self::TopLevel => Self::NoSDClaims, 85 | Self::AllLevels => Self::AllLevels, 86 | Self::Custom(sd_keys) => { 87 | let next_sd_keys = sd_keys 88 | .iter() 89 | .filter_map(|str| { 90 | str.strip_prefix(key).and_then(|str| 91 | match str.chars().next() { 92 | Some('.') => Some(&str[1..]), // next token 93 | Some('[') => Some(str), // array index 94 | _ => None, 95 | } 96 | ) 97 | }) 98 | .collect(); 99 | Self::Custom(next_sd_keys) 100 | } 101 | } 102 | } 103 | 104 | fn sd_for_key(&self, key: &str) -> bool { 105 | match self { 106 | Self::NoSDClaims => false, 107 | Self::TopLevel => true, 108 | Self::AllLevels => true, 109 | Self::Custom(sd_keys) => sd_keys.contains(&key), 110 | } 111 | } 112 | } 113 | 114 | impl SDJWTIssuer { 115 | const DECOY_MIN_ELEMENTS: u32 = 2; 116 | const DECOY_MAX_ELEMENTS: u32 = 5; 117 | 118 | /// Creates a new SDJWTIssuer instance. 119 | /// 120 | /// The instance can be used mutliple times to issue SD-JWTs. 121 | /// 122 | /// # Arguments 123 | /// * `issuer_key` - The key used to sign the SD-JWT. 124 | /// * `sign_alg` - The signing algorithm used to sign the SD-JWT. If not provided, the default algorithm is used. 125 | /// 126 | /// # Returns 127 | /// A new SDJWTIssuer instance. 128 | pub fn new(issuer_key: EncodingKey, sign_alg: Option) -> Self { 129 | SDJWTIssuer { 130 | sign_alg: sign_alg.unwrap_or(DEFAULT_SIGNING_ALG.to_owned()), 131 | add_decoy_claims: false, 132 | extra_header_parameters: None, 133 | issuer_key, 134 | holder_key: None, 135 | inner: Default::default(), 136 | all_disclosures: vec![], 137 | sd_jwt_payload: Default::default(), 138 | signed_sd_jwt: "".to_string(), 139 | serialized_sd_jwt: "".to_string(), 140 | } 141 | } 142 | 143 | fn reset(&mut self) { 144 | self.extra_header_parameters = Default::default(); 145 | self.all_disclosures = Default::default(); 146 | self.sd_jwt_payload = Default::default(); 147 | self.signed_sd_jwt = Default::default(); 148 | self.serialized_sd_jwt = Default::default(); 149 | } 150 | 151 | /// Issues a SD-JWT. 152 | /// 153 | /// # Arguments 154 | /// * `user_claims` - The claims to be included in the SD-JWT. 155 | /// * `sd_strategy` - The strategy to be used to determine which claims to be selectively disclosed. See [ClaimsForSelectiveDisclosureStrategy] for more details. 156 | /// * `holder_key` - The key used to sign the SD-JWT. If not provided, no key binding is added to the SD-JWT. 157 | /// * `add_decoy_claims` - If true, decoy claims are added to the SD-JWT. 158 | /// * `serialization_format` - The serialization format to be used for the SD-JWT, see [SDJWTSerializationFormat]. 159 | /// 160 | /// # Returns 161 | /// The issued SD-JWT as a string in the requested serialization format. 162 | pub fn issue_sd_jwt( 163 | &mut self, 164 | user_claims: Value, 165 | mut sd_strategy: ClaimsForSelectiveDisclosureStrategy, 166 | holder_key: Option, 167 | add_decoy_claims: bool, 168 | serialization_format: SDJWTSerializationFormat, 169 | // extra_header_parameters: Option>, 170 | ) -> Result { 171 | let inner = SDJWTCommon { 172 | serialization_format, 173 | ..Default::default() 174 | }; 175 | 176 | sd_strategy.finalize_input()?; 177 | 178 | SDJWTCommon::check_for_sd_claim(&user_claims)?; 179 | 180 | self.reset(); 181 | self.inner = inner; 182 | self.holder_key = holder_key; 183 | self.add_decoy_claims = add_decoy_claims; 184 | 185 | self.assemble_sd_jwt_payload(user_claims, sd_strategy)?; 186 | self.create_signed_jws()?; 187 | self.create_combined()?; 188 | 189 | Ok(self.serialized_sd_jwt.clone()) 190 | } 191 | 192 | fn assemble_sd_jwt_payload( 193 | &mut self, 194 | mut user_claims: Value, 195 | sd_strategy: ClaimsForSelectiveDisclosureStrategy, 196 | ) -> Result<()> { 197 | let claims_obj_ref = user_claims 198 | .as_object_mut() 199 | .ok_or(Error::ConversionError("json object".to_string()))?; 200 | let always_revealed_root_keys = vec!["iss", "iat", "exp"]; 201 | let mut always_revealed_claims: Map = always_revealed_root_keys 202 | .into_iter() 203 | .filter_map(|key| claims_obj_ref.shift_remove_entry(key)) 204 | .collect(); 205 | 206 | self.sd_jwt_payload = self 207 | .create_sd_claims(&user_claims, sd_strategy) 208 | .as_object() 209 | .ok_or(Error::ConversionError("json object".to_string()))? 210 | .clone(); 211 | 212 | self.sd_jwt_payload.insert( 213 | DIGEST_ALG_KEY.to_owned(), 214 | Value::String(DEFAULT_DIGEST_ALG.to_owned()), 215 | ); //TODO 216 | self.sd_jwt_payload.append(&mut always_revealed_claims); 217 | 218 | if let Some(holder_key) = &self.holder_key { 219 | self.sd_jwt_payload 220 | .entry(CNF_KEY) 221 | .or_insert_with(|| json!({JWK_KEY: holder_key})); 222 | } 223 | 224 | Ok(()) 225 | } 226 | 227 | fn create_sd_claims(&mut self, user_claims: &Value, sd_strategy: ClaimsForSelectiveDisclosureStrategy) -> Value { 228 | match user_claims { 229 | Value::Array(list) => self.create_sd_claims_list(list, sd_strategy), 230 | Value::Object(object) => self.create_sd_claims_object(object, sd_strategy), 231 | _ => user_claims.to_owned(), 232 | } 233 | } 234 | 235 | fn create_sd_claims_list(&mut self, list: &[Value], sd_strategy: ClaimsForSelectiveDisclosureStrategy) -> Value { 236 | let mut claims = Vec::new(); 237 | for (idx, object) in list.iter().enumerate() { 238 | let key = format!("[{idx}]"); 239 | let strategy_for_child = sd_strategy.next_level(&key); 240 | let subtree = self.create_sd_claims(object, strategy_for_child); 241 | 242 | if sd_strategy.sd_for_key(&key) { 243 | let disclosure = SDJWTDisclosure::new(None, subtree); 244 | claims.push(json!({ SD_LIST_PREFIX: disclosure.hash})); 245 | self.all_disclosures.push(disclosure); 246 | } else { 247 | claims.push(subtree); 248 | } 249 | } 250 | Value::Array(claims) 251 | } 252 | 253 | fn create_sd_claims_object( 254 | &mut self, 255 | user_claims: &SJMap, 256 | sd_strategy: ClaimsForSelectiveDisclosureStrategy, 257 | ) -> Value { 258 | let mut claims = SJMap::new(); 259 | 260 | // to have the first key "_sd" in the ordered map 261 | claims.insert(SD_DIGESTS_KEY.to_owned(), Value::Null); 262 | 263 | let mut sd_claims = Vec::new(); 264 | 265 | for (key, value) in user_claims.iter() { 266 | let strategy_for_child = sd_strategy.next_level(key); 267 | let subtree_from_here = self.create_sd_claims(value, strategy_for_child); 268 | 269 | if sd_strategy.sd_for_key(key) { 270 | let disclosure = SDJWTDisclosure::new(Some(key.to_owned()), subtree_from_here); 271 | sd_claims.push(disclosure.hash.clone()); 272 | self.all_disclosures.push(disclosure); 273 | } else { 274 | claims.insert(key.to_owned(), subtree_from_here); 275 | } 276 | } 277 | 278 | if self.add_decoy_claims { 279 | let num_decoy_elements = 280 | rand::thread_rng().gen_range(Self::DECOY_MIN_ELEMENTS..Self::DECOY_MAX_ELEMENTS); 281 | for _ in 0..num_decoy_elements { 282 | sd_claims.push(self.create_decoy_claim_entry()); 283 | } 284 | } 285 | 286 | if !sd_claims.is_empty() { 287 | sd_claims.sort(); 288 | claims.insert( 289 | SD_DIGESTS_KEY.to_owned(), 290 | Value::Array(sd_claims.into_iter().map(Value::String).collect()), 291 | ); 292 | } else { 293 | claims.shift_remove(SD_DIGESTS_KEY); 294 | } 295 | 296 | Value::Object(claims) 297 | } 298 | 299 | fn create_signed_jws(&mut self) -> Result<()> { 300 | if let Some(extra_headers) = &self.extra_header_parameters { 301 | let mut _protected_headers = extra_headers.clone(); 302 | for (key, value) in extra_headers.iter() { 303 | _protected_headers.insert(key.to_string(), value.to_string()); 304 | } 305 | unimplemented!("extra_headers are not supported for issuance"); 306 | } 307 | 308 | let mut header = Header::new( 309 | Algorithm::from_str(&self.sign_alg) 310 | .map_err(|e| Error::DeserializationError(e.to_string()))?, 311 | ); 312 | header.typ = self.inner.typ.clone(); 313 | self.signed_sd_jwt = jsonwebtoken::encode(&header, &self.sd_jwt_payload, &self.issuer_key) 314 | .map_err(|e| Error::DeserializationError(e.to_string()))?; 315 | 316 | Ok(()) 317 | } 318 | 319 | fn create_combined(&mut self) -> Result<()> { 320 | match self.inner.serialization_format { 321 | SDJWTSerializationFormat::Compact => { 322 | let mut disclosures: VecDeque = self 323 | .all_disclosures 324 | .iter() 325 | .map(|d| d.raw_b64.to_string()) 326 | .collect(); 327 | disclosures.push_front(self.signed_sd_jwt.clone()); 328 | 329 | let disclosures: Vec<&str> = disclosures.iter().map(|s| s.as_str()).collect(); 330 | 331 | self.serialized_sd_jwt = format!( 332 | "{}{}", 333 | disclosures.join(COMBINED_SERIALIZATION_FORMAT_SEPARATOR), 334 | COMBINED_SERIALIZATION_FORMAT_SEPARATOR, 335 | ); 336 | }, 337 | SDJWTSerializationFormat::JSON => { 338 | let jwt: Vec<&str> = self.signed_sd_jwt.split('.').collect(); 339 | if jwt.len() != 3 { 340 | return Err(Error::InvalidInput(format!( 341 | "Invalid JWT, JWT must contain three parts after splitting with \".\": jwt {}", 342 | self.signed_sd_jwt 343 | ))); 344 | } 345 | let sd_jwt_json = SDJWTJson { 346 | protected: jwt[0].to_owned(), 347 | payload: jwt[1].to_owned(), 348 | signature: jwt[2].to_owned(), 349 | kb_jwt: None, 350 | disclosures: self 351 | .all_disclosures 352 | .iter() 353 | .map(|d| d.raw_b64.to_string()) 354 | .collect(), 355 | }; 356 | self.serialized_sd_jwt = serde_json::to_string(&sd_jwt_json) 357 | .map_err(|e| Error::DeserializationError(e.to_string()))?; 358 | } 359 | } 360 | 361 | Ok(()) 362 | } 363 | 364 | fn create_decoy_claim_entry(&mut self) -> String { 365 | let digest = base64_hash(generate_salt().as_bytes()).to_string(); 366 | digest 367 | } 368 | } 369 | 370 | #[cfg(test)] 371 | mod tests { 372 | use jsonwebtoken::EncodingKey; 373 | use log::trace; 374 | use serde_json::json; 375 | 376 | use crate::issuer::ClaimsForSelectiveDisclosureStrategy; 377 | use crate::{SDJWTIssuer, SDJWTSerializationFormat}; 378 | 379 | const PRIVATE_ISSUER_PEM: &str = "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgUr2bNKuBPOrAaxsR\nnbSH6hIhmNTxSGXshDSUD1a1y7ihRANCAARvbx3gzBkyPDz7TQIbjF+ef1IsxUwz\nX1KWpmlVv+421F7+c1sLqGk4HUuoVeN8iOoAcE547pJhUEJyf5Asc6pP\n-----END PRIVATE KEY-----\n"; 380 | 381 | #[test] 382 | fn test_assembly_sd_full_recursive() { 383 | let user_claims = json!({ 384 | "sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c", 385 | "iss": "https://example.com/issuer", 386 | "iat": 1683000000, 387 | "exp": 1883000000, 388 | "address": { 389 | "street_address": "Schulstr. 12", 390 | "locality": "Schulpforta", 391 | "region": "Sachsen-Anhalt", 392 | "country": "DE" 393 | } 394 | }); 395 | let private_issuer_bytes = PRIVATE_ISSUER_PEM.as_bytes(); 396 | let issuer_key = EncodingKey::from_ec_pem(private_issuer_bytes).unwrap(); 397 | let sd_jwt = SDJWTIssuer::new(issuer_key, None).issue_sd_jwt( 398 | user_claims, 399 | ClaimsForSelectiveDisclosureStrategy::AllLevels, 400 | None, 401 | false, 402 | SDJWTSerializationFormat::Compact, 403 | ) 404 | .unwrap(); 405 | trace!("{:?}", sd_jwt) 406 | } 407 | 408 | #[test] 409 | fn test_next_level_array() { 410 | let strategy = ClaimsForSelectiveDisclosureStrategy::Custom(vec![ 411 | "name", 412 | "addresses[1]", 413 | "addresses[1].country", 414 | "nationalities[0]", 415 | ]); 416 | 417 | let next_strategy = strategy.next_level("addresses"); 418 | assert_eq!(&next_strategy, &ClaimsForSelectiveDisclosureStrategy::Custom(vec!["[1]", "[1].country"])); 419 | let next_strategy = next_strategy.next_level("[1]"); 420 | assert_eq!(&next_strategy, &ClaimsForSelectiveDisclosureStrategy::Custom(vec!["country"])); 421 | } 422 | 423 | #[test] 424 | fn test_next_level_object() { 425 | let strategy = ClaimsForSelectiveDisclosureStrategy::Custom(vec![ 426 | "address.street_address", 427 | "address.locality", 428 | "address.region", 429 | "address.country", 430 | ]); 431 | 432 | let next_strategy = strategy.next_level("address"); 433 | assert_eq!(&next_strategy, &ClaimsForSelectiveDisclosureStrategy::Custom(vec![ 434 | "street_address", 435 | "locality", 436 | "region", 437 | "country" 438 | ])); 439 | } 440 | } 441 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 DSR Corporation, Denver, Colorado. 2 | // https://www.dsr-corporation.com 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | use crate::error::Error; 6 | use crate::utils::{base64_hash, base64url_decode, jwt_payload_decode}; 7 | 8 | use error::Result; 9 | use serde::{Deserialize, Serialize}; 10 | use serde_json::{Map, Value}; 11 | use strum::Display; 12 | use std::collections::HashMap; 13 | pub use {holder::SDJWTHolder, issuer::SDJWTIssuer, issuer::ClaimsForSelectiveDisclosureStrategy, verifier::SDJWTVerifier}; 14 | 15 | mod disclosure; 16 | pub mod error; 17 | pub mod holder; 18 | pub mod issuer; 19 | pub mod utils; 20 | pub mod verifier; 21 | 22 | pub const DEFAULT_SIGNING_ALG: &str = "ES256"; 23 | const SD_DIGESTS_KEY: &str = "_sd"; 24 | const DIGEST_ALG_KEY: &str = "_sd_alg"; 25 | pub const DEFAULT_DIGEST_ALG: &str = "sha-256"; 26 | const SD_LIST_PREFIX: &str = "..."; 27 | const _SD_JWT_TYP_HEADER: &str = "sd+jwt"; 28 | const KB_JWT_TYP_HEADER: &str = "kb+jwt"; 29 | const KB_DIGEST_KEY: &str = "sd_hash"; 30 | pub const COMBINED_SERIALIZATION_FORMAT_SEPARATOR: &str = "~"; 31 | const JWT_SEPARATOR: &str = "."; 32 | const CNF_KEY: &str = "cnf"; 33 | const JWK_KEY: &str = "jwk"; 34 | 35 | #[derive(Debug)] 36 | #[allow(dead_code)] 37 | pub(crate) struct SDJWTHasSDClaimException(String); 38 | 39 | impl SDJWTHasSDClaimException {} 40 | 41 | /// SDJWTSerializationFormat is used to determine how an SD-JWT is serialized to String 42 | #[derive(Default, Clone, PartialEq, Debug, Display)] 43 | pub enum SDJWTSerializationFormat { 44 | /// JSON-encoded representation 45 | #[default] 46 | JSON, 47 | /// Base64-encoded representation 48 | Compact, 49 | } 50 | 51 | #[derive(Default)] 52 | pub(crate) struct SDJWTCommon { 53 | typ: Option, 54 | serialization_format: SDJWTSerializationFormat, 55 | unverified_input_key_binding_jwt: Option, 56 | unverified_sd_jwt: Option, 57 | unverified_sd_jwt_json: Option, 58 | unverified_input_sd_jwt_payload: Option>, 59 | hash_to_decoded_disclosure: HashMap, 60 | hash_to_disclosure: HashMap, 61 | input_disclosures: Vec, 62 | sign_alg: Option, 63 | } 64 | 65 | #[derive(Default, Serialize, Deserialize, Clone, Eq, PartialEq, Debug)] 66 | pub struct SDJWTJson { 67 | protected: String, 68 | payload: String, 69 | signature: String, 70 | pub disclosures: Vec, 71 | pub kb_jwt: Option, 72 | } 73 | 74 | // Define the SDJWTCommon struct to hold common properties. 75 | impl SDJWTCommon { 76 | fn create_hash_mappings(&mut self) -> Result<()> { 77 | self.hash_to_decoded_disclosure = HashMap::new(); 78 | self.hash_to_disclosure = HashMap::new(); 79 | 80 | for disclosure in &self.input_disclosures { 81 | let decoded_disclosure = base64url_decode(disclosure).map_err(|err| { 82 | Error::InvalidDisclosure(format!( 83 | "Error decoding disclosure {}: {}", 84 | disclosure, err 85 | )) 86 | })?; 87 | let decoded_disclosure: Value = 88 | serde_json::from_slice(&decoded_disclosure).map_err(|err| { 89 | Error::InvalidDisclosure(format!( 90 | "Error parsing disclosure {}: {}", 91 | disclosure, err 92 | )) 93 | })?; 94 | 95 | let hash = base64_hash(disclosure.as_bytes()); 96 | if self.hash_to_decoded_disclosure.contains_key(&hash) { 97 | return Err(Error::DuplicateDigestError(hash)); 98 | } 99 | self.hash_to_decoded_disclosure 100 | .insert(hash.clone(), decoded_disclosure); 101 | self.hash_to_disclosure 102 | .insert(hash.clone(), disclosure.to_owned()); 103 | } 104 | 105 | Ok(()) 106 | } 107 | 108 | fn check_for_sd_claim(the_object: &Value) -> Result<()> { 109 | match the_object { 110 | Value::Object(obj) => { 111 | for (key, value) in obj.iter() { 112 | if key == SD_DIGESTS_KEY { 113 | return Err(Error::DataFieldMismatch(format!( 114 | "Claim object cannot have `{}` field", 115 | SD_DIGESTS_KEY 116 | ))); 117 | } else { 118 | Self::check_for_sd_claim(value)?; 119 | } 120 | } 121 | } 122 | Value::Array(arr) => { 123 | for item in arr { 124 | Self::check_for_sd_claim(item)?; 125 | } 126 | } 127 | _ => {} 128 | } 129 | 130 | Ok(()) 131 | } 132 | 133 | fn parse_compact_sd_jwt(&mut self, sd_jwt_with_disclosures: String) -> Result<()> { 134 | let parts: Vec<&str> = sd_jwt_with_disclosures 135 | .split(COMBINED_SERIALIZATION_FORMAT_SEPARATOR) 136 | .collect(); 137 | if parts.len() < 2 { // minimal number of SD-JWT parts according to the standard 138 | return Err(Error::InvalidInput(format!( 139 | "Invalid SD-JWT length: {}", 140 | parts.len() 141 | ))); 142 | } 143 | let idx = parts.len(); 144 | let mut parts = parts.into_iter(); 145 | let sd_jwt = parts.next().ok_or(Error::IndexOutOfBounds { 146 | idx: 0, 147 | length: parts.len(), 148 | msg: format!("Invalid SD-JWT: {}", sd_jwt_with_disclosures), 149 | })?; 150 | self.sign_alg = Self::decode_header_and_get_sign_algorithm(sd_jwt); 151 | self.unverified_input_key_binding_jwt = Some( 152 | parts 153 | .next_back() 154 | .ok_or(Error::IndexOutOfBounds { 155 | idx: idx - 1, 156 | length: idx, 157 | msg: format!( 158 | "Invalid SD-JWT. Key binding not found: {}", 159 | sd_jwt_with_disclosures 160 | ), 161 | })? 162 | .to_owned(), 163 | ); 164 | self.input_disclosures = parts.map(str::to_owned).collect(); 165 | self.unverified_sd_jwt = Some(sd_jwt.to_owned()); 166 | 167 | let mut sd_jwt = sd_jwt.split(JWT_SEPARATOR); 168 | sd_jwt.next(); 169 | let jwt_body = sd_jwt.next().ok_or(Error::IndexOutOfBounds { 170 | idx: 1, 171 | length: 3, 172 | msg: format!( 173 | "Invalid JWT: Cannot extract JWT payload: {}", 174 | self.unverified_sd_jwt.to_owned().unwrap_or("".to_string()) 175 | ), 176 | })?; 177 | self.unverified_input_sd_jwt_payload = Some(jwt_payload_decode(jwt_body)?); 178 | Ok(()) 179 | } 180 | 181 | fn parse_json_sd_jwt(&mut self, sd_jwt_with_disclosures: String) -> Result<()> { 182 | let parsed_sd_jwt_json: SDJWTJson = serde_json::from_str(&sd_jwt_with_disclosures) 183 | .map_err(|e| Error::DeserializationError(e.to_string()))?; 184 | self.unverified_sd_jwt_json = Some(parsed_sd_jwt_json.clone()); 185 | self.unverified_input_key_binding_jwt = parsed_sd_jwt_json.kb_jwt; 186 | self.input_disclosures = parsed_sd_jwt_json.disclosures; 187 | self.unverified_input_sd_jwt_payload = 188 | Some(jwt_payload_decode(&parsed_sd_jwt_json.payload)?); 189 | let sd_jwt = format!( 190 | "{}.{}.{}", 191 | parsed_sd_jwt_json.protected, 192 | parsed_sd_jwt_json.payload, 193 | parsed_sd_jwt_json.signature 194 | ); 195 | self.unverified_sd_jwt = Some(sd_jwt.clone()); 196 | self.sign_alg = Self::decode_header_and_get_sign_algorithm(&sd_jwt); 197 | Ok(()) 198 | } 199 | 200 | fn parse_sd_jwt(&mut self, sd_jwt_with_disclosures: String) -> Result<()> { 201 | match self.serialization_format { 202 | SDJWTSerializationFormat::Compact => { 203 | self.parse_compact_sd_jwt(sd_jwt_with_disclosures) 204 | } 205 | SDJWTSerializationFormat::JSON => { 206 | self.parse_json_sd_jwt(sd_jwt_with_disclosures) 207 | } 208 | } 209 | } 210 | /// Decodes a header jwt string and extracts the "alg" field from the JSON object. 211 | /// # Arguments 212 | /// * `sd_jwt` - jwt format string. 213 | /// # Returns 214 | /// * `Option` - The result containing the algorithm String e.g ES256 or on failure None. 215 | fn decode_header_and_get_sign_algorithm(sd_jwt: &str) -> Option { 216 | let parts: Vec<&str> = sd_jwt.split('.').collect(); 217 | if parts.len() < 2 { 218 | return None; 219 | } 220 | let jwt_header = parts[0]; 221 | let decoded = base64url_decode(jwt_header).ok()?; 222 | let decoded_str = std::str::from_utf8(&decoded).ok()?; 223 | let json_sign_alg: Value = serde_json::from_str(decoded_str).ok()?; 224 | let sign_alg = json_sign_alg.get("alg") 225 | .and_then(Value::as_str) 226 | .map(String::from); 227 | sign_alg 228 | } 229 | } 230 | 231 | #[cfg(test)] 232 | mod tests { 233 | use crate::{utils, SDJWTCommon}; 234 | 235 | 236 | #[test] 237 | fn test_parse_compact_sd_jwt(){ 238 | let mut sdjwt = SDJWTCommon::default(); 239 | let encoded_empty_object = utils::base64url_encode("{}".as_bytes()); 240 | sdjwt.parse_compact_sd_jwt(format!("jwt1.{encoded_empty_object}.jwt3~disc1~disc2~kbjwt")).unwrap(); 241 | assert_eq!(sdjwt.unverified_sd_jwt.unwrap(), format!("jwt1.{encoded_empty_object}.jwt3")); 242 | assert_eq!(sdjwt.unverified_input_key_binding_jwt.unwrap(), "kbjwt"); 243 | assert_eq!(sdjwt.input_disclosures, vec!["disc1".to_string(), "disc2".to_string()]); 244 | } 245 | 246 | #[test] 247 | fn test_parse_json_sd_jwt() { 248 | let mut sdjwt = SDJWTCommon::default(); 249 | let encoded_empty_object = utils::base64url_encode("{}".as_bytes()); 250 | sdjwt.parse_json_sd_jwt(format!( 251 | "{{\"protected\":\"jwt1\",\"payload\":\"{encoded_empty_object}\",\"signature\":\"jwt3\",\"disclosures\":[\"disc1\",\"disc2\"],\"kb_jwt\":\"kbjwt\"}}" 252 | )).unwrap(); 253 | assert_eq!(sdjwt.unverified_sd_jwt.unwrap(), format!("jwt1.{encoded_empty_object}.jwt3")); 254 | assert_eq!(sdjwt.unverified_input_key_binding_jwt.unwrap(), "kbjwt"); 255 | assert_eq!(sdjwt.input_disclosures, vec!["disc1".to_string(), "disc2".to_string()]); 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 DSR Corporation, Denver, Colorado. 2 | // https://www.dsr-corporation.com 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | use crate::error; 6 | use crate::error::Error; 7 | use crate::error::Error::DeserializationError; 8 | 9 | use base64::engine::general_purpose; 10 | use base64::Engine; 11 | use error::Result; 12 | #[cfg(feature = "mock_salts")] 13 | use lazy_static::lazy_static; 14 | use rand::prelude::ThreadRng; 15 | use rand::RngCore; 16 | use serde_json::Value; 17 | use sha2::Digest; 18 | #[cfg(feature = "mock_salts")] 19 | use std::{collections::VecDeque, sync::Mutex}; 20 | 21 | #[cfg(feature = "mock_salts")] 22 | lazy_static! { 23 | pub static ref SALTS: Mutex> = Mutex::new(VecDeque::new()); 24 | } 25 | 26 | #[doc(hidden)] 27 | pub fn base64_hash(data: &[u8]) -> String { 28 | let mut hasher = sha2::Sha256::new(); 29 | hasher.update(data); 30 | let hash = hasher.finalize(); 31 | 32 | general_purpose::URL_SAFE_NO_PAD.encode(hash) 33 | } 34 | 35 | pub(crate) fn base64url_encode(data: &[u8]) -> String { 36 | general_purpose::URL_SAFE_NO_PAD.encode(data) 37 | } 38 | 39 | #[doc(hidden)] 40 | pub fn base64url_decode(b64data: &str) -> Result> { 41 | general_purpose::URL_SAFE_NO_PAD 42 | .decode(b64data) 43 | .map_err(|e| Error::DeserializationError(e.to_string())) 44 | } 45 | 46 | pub(crate) fn generate_salt() -> String { 47 | let mut buf = [0u8; 16]; 48 | ThreadRng::default().fill_bytes(&mut buf); 49 | base64url_encode(&buf) 50 | } 51 | 52 | #[cfg(feature = "mock_salts")] 53 | pub(crate) fn generate_salt_mock() -> String { 54 | let mut salts = SALTS.lock().unwrap(); 55 | return salts.pop_front().expect("SALTS is empty"); 56 | } 57 | 58 | pub(crate) fn jwt_payload_decode(b64data: &str) -> Result> { 59 | serde_json::from_str( 60 | &String::from_utf8( 61 | base64url_decode(b64data).map_err(|e| DeserializationError(e.to_string()))?, 62 | ) 63 | .map_err(|e| DeserializationError(e.to_string()))?, 64 | ) 65 | .map_err(|e| DeserializationError(e.to_string())) 66 | } 67 | -------------------------------------------------------------------------------- /src/verifier.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 DSR Corporation, Denver, Colorado. 2 | // https://www.dsr-corporation.com 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | use crate::SDJWTSerializationFormat; 6 | use crate::error::Error; 7 | use crate::error::Result; 8 | use jsonwebtoken::jwk::Jwk; 9 | use jsonwebtoken::{Algorithm, DecodingKey, Header, Validation}; 10 | use log::debug; 11 | use serde_json::{Map, Value}; 12 | use std::ops::Add; 13 | use std::option::Option; 14 | use std::str::FromStr; 15 | use std::string::String; 16 | use std::vec::Vec; 17 | 18 | use crate::utils::base64_hash; 19 | use crate::{ 20 | SDJWTCommon, CNF_KEY, COMBINED_SERIALIZATION_FORMAT_SEPARATOR, DEFAULT_DIGEST_ALG, 21 | DEFAULT_SIGNING_ALG, DIGEST_ALG_KEY, JWK_KEY, KB_DIGEST_KEY, KB_JWT_TYP_HEADER, SD_DIGESTS_KEY, 22 | SD_LIST_PREFIX, 23 | }; 24 | 25 | type KeyResolver = dyn Fn(&str, &Header) -> DecodingKey; 26 | 27 | pub struct SDJWTVerifier { 28 | sd_jwt_engine: SDJWTCommon, 29 | 30 | sd_jwt_payload: Map, 31 | _holder_public_key_payload: Option>, 32 | duplicate_hash_check: Vec, 33 | pub verified_claims: Value, 34 | 35 | cb_get_issuer_key: Box, 36 | } 37 | 38 | impl SDJWTVerifier { 39 | /// Create a new SDJWTVerifier instance. 40 | /// 41 | /// # Arguments 42 | /// * `sd_jwt_presentation` - The SD-JWT presentation to verify. 43 | /// * `cb_get_issuer_key` - A callback function that takes the issuer and the header of the SD-JWT and returns the public key of the issuer. 44 | /// * `expected_aud` - The expected audience of the SD-JWT. 45 | /// * `expected_nonce` - The expected nonce of the SD-JWT. 46 | /// * `serialization_format` - The serialization format of the SD-JWT, see [SDJWTSerializationFormat]. 47 | /// 48 | /// # Returns 49 | /// * `SDJWTVerifier` - The SDJWTVerifier instance. The verified claims can be accessed via the `verified_claims` property. 50 | pub fn new( 51 | sd_jwt_presentation: String, 52 | cb_get_issuer_key: Box, 53 | expected_aud: Option, 54 | expected_nonce: Option, 55 | serialization_format: SDJWTSerializationFormat, 56 | ) -> Result { 57 | let mut verifier = SDJWTVerifier { 58 | sd_jwt_payload: serde_json::Map::new(), 59 | _holder_public_key_payload: None, 60 | duplicate_hash_check: Vec::new(), 61 | cb_get_issuer_key, 62 | sd_jwt_engine: SDJWTCommon { 63 | serialization_format, 64 | ..Default::default() 65 | }, 66 | verified_claims: Value::Null, 67 | }; 68 | 69 | verifier.sd_jwt_engine.parse_sd_jwt(sd_jwt_presentation)?; 70 | verifier.sd_jwt_engine.create_hash_mappings()?; 71 | let sign_alg = verifier.sd_jwt_engine.sign_alg.clone(); 72 | verifier.verify_sd_jwt(sign_alg.clone())?; 73 | verifier.verified_claims = verifier.extract_sd_claims()?; 74 | 75 | if let (Some(expected_aud), Some(expected_nonce)) = (&expected_aud, &expected_nonce) { 76 | let sign_alg = verifier.sd_jwt_engine.unverified_input_key_binding_jwt 77 | .as_ref() 78 | .and_then(|value| { 79 | SDJWTCommon::decode_header_and_get_sign_algorithm(value) 80 | }); 81 | 82 | verifier.verify_key_binding_jwt( 83 | expected_aud.to_owned(), 84 | expected_nonce.to_owned(), 85 | sign_alg.as_deref(), 86 | )?; 87 | } else if expected_aud.is_some() || expected_nonce.is_some() { 88 | return Err(Error::InvalidInput( 89 | "Either both expected_aud and expected_nonce must be provided or both must be None" 90 | .to_string(), 91 | )); 92 | } 93 | 94 | Ok(verifier) 95 | } 96 | 97 | fn verify_sd_jwt(&mut self, sign_alg: Option) -> Result<()> { 98 | let sd_jwt = self 99 | .sd_jwt_engine 100 | .unverified_sd_jwt 101 | .as_ref() 102 | .ok_or(Error::ConversionError("reference".to_string()))?; 103 | let parsed_header_sd_jwt = jsonwebtoken::decode_header(sd_jwt) 104 | .map_err(|e| Error::DeserializationError(e.to_string()))?; 105 | 106 | let unverified_issuer = self 107 | .sd_jwt_engine 108 | .unverified_input_sd_jwt_payload 109 | .as_ref() 110 | .ok_or(Error::ConversionError("reference".to_string()))?["iss"] 111 | .as_str() 112 | .ok_or(Error::ConversionError("str".to_string()))?; 113 | let issuer_public_key = (self.cb_get_issuer_key)(unverified_issuer, &parsed_header_sd_jwt); 114 | let algorithm: Algorithm = match sign_alg { 115 | Some(alg_str) => Algorithm::from_str(&alg_str) 116 | .map_err(|e| Error::DeserializationError(e.to_string()))?, 117 | None => Algorithm::ES256, // Default or handle as needed 118 | }; 119 | let claims = jsonwebtoken::decode( 120 | sd_jwt, 121 | &issuer_public_key, 122 | &Validation::new(algorithm), 123 | ) 124 | .map_err(|e| Error::DeserializationError(format!("Cannot decode jwt: {}", e)))? 125 | .claims; 126 | 127 | let _ = sign_alg; //FIXME check algo 128 | 129 | self.sd_jwt_payload = claims; 130 | self._holder_public_key_payload = self 131 | .sd_jwt_payload 132 | .get(CNF_KEY) 133 | .and_then(Value::as_object) 134 | .cloned(); 135 | 136 | Ok(()) 137 | } 138 | 139 | fn verify_key_binding_jwt( 140 | &mut self, 141 | expected_aud: String, 142 | expected_nonce: String, 143 | sign_alg: Option<&str>, 144 | ) -> Result<()> { 145 | let sign_alg = sign_alg.unwrap_or(DEFAULT_SIGNING_ALG); 146 | let holder_public_key_payload_jwk = match &self._holder_public_key_payload { 147 | None => { 148 | return Err(Error::KeyNotFound( 149 | "No holder public key in SD-JWT".to_string(), 150 | )); 151 | } 152 | Some(payload) => { 153 | if let Some(jwk) = payload.get(JWK_KEY) { 154 | jwk.clone() 155 | } else { 156 | return Err(Error::InvalidInput("The holder_public_key_payload is malformed. It doesn't contain the claim jwk".to_string())); 157 | } 158 | } 159 | }; 160 | let pubkey: DecodingKey = match serde_json::from_value::(holder_public_key_payload_jwk) 161 | { 162 | Ok(jwk) => { 163 | if let Ok(pubkey) = DecodingKey::from_jwk(&jwk) { 164 | pubkey 165 | } else { 166 | return Err(Error::DeserializationError( 167 | "Cannot parse DecodingKey from json".to_string(), 168 | )); 169 | } 170 | } 171 | Err(_) => { 172 | return Err(Error::DeserializationError( 173 | "Cannot parse JWK from json".to_string(), 174 | )); 175 | } 176 | }; 177 | let key_binding_jwt = match &self.sd_jwt_engine.unverified_input_key_binding_jwt { 178 | Some(payload) => { 179 | let mut validation = Validation::new( 180 | Algorithm::from_str(sign_alg) 181 | .map_err(|e| Error::DeserializationError(e.to_string()))?, 182 | ); 183 | validation.set_audience(&[&expected_aud]); 184 | validation.set_required_spec_claims(&["aud"]); 185 | 186 | jsonwebtoken::decode::>(&payload, &pubkey, &validation) 187 | .map_err(|e| Error::DeserializationError(e.to_string()))? 188 | } 189 | None => { 190 | return Err(Error::InvalidState( 191 | "Cannot take Key Binding JWK from String".to_string(), 192 | )); 193 | } 194 | }; 195 | if key_binding_jwt.header.typ != Some(KB_JWT_TYP_HEADER.to_string()) { 196 | return Err(Error::InvalidInput("Invalid header type".to_string())); 197 | } 198 | if key_binding_jwt.claims.get("nonce") != Some(&Value::String(expected_nonce)) { 199 | return Err(Error::InvalidInput("Invalid nonce".to_string())); 200 | } 201 | if self.sd_jwt_engine.serialization_format == SDJWTSerializationFormat::Compact { 202 | let sd_hash = self._get_key_binding_digest_hash()?; 203 | if key_binding_jwt.claims.get(KB_DIGEST_KEY) != Some(&Value::String(sd_hash)) { 204 | return Err(Error::InvalidInput("Invalid digest in KB-JWT".to_string())); 205 | } 206 | } 207 | 208 | Ok(()) 209 | } 210 | 211 | fn _get_key_binding_digest_hash(&mut self) -> Result { 212 | let mut combined: Vec<&str> = 213 | Vec::with_capacity(self.sd_jwt_engine.input_disclosures.len() + 1); 214 | combined.push( 215 | self.sd_jwt_engine 216 | .unverified_sd_jwt 217 | .as_ref() 218 | .ok_or(Error::ConversionError("reference".to_string()))? 219 | ); 220 | combined.extend( 221 | self.sd_jwt_engine 222 | .input_disclosures 223 | .iter() 224 | .map(|s| s.as_str()), 225 | ); 226 | let combined = combined 227 | .join(COMBINED_SERIALIZATION_FORMAT_SEPARATOR) 228 | .add(COMBINED_SERIALIZATION_FORMAT_SEPARATOR); 229 | 230 | Ok(base64_hash(combined.as_bytes())) 231 | } 232 | 233 | fn extract_sd_claims(&mut self) -> Result { 234 | if self.sd_jwt_payload.contains_key(DIGEST_ALG_KEY) 235 | && self.sd_jwt_payload[DIGEST_ALG_KEY] != DEFAULT_DIGEST_ALG 236 | { 237 | return Err(Error::DeserializationError(format!( 238 | "Invalid hash algorithm {}", 239 | self.sd_jwt_payload[DIGEST_ALG_KEY] 240 | ))); 241 | } 242 | 243 | self.duplicate_hash_check = Vec::new(); 244 | let claims: Value = self.sd_jwt_payload.clone().into_iter().collect(); 245 | self.unpack_disclosed_claims(&claims) 246 | } 247 | 248 | fn unpack_disclosed_claims(&mut self, sd_jwt_claims: &Value) -> Result { 249 | match sd_jwt_claims { 250 | Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => { 251 | Ok(sd_jwt_claims.to_owned()) 252 | } 253 | Value::Array(arr) => { 254 | self.unpack_disclosed_claims_in_array(arr) 255 | } 256 | Value::Object(obj) => { 257 | self.unpack_disclosed_claims_in_object(obj) 258 | } 259 | } 260 | } 261 | 262 | fn unpack_disclosed_claims_in_array(&mut self, arr: &Vec) -> Result { 263 | if arr.is_empty() { 264 | return Err(Error::InvalidArrayDisclosureObject( 265 | "Array of disclosed claims cannot be empty".to_string(), 266 | )); 267 | } 268 | 269 | let mut claims = vec![]; 270 | for value in arr { 271 | 272 | match value { 273 | // case for SD objects in arrays 274 | Value::Object(obj) if obj.contains_key(SD_LIST_PREFIX) => { 275 | if obj.len() > 1 { 276 | return Err(Error::InvalidDisclosure( 277 | "Disclosed claim object in an array maust contain only one key".to_string(), 278 | )); 279 | } 280 | 281 | let digest = obj.get(SD_LIST_PREFIX).unwrap(); 282 | let disclosed_claim = self.unpack_from_digest(digest)?; 283 | if let Some(disclosed_claim) = disclosed_claim { 284 | claims.push(disclosed_claim); 285 | } 286 | }, 287 | _ => { 288 | let claim = self.unpack_disclosed_claims(value)?; 289 | claims.push(claim); 290 | }, 291 | } 292 | } 293 | Ok(Value::Array(claims)) 294 | } 295 | 296 | fn unpack_disclosed_claims_in_object(&mut self, nested_sd_jwt_claims: &Map) -> Result { 297 | let mut disclosed_claims: Map = serde_json::Map::new(); 298 | 299 | for (key, value) in nested_sd_jwt_claims { 300 | if key != SD_DIGESTS_KEY && key != DIGEST_ALG_KEY { 301 | disclosed_claims.insert(key.to_owned(), self.unpack_disclosed_claims(value)?); 302 | } 303 | } 304 | 305 | if let Some(Value::Array(digest_of_disclosures)) = nested_sd_jwt_claims.get(SD_DIGESTS_KEY) 306 | { 307 | self.unpack_from_digests(&mut disclosed_claims, digest_of_disclosures)?; 308 | } 309 | 310 | Ok(Value::Object(disclosed_claims)) 311 | } 312 | 313 | fn unpack_from_digests( 314 | &mut self, 315 | pre_output: &mut Map, 316 | digests_of_disclosures: &Vec, 317 | ) -> Result<()> { 318 | for digest in digests_of_disclosures { 319 | let digest = digest 320 | .as_str() 321 | .ok_or(Error::ConversionError("str".to_string()))?; 322 | if self.duplicate_hash_check.contains(&digest.to_string()) { 323 | return Err(Error::DuplicateDigestError(digest.to_string())); 324 | } 325 | self.duplicate_hash_check.push(digest.to_string()); 326 | 327 | if let Some(value_for_digest) = 328 | self.sd_jwt_engine.hash_to_decoded_disclosure.get(digest) 329 | { 330 | let disclosure = 331 | value_for_digest 332 | .as_array() 333 | .ok_or(Error::InvalidArrayDisclosureObject( 334 | value_for_digest.to_string(), 335 | ))?; 336 | let key = disclosure[1] 337 | .as_str() 338 | .ok_or(Error::ConversionError("str".to_string()))? 339 | .to_owned(); 340 | let value = disclosure[2].clone(); 341 | if pre_output.contains_key(&key) { 342 | return Err(Error::DuplicateKeyError(key.to_string())); 343 | } 344 | let unpacked_value = self.unpack_disclosed_claims(&value)?; 345 | pre_output.insert(key, unpacked_value); 346 | } else { 347 | debug!("Digest {:?} skipped as decoy", digest) 348 | } 349 | } 350 | 351 | Ok(()) 352 | } 353 | 354 | fn unpack_from_digest( 355 | &mut self, 356 | digest: &Value, 357 | ) -> Result> { 358 | let digest = digest 359 | .as_str() 360 | .ok_or(Error::ConversionError("str".to_string()))?; 361 | if self.duplicate_hash_check.contains(&digest.to_string()) { 362 | return Err(Error::DuplicateDigestError(digest.to_string())); 363 | } 364 | self.duplicate_hash_check.push(digest.to_string()); 365 | 366 | if let Some(value_for_digest) = 367 | self.sd_jwt_engine.hash_to_decoded_disclosure.get(digest) 368 | { 369 | let disclosure = 370 | value_for_digest 371 | .as_array() 372 | .ok_or(Error::InvalidArrayDisclosureObject( 373 | value_for_digest.to_string(), 374 | ))?; 375 | 376 | let value = disclosure[1].clone(); 377 | let unpacked_value = self.unpack_disclosed_claims(&value)?; 378 | return Ok(Some(unpacked_value)); 379 | } else { 380 | debug!("Digest {:?} skipped as decoy", digest) 381 | } 382 | 383 | Ok(None) 384 | } 385 | } 386 | 387 | #[cfg(test)] 388 | mod tests { 389 | use crate::issuer::ClaimsForSelectiveDisclosureStrategy; 390 | use crate::{SDJWTHolder, SDJWTIssuer, SDJWTVerifier, SDJWTSerializationFormat}; 391 | use jsonwebtoken::{DecodingKey, EncodingKey}; 392 | use serde_json::{json, Value}; 393 | 394 | const PRIVATE_ISSUER_PEM: &str = "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgUr2bNKuBPOrAaxsR\nnbSH6hIhmNTxSGXshDSUD1a1y7ihRANCAARvbx3gzBkyPDz7TQIbjF+ef1IsxUwz\nX1KWpmlVv+421F7+c1sLqGk4HUuoVeN8iOoAcE547pJhUEJyf5Asc6pP\n-----END PRIVATE KEY-----\n"; 395 | const PUBLIC_ISSUER_PEM: &str = "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEb28d4MwZMjw8+00CG4xfnn9SLMVM\nM19SlqZpVb/uNtRe/nNbC6hpOB1LqFXjfIjqAHBOeO6SYVBCcn+QLHOqTw==\n-----END PUBLIC KEY-----\n"; 396 | const PRIVATE_ISSUER_ED25519_PEM: &str = "-----BEGIN PRIVATE KEY-----\nMFECAQEwBQYDK2VwBCIEIF93k6rxZ8W38cm0rOwfGdH+YY3k10hP+7gd0falPLg0\ngSEAdW31QyWzfed4EPcw1rYuUa1QU+fXEL0HhdAfYZRkihc=\n-----END PRIVATE KEY-----\n"; 397 | const PUBLIC_ISSUER_ED25519_PEM: &str = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAdW31QyWzfed4EPcw1rYuUa1QU+fXEL0HhdAfYZRkihc=\n-----END PUBLIC KEY-----\n"; 398 | 399 | // EdDSA (Ed25519) 400 | const HOLDER_KEY_ED25519: &str = "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIOeIDnHHMoPCUTiq206gR+FdCdNtc31SzF1nKX31hvhd\n-----END PRIVATE KEY-----"; 401 | 402 | const HOLDER_JWK_KEY_ED25519: &str = r#"{ 403 | "alg": "EdDSA", 404 | "crv": "Ed25519", 405 | "kid": "52128f2e-900e-414e-81c3-0b5f86f0f7b3", 406 | "kty": "OKP", 407 | "x": "24QLWXJ18wtbg3k_MDGhGM17Xh39UftuxbwJZzRLzkA" 408 | }"#; 409 | 410 | #[test] 411 | fn verify_full_presentation() { 412 | let user_claims = json!({ 413 | "sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c", 414 | "iss": "https://example.com/issuer", 415 | "iat": 1683000000, 416 | "exp": 1883000000, 417 | "address": { 418 | "street_address": "Schulstr. 12", 419 | "locality": "Schulpforta", 420 | "region": "Sachsen-Anhalt", 421 | "country": "DE" 422 | } 423 | }); 424 | let private_issuer_bytes = PRIVATE_ISSUER_PEM.as_bytes(); 425 | let issuer_key = EncodingKey::from_ec_pem(private_issuer_bytes).unwrap(); 426 | let sd_jwt = SDJWTIssuer::new(issuer_key, None).issue_sd_jwt( 427 | user_claims.clone(), 428 | ClaimsForSelectiveDisclosureStrategy::AllLevels, 429 | None, 430 | false, 431 | SDJWTSerializationFormat::Compact, 432 | ) 433 | .unwrap(); 434 | let presentation = SDJWTHolder::new(sd_jwt.clone(), SDJWTSerializationFormat::Compact) 435 | .unwrap() 436 | .create_presentation( 437 | user_claims.as_object().unwrap().clone(), 438 | None, 439 | None, 440 | None, 441 | None, 442 | ) 443 | .unwrap(); 444 | assert_eq!(sd_jwt, presentation); 445 | let verified_claims = SDJWTVerifier::new( 446 | presentation, 447 | Box::new(|_, _| { 448 | let public_issuer_bytes = PUBLIC_ISSUER_PEM.as_bytes(); 449 | DecodingKey::from_ec_pem(public_issuer_bytes).unwrap() 450 | }), 451 | None, 452 | None, 453 | SDJWTSerializationFormat::Compact, 454 | ) 455 | .unwrap() 456 | .verified_claims; 457 | assert_eq!(user_claims, verified_claims); 458 | } 459 | 460 | #[test] 461 | fn verify_noclaim_presentation() { 462 | let user_claims = json!({ 463 | "sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c", 464 | "iss": "https://example.com/issuer", 465 | "iat": 1683000000, 466 | "exp": 1883000000, 467 | "address": { 468 | "street_address": "Schulstr. 12", 469 | "locality": "Schulpforta", 470 | "region": "Sachsen-Anhalt", 471 | "country": "DE" 472 | } 473 | }); 474 | let private_issuer_bytes = PRIVATE_ISSUER_PEM.as_bytes(); 475 | let issuer_key = EncodingKey::from_ec_pem(private_issuer_bytes).unwrap(); 476 | let sd_jwt = SDJWTIssuer::new(issuer_key, None).issue_sd_jwt( 477 | user_claims.clone(), 478 | ClaimsForSelectiveDisclosureStrategy::NoSDClaims, 479 | None, 480 | false, 481 | SDJWTSerializationFormat::Compact, 482 | ) 483 | .unwrap(); 484 | 485 | let presentation = SDJWTHolder::new(sd_jwt.clone(), SDJWTSerializationFormat::Compact) 486 | .unwrap() 487 | .create_presentation( 488 | user_claims.as_object().unwrap().clone(), 489 | None, 490 | None, 491 | None, 492 | None, 493 | ) 494 | .unwrap(); 495 | assert_eq!(sd_jwt, presentation); 496 | let verified_claims = SDJWTVerifier::new( 497 | presentation, 498 | Box::new(|_, _| { 499 | let public_issuer_bytes = PUBLIC_ISSUER_PEM.as_bytes(); 500 | DecodingKey::from_ec_pem(public_issuer_bytes).unwrap() 501 | }), 502 | None, 503 | None, 504 | SDJWTSerializationFormat::Compact, 505 | ) 506 | .unwrap() 507 | .verified_claims; 508 | assert_eq!(user_claims, verified_claims); 509 | } 510 | 511 | #[test] 512 | fn verify_arrayed_presentation() { 513 | let user_claims = json!( 514 | { 515 | "sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c", 516 | "name": "Bois", 517 | "iss": "https://example.com/issuer", 518 | "iat": 1683000000, 519 | "exp": 1883000000, 520 | "addresses": [ 521 | { 522 | "street_address": "Schulstr. 12", 523 | "locality": "Schulpforta", 524 | "region": "Sachsen-Anhalt", 525 | "country": "DE" 526 | }, 527 | { 528 | "street_address": "456 Main St", 529 | "locality": "Anytown", 530 | "region": "NY", 531 | "country": "US" 532 | } 533 | ], 534 | "nationalities": [ 535 | "US", 536 | "CA" 537 | ] 538 | } 539 | ); 540 | let private_issuer_bytes = PRIVATE_ISSUER_PEM.as_bytes(); 541 | let issuer_key = EncodingKey::from_ec_pem(private_issuer_bytes).unwrap(); 542 | let strategy = ClaimsForSelectiveDisclosureStrategy::Custom(vec![ 543 | "$.name", 544 | "$.addresses[1]", 545 | "$.addresses[1].country", 546 | "$.nationalities[0]", 547 | ]); 548 | let sd_jwt = SDJWTIssuer::new(issuer_key, None).issue_sd_jwt( 549 | user_claims.clone(), 550 | strategy, 551 | None, 552 | false, 553 | SDJWTSerializationFormat::Compact, 554 | ) 555 | .unwrap(); 556 | 557 | let mut claims_to_disclose = user_claims.clone(); 558 | claims_to_disclose["addresses"] = Value::Array(vec![Value::Bool(true), Value::Bool(true)]); 559 | claims_to_disclose["nationalities"] = 560 | Value::Array(vec![Value::Bool(true), Value::Bool(true)]); 561 | let presentation = SDJWTHolder::new(sd_jwt, SDJWTSerializationFormat::Compact) 562 | .unwrap() 563 | .create_presentation( 564 | claims_to_disclose.as_object().unwrap().clone(), 565 | None, 566 | None, 567 | None, 568 | None, 569 | ) 570 | .unwrap(); 571 | 572 | let verified_claims = SDJWTVerifier::new( 573 | presentation.clone(), 574 | Box::new(|_, _| { 575 | let public_issuer_bytes = PUBLIC_ISSUER_PEM.as_bytes(); 576 | DecodingKey::from_ec_pem(public_issuer_bytes).unwrap() 577 | }), 578 | None, 579 | None, 580 | SDJWTSerializationFormat::Compact, 581 | ) 582 | .unwrap() 583 | .verified_claims; 584 | 585 | let expected_verified_claims = json!( 586 | { 587 | "sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c", 588 | "addresses": [ 589 | { 590 | "street_address": "Schulstr. 12", 591 | "locality": "Schulpforta", 592 | "region": "Sachsen-Anhalt", 593 | "country": "DE", 594 | }, 595 | { 596 | "street_address": "456 Main St", 597 | "locality": "Anytown", 598 | "region": "NY", 599 | }, 600 | ], 601 | "nationalities": [ 602 | "US", 603 | "CA", 604 | ], 605 | "iss": "https://example.com/issuer", 606 | "iat": 1683000000, 607 | "exp": 1883000000, 608 | "name": "Bois" 609 | } 610 | ); 611 | 612 | assert_eq!(verified_claims, expected_verified_claims); 613 | } 614 | 615 | #[test] 616 | fn verify_arrayed_no_sd_presentation() { 617 | let user_claims = json!( 618 | { 619 | "iss": "https://example.com/issuer", 620 | "iat": 1683000000, 621 | "exp": 1883000000, 622 | "array_with_recursive_sd": [ 623 | "boring", 624 | { 625 | "foo": "bar", 626 | "baz": { 627 | "qux": "quux" 628 | } 629 | }, 630 | ["foo", "bar"] 631 | ], 632 | "test2": ["foo", "bar"] 633 | } 634 | ); 635 | let private_issuer_bytes = PRIVATE_ISSUER_PEM.as_bytes(); 636 | let issuer_key = EncodingKey::from_ec_pem(private_issuer_bytes).unwrap(); 637 | let strategy = ClaimsForSelectiveDisclosureStrategy::Custom(vec![ 638 | "$.array_with_recursive_sd[1]", 639 | "$.array_with_recursive_sd[1].baz", 640 | "$.array_with_recursive_sd[2][0]", 641 | "$.array_with_recursive_sd[2][1]", 642 | "$.test2[0]", 643 | "$.test2[1]", 644 | ]); 645 | let sd_jwt = SDJWTIssuer::new(issuer_key, None).issue_sd_jwt( 646 | user_claims.clone(), 647 | strategy, 648 | None, 649 | false, 650 | SDJWTSerializationFormat::Compact, 651 | ) 652 | .unwrap(); 653 | 654 | let claims_to_disclose = json!({}); 655 | 656 | let presentation = SDJWTHolder::new(sd_jwt, SDJWTSerializationFormat::Compact) 657 | .unwrap() 658 | .create_presentation( 659 | claims_to_disclose.as_object().unwrap().clone(), 660 | None, 661 | None, 662 | None, 663 | None, 664 | ) 665 | .unwrap(); 666 | 667 | let verified_claims = SDJWTVerifier::new( 668 | presentation.clone(), 669 | Box::new(|_, _| { 670 | let public_issuer_bytes = PUBLIC_ISSUER_PEM.as_bytes(); 671 | DecodingKey::from_ec_pem(public_issuer_bytes).unwrap() 672 | }), 673 | None, 674 | None, 675 | SDJWTSerializationFormat::Compact, 676 | ) 677 | .unwrap() 678 | .verified_claims; 679 | 680 | let expected_verified_claims = json!( 681 | { 682 | "iss": "https://example.com/issuer", 683 | "iat": 1683000000, 684 | "exp": 1883000000, 685 | "array_with_recursive_sd": [ 686 | "boring", 687 | [], 688 | ], 689 | "test2": [], 690 | } 691 | ); 692 | 693 | assert_eq!(verified_claims, expected_verified_claims); 694 | } 695 | 696 | #[test] 697 | fn verify_full_presentation_to_allow_other_algorithms_json_format() { 698 | 699 | let user_claims = json!({ 700 | "sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c", 701 | "iss": "https://example.com/issuer", 702 | "iat": 1683000000, 703 | "exp": 1883000000, 704 | "address": { 705 | "street_address": "Schulstr. 12", 706 | "locality": "Schulpforta", 707 | "region": "Sachsen-Anhalt", 708 | "country": "DE" 709 | } 710 | }); 711 | let private_issuer_bytes = PRIVATE_ISSUER_ED25519_PEM.as_bytes(); 712 | let issuer_key = EncodingKey::from_ed_pem(private_issuer_bytes).unwrap(); 713 | let sd_jwt = SDJWTIssuer::new(issuer_key, Some("EdDSA".to_string())).issue_sd_jwt( 714 | user_claims.clone(), 715 | ClaimsForSelectiveDisclosureStrategy::AllLevels, 716 | None, 717 | false, 718 | SDJWTSerializationFormat::JSON, // Changed to Json format 719 | ) 720 | .unwrap(); 721 | 722 | let presentation = SDJWTHolder::new(sd_jwt.clone(), SDJWTSerializationFormat::JSON) // Changed to Json format 723 | .unwrap() 724 | .create_presentation( 725 | user_claims.as_object().unwrap().clone(), 726 | None, 727 | None, 728 | None, 729 | None 730 | ) 731 | .unwrap(); 732 | assert_eq!(sd_jwt, presentation); 733 | let verified_claims = SDJWTVerifier::new( 734 | presentation, 735 | Box::new(|_, _| { 736 | let public_issuer_bytes = PUBLIC_ISSUER_ED25519_PEM.as_bytes(); 737 | DecodingKey::from_ed_pem(public_issuer_bytes).unwrap() 738 | }), 739 | None, 740 | None, 741 | SDJWTSerializationFormat::JSON, // Changed to Json format 742 | ) 743 | .unwrap() 744 | .verified_claims; 745 | assert_eq!(user_claims, verified_claims); 746 | } 747 | #[test] 748 | fn verify_presentation_when_sd_jwt_uses_es256_and_key_binding_uses_eddsa() { 749 | 750 | let user_claims = json!({ 751 | "address": { 752 | "street_address": "Schulstr. 12", 753 | "locality": "Schulpforta", 754 | "region": "Sachsen-Anhalt", 755 | "country": "DE" 756 | }, 757 | "exp": 1883000000, 758 | "iat": 1683000000, 759 | "iss": "https://example.com/issuer", 760 | "sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c", 761 | 762 | }); 763 | 764 | let private_issuer_bytes = PRIVATE_ISSUER_PEM.as_bytes(); 765 | let issuer_key = EncodingKey::from_ec_pem(private_issuer_bytes).unwrap(); 766 | 767 | let mut issuer = SDJWTIssuer::new(issuer_key, Some("ES256".to_string())); 768 | 769 | let sd_jwt = issuer.issue_sd_jwt( 770 | user_claims.clone(), 771 | ClaimsForSelectiveDisclosureStrategy::AllLevels, 772 | Some(serde_json::from_str(HOLDER_JWK_KEY_ED25519).unwrap()), 773 | false, 774 | SDJWTSerializationFormat::JSON, // Changed to Json format 775 | ).unwrap(); 776 | 777 | let private_holder_bytes = HOLDER_KEY_ED25519.as_bytes(); 778 | let holder_key = EncodingKey::from_ed_pem(private_holder_bytes).unwrap(); 779 | 780 | let nonce = Some(String::from("testNonce")); 781 | let aud = Some(String::from("testAud")); 782 | 783 | let mut holder = SDJWTHolder::new(sd_jwt.clone(), SDJWTSerializationFormat::JSON).unwrap(); // Changed to Json format 784 | let presentation = holder.create_presentation( 785 | user_claims.as_object().unwrap().clone(), 786 | nonce.clone(), 787 | aud.clone(), 788 | Some(holder_key), 789 | Some("EdDSA".to_string()) 790 | ) 791 | .unwrap(); 792 | let verified_claims = SDJWTVerifier::new( 793 | presentation, 794 | Box::new(|_, _| { 795 | let public_issuer_bytes = PUBLIC_ISSUER_PEM.as_bytes(); 796 | DecodingKey::from_ec_pem(public_issuer_bytes).unwrap() 797 | }), 798 | aud.clone(), 799 | nonce.clone(), 800 | SDJWTSerializationFormat::JSON, // Changed to Json format 801 | ) 802 | .unwrap() 803 | .verified_claims; 804 | 805 | let claims_to_check = json!({ 806 | "iss": user_claims["iss"].clone(), 807 | "iat": user_claims["iat"].clone(), 808 | "exp": user_claims["exp"].clone(), 809 | "cnf": { 810 | "jwk": serde_json::from_str::(HOLDER_JWK_KEY_ED25519).unwrap(), 811 | }, 812 | "sub": user_claims["sub"].clone(), 813 | "address": user_claims["address"].clone(), 814 | }); 815 | 816 | assert_eq!(claims_to_check, verified_claims); 817 | } 818 | } 819 | -------------------------------------------------------------------------------- /tests/demos.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 DSR Corporation, Denver, Colorado. 2 | // https://www.dsr-corporation.com 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | use crate::utils::fixtures::{ 6 | ADDRESS_CLAIMS, ADDRESS_ONLY_STRUCTURED_JSONPATH, ADDRESS_ONLY_STRUCTURED_ONE_OPEN_JSONPATH, 7 | ARRAYED_CLAIMS, ARRAYED_CLAIMS_JSONPATH, COMPLEX_EIDAS_CLAIMS, COMPLEX_EIDAS_JSONPATH, 8 | HOLDER_JWK_KEY, HOLDER_KEY, ISSUER_KEY, ISSUER_PUBLIC_KEY, NESTED_ARRAY_CLAIMS, 9 | NESTED_ARRAY_JSONPATH, W3C_VC_CLAIMS, W3C_VC_JSONPATH, 10 | }; 11 | use jsonwebtoken::jwk::Jwk; 12 | use jsonwebtoken::{DecodingKey, EncodingKey}; 13 | use rstest::{fixture, rstest}; 14 | use sd_jwt_rs::issuer::ClaimsForSelectiveDisclosureStrategy; 15 | use sd_jwt_rs::{SDJWTHolder, SDJWTIssuer, SDJWTJson, SDJWTVerifier, SDJWTSerializationFormat}; 16 | use sd_jwt_rs::{COMBINED_SERIALIZATION_FORMAT_SEPARATOR, DEFAULT_SIGNING_ALG}; 17 | use serde_json::{json, Map, Value}; 18 | use std::collections::HashSet; 19 | 20 | mod utils; 21 | 22 | #[fixture] 23 | fn issuer_key() -> EncodingKey { 24 | let private_issuer_bytes = ISSUER_KEY.as_bytes(); 25 | EncodingKey::from_ec_pem(private_issuer_bytes).unwrap() 26 | } 27 | 28 | fn holder_jwk() -> Option { 29 | let jwk: Jwk = serde_json::from_str(HOLDER_JWK_KEY).unwrap(); 30 | Some(jwk) 31 | } 32 | 33 | #[allow(unused)] 34 | fn holder_key() -> Option { 35 | let private_issuer_bytes = HOLDER_KEY.as_bytes(); 36 | let key = EncodingKey::from_ec_pem(private_issuer_bytes).unwrap(); 37 | Some(key) 38 | } 39 | 40 | fn _address_claims() -> serde_json::Value { 41 | serde_json::from_str(ADDRESS_CLAIMS).unwrap() 42 | } 43 | 44 | #[fixture] 45 | fn address_flat<'a>() -> ( 46 | serde_json::Value, 47 | ClaimsForSelectiveDisclosureStrategy<'a>, 48 | Map, 49 | usize, 50 | ) { 51 | let value = _address_claims(); 52 | let number_of_revealed_sds = 2; // 2 == 1('sub') + 1('address') 53 | ( 54 | value.clone(), 55 | ClaimsForSelectiveDisclosureStrategy::TopLevel, 56 | value.as_object().unwrap().clone(), 57 | number_of_revealed_sds, 58 | ) 59 | } 60 | 61 | #[fixture] 62 | fn address_full_recursive<'a>() -> ( 63 | serde_json::Value, 64 | ClaimsForSelectiveDisclosureStrategy<'a>, 65 | Map, 66 | usize, 67 | ) { 68 | let value = _address_claims(); 69 | let claims_to_disclose = value.as_object().unwrap().clone(); 70 | 71 | // revealed sds are: 72 | // sub 73 | // address 74 | // address.street_address 75 | // address.locality 76 | // address.region 77 | // address.country 78 | let number_of_revealed_sds = 6; 79 | ( 80 | value, 81 | ClaimsForSelectiveDisclosureStrategy::AllLevels, 82 | claims_to_disclose, 83 | number_of_revealed_sds, 84 | ) 85 | } 86 | 87 | #[fixture] 88 | fn address_only_structured<'a>() -> ( 89 | serde_json::Value, 90 | ClaimsForSelectiveDisclosureStrategy<'a>, 91 | Map, 92 | usize, 93 | ) { 94 | let value = _address_claims(); 95 | let mut claims_to_disclose = value.clone(); 96 | claims_to_disclose["address"] = json!({ 97 | "street_address": "Schulstr. 12", 98 | "region": "Sachsen-Anhalt", 99 | "country": "DE" 100 | }); 101 | 102 | let claims_to_disclose = claims_to_disclose.as_object().unwrap().clone(); 103 | let number_of_revealed_sds = 3; 104 | 105 | ( 106 | value.clone(), 107 | ClaimsForSelectiveDisclosureStrategy::Custom(ADDRESS_ONLY_STRUCTURED_JSONPATH.to_vec()), 108 | claims_to_disclose, 109 | number_of_revealed_sds, 110 | ) 111 | } 112 | 113 | #[fixture] 114 | fn address_only_structured_one_open<'a>() -> ( 115 | serde_json::Value, 116 | ClaimsForSelectiveDisclosureStrategy<'a>, 117 | Map, 118 | usize, 119 | ) { 120 | let value = _address_claims(); 121 | let mut claims_to_disclose = value.clone(); 122 | claims_to_disclose["address"] = json!({ 123 | "region": "Sachsen-Anhalt", 124 | "country": "DE" 125 | }); 126 | 127 | let claims_to_disclose = claims_to_disclose.as_object().unwrap().clone(); 128 | let number_of_revealed_sds = 1; 129 | 130 | ( 131 | value, 132 | ClaimsForSelectiveDisclosureStrategy::Custom(ADDRESS_ONLY_STRUCTURED_ONE_OPEN_JSONPATH.to_vec()), 133 | claims_to_disclose, 134 | number_of_revealed_sds, 135 | ) 136 | } 137 | 138 | #[fixture] 139 | fn arrayed_claims<'a>() -> ( 140 | serde_json::Value, 141 | ClaimsForSelectiveDisclosureStrategy<'a>, 142 | Map, 143 | usize, 144 | ) { 145 | let value: serde_json::Value = serde_json::from_str(ARRAYED_CLAIMS).unwrap(); 146 | let mut claims_to_disclose = value.clone(); 147 | claims_to_disclose["addresses"] = json!([true, true]); 148 | claims_to_disclose["nationalities"] = json!([false, true]); 149 | 150 | let claims_to_disclose = claims_to_disclose.as_object().unwrap().clone(); 151 | let number_of_revealed_sds = 1; 152 | 153 | ( 154 | value, 155 | ClaimsForSelectiveDisclosureStrategy::Custom(ARRAYED_CLAIMS_JSONPATH.to_vec()), 156 | claims_to_disclose, 157 | number_of_revealed_sds, 158 | ) 159 | } 160 | 161 | #[fixture] 162 | fn nested_array<'a>() -> ( 163 | serde_json::Value, 164 | ClaimsForSelectiveDisclosureStrategy<'a>, 165 | Map, 166 | usize, 167 | ) { 168 | let value: serde_json::Value = serde_json::from_str(NESTED_ARRAY_CLAIMS).unwrap(); 169 | let mut claims_to_disclose = value.clone(); 170 | claims_to_disclose["nationalities"] = json!([[false, true]]); 171 | 172 | let claims_to_disclose = claims_to_disclose.as_object().unwrap().clone(); 173 | 174 | // since the claim are nested the holder must reveal 175 | // all parents of the desired claim. 176 | // 2 is 1 (desired claim) + 1 (parent SD item of desired claim) 177 | let number_of_revealed_sds = 2; 178 | 179 | ( 180 | value.clone(), 181 | ClaimsForSelectiveDisclosureStrategy::Custom(NESTED_ARRAY_JSONPATH.to_vec()), 182 | claims_to_disclose, 183 | number_of_revealed_sds, 184 | ) 185 | } 186 | 187 | #[fixture] 188 | fn complex_eidas<'a>() -> ( 189 | serde_json::Value, 190 | ClaimsForSelectiveDisclosureStrategy<'a>, 191 | Map, 192 | usize, 193 | ) { 194 | let value: serde_json::Value = serde_json::from_str(COMPLEX_EIDAS_CLAIMS).unwrap(); 195 | let mut claims_to_disclose = value.clone(); 196 | claims_to_disclose["verified_claims"] = json!({ 197 | "verification": { 198 | "trust_framework": "eidas", 199 | "assurance_level": "high", 200 | "evidence": [ 201 | { 202 | "document": { 203 | "type": "idcard", 204 | "issuer": { 205 | "name": "c_d612", 206 | "country": "IT" 207 | } 208 | } 209 | } 210 | ] 211 | }, 212 | "claims": { 213 | "place_of_birth": { 214 | "country": "IT", 215 | "locality": "Firenze" 216 | }, 217 | "nationalities": [ 218 | "IT" 219 | ] 220 | } 221 | }); 222 | 223 | let claims_to_disclose = claims_to_disclose.as_object().unwrap().clone(); 224 | let number_of_revealed_sds = 5; 225 | 226 | ( 227 | value.clone(), 228 | ClaimsForSelectiveDisclosureStrategy::Custom(COMPLEX_EIDAS_JSONPATH.to_vec()), 229 | claims_to_disclose, 230 | number_of_revealed_sds, 231 | ) 232 | } 233 | 234 | #[fixture] 235 | fn w3c_vc<'a>() -> ( 236 | serde_json::Value, 237 | ClaimsForSelectiveDisclosureStrategy<'a>, 238 | Map, 239 | usize, 240 | ) { 241 | let value: serde_json::Value = serde_json::from_str(W3C_VC_CLAIMS).unwrap(); 242 | let mut claims_to_disclose = value.clone(); 243 | claims_to_disclose["credentialSubject"] = json!({ 244 | "email": "johndoe@example.com", 245 | "address": { 246 | "street_address": "123 Main St", 247 | "locality": "Anytown", 248 | "region": "Anystate", 249 | "country": "US" 250 | }, 251 | "birthdate": "1940-01-01", 252 | "is_over_18": true, 253 | "is_over_21": true, 254 | "is_over_65": true 255 | }); 256 | 257 | let claims_to_disclose = claims_to_disclose.as_object().unwrap().clone(); 258 | let number_of_revealed_sds = 6; 259 | 260 | ( 261 | value.clone(), 262 | ClaimsForSelectiveDisclosureStrategy::Custom(W3C_VC_JSONPATH.to_vec()), 263 | claims_to_disclose, 264 | number_of_revealed_sds, 265 | ) 266 | } 267 | 268 | #[allow(unused)] 269 | fn presentation_metadata() -> ( 270 | Option, 271 | Option, 272 | Option, 273 | Option, 274 | ) { 275 | ( 276 | Some("1234567890".to_owned()), 277 | Some("https://verifier.example.org".to_owned()), 278 | holder_key(), 279 | holder_jwk(), 280 | ) 281 | } 282 | 283 | #[rstest] 284 | #[case(address_flat())] 285 | #[case(address_full_recursive())] 286 | #[case(address_only_structured())] 287 | #[case(address_only_structured_one_open())] 288 | #[case(arrayed_claims())] 289 | #[case(nested_array())] 290 | #[case(complex_eidas())] 291 | #[case(w3c_vc())] 292 | fn demo_positive_cases( 293 | issuer_key: EncodingKey, 294 | #[case] data: ( 295 | serde_json::Value, 296 | ClaimsForSelectiveDisclosureStrategy, 297 | Map, 298 | usize, 299 | ), 300 | #[values((None, None, None, None), presentation_metadata())] presentation_metadata: ( 301 | Option, 302 | Option, 303 | Option, 304 | Option, 305 | ), 306 | #[values(SDJWTSerializationFormat::Compact, SDJWTSerializationFormat::JSON)] format: SDJWTSerializationFormat, 307 | #[values(None, Some(DEFAULT_SIGNING_ALG.to_owned()))] sign_algo: Option, 308 | #[values(true, false)] add_decoy: bool, 309 | ) { 310 | let (user_claims, strategy, holder_disclosed_claims, number_of_revealed_sds) = data; 311 | let (nonce, aud, holder_key, holder_jwk) = presentation_metadata; 312 | // Issuer issues SD-JWT 313 | let sd_jwt = SDJWTIssuer::new(issuer_key, sign_algo.clone()).issue_sd_jwt( 314 | user_claims.clone(), 315 | strategy, 316 | holder_jwk.clone(), 317 | add_decoy, 318 | format.clone(), 319 | ) 320 | .unwrap(); 321 | let issued = sd_jwt.clone(); 322 | // Holder creates presentation 323 | let mut holder = SDJWTHolder::new(sd_jwt.clone(), format.clone()).unwrap(); 324 | let presentation = holder 325 | .create_presentation( 326 | holder_disclosed_claims, 327 | nonce.clone(), 328 | aud.clone(), 329 | holder_key, 330 | sign_algo, 331 | ) 332 | .unwrap(); 333 | 334 | if format == SDJWTSerializationFormat::Compact { 335 | let mut issued_parts: HashSet<&str> = issued 336 | .split(COMBINED_SERIALIZATION_FORMAT_SEPARATOR) 337 | .collect(); 338 | issued_parts.remove(""); 339 | 340 | let mut revealed_parts: HashSet<&str> = presentation 341 | .split(COMBINED_SERIALIZATION_FORMAT_SEPARATOR) 342 | .collect(); 343 | revealed_parts.remove(""); 344 | 345 | let intersected_parts: HashSet<_> = issued_parts.intersection(&revealed_parts).collect(); 346 | // Compare that number of disclosed parts are equal 347 | let mut revealed_parts_number = revealed_parts.len(); 348 | if holder_jwk.is_some() { 349 | // Remove KB 350 | revealed_parts_number -= 1; 351 | } 352 | assert_eq!(intersected_parts.len(), revealed_parts_number); 353 | // here `+1` means adding issued jwt part also 354 | assert_eq!(number_of_revealed_sds + 1, revealed_parts_number); 355 | } else { 356 | let mut issued: SDJWTJson = serde_json::from_str(&issued).unwrap(); 357 | let mut revealed: SDJWTJson = serde_json::from_str(&presentation).unwrap(); 358 | let disclosures: Vec = revealed 359 | .disclosures 360 | .clone() 361 | .into_iter() 362 | .filter(|d| issued.disclosures.contains(d)) 363 | .collect(); 364 | assert_eq!(number_of_revealed_sds, disclosures.len()); 365 | 366 | if holder_jwk.is_some() { 367 | assert!(revealed.kb_jwt.is_some()); 368 | } 369 | 370 | issued.disclosures = disclosures; 371 | revealed.kb_jwt = None; 372 | assert_eq!(revealed, issued); 373 | } 374 | 375 | // Verify presentation 376 | let _verified = SDJWTVerifier::new( 377 | presentation.clone(), 378 | Box::new(|_, _| { 379 | let public_issuer_bytes = ISSUER_PUBLIC_KEY.as_bytes(); 380 | DecodingKey::from_ec_pem(public_issuer_bytes).unwrap() 381 | }), 382 | aud, 383 | nonce, 384 | format, 385 | ) 386 | .unwrap(); 387 | } 388 | -------------------------------------------------------------------------------- /tests/utils/fixtures.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 DSR Corporation, Denver, Colorado. 2 | // https://www.dsr-corporation.com 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | pub const ISSUER_KEY: &str = "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgUr2bNKuBPOrAaxsR\nnbSH6hIhmNTxSGXshDSUD1a1y7ihRANCAARvbx3gzBkyPDz7TQIbjF+ef1IsxUwz\nX1KWpmlVv+421F7+c1sLqGk4HUuoVeN8iOoAcE547pJhUEJyf5Asc6pP\n-----END PRIVATE KEY-----\n"; 6 | pub const ISSUER_PUBLIC_KEY: &str = "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEb28d4MwZMjw8+00CG4xfnn9SLMVM\nM19SlqZpVb/uNtRe/nNbC6hpOB1LqFXjfIjqAHBOeO6SYVBCcn+QLHOqTw==\n-----END PUBLIC KEY-----\n"; 7 | pub const HOLDER_KEY: &str = "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg5K5SCos8zf9zRemG\nGUl6yfok+/NiiryNZsvANWMhF+KhRANCAARMIARHX1m+7c4cXiPhbi99JWgcg/Ug\nuKUOWzu8J4Z6Z2cY4llm2TEBh1VilUOIW0iIq7FX7nnAhOreI0/Rdh2U\n-----END PRIVATE KEY-----\n"; 8 | pub const HOLDER_JWK_KEY: &str = r#"{ 9 | "kty": "EC", 10 | "crv": "P-256", 11 | "x": "TCAER19Zvu3OHF4j4W4vfSVoHIP1ILilDls7vCeGemc", 12 | "y": "ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ" 13 | }"#; 14 | pub const ADDRESS_CLAIMS: &str = r#"{ 15 | "sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c", 16 | "iss": "https://example.com/issuer", 17 | "iat": 1683000000, 18 | "exp": 1883000000, 19 | "address": { 20 | "street_address": "Schulstr. 12", 21 | "locality": "Schulpforta", 22 | "region": "Sachsen-Anhalt", 23 | "country": "DE" 24 | } 25 | }"#; 26 | pub const ADDRESS_ONLY_STRUCTURED_JSONPATH: [&str; 4] = [ 27 | "$.address.street_address", 28 | "$.address.locality", 29 | "$.address.region", 30 | "$.address.country", 31 | ]; 32 | pub const ADDRESS_ONLY_STRUCTURED_ONE_OPEN_JSONPATH: [&str; 3] = [ 33 | "$.address.street_address", 34 | "$.address.locality", 35 | "$.address.region", 36 | ]; 37 | pub const ARRAYED_CLAIMS: &str = r#" 38 | { 39 | "sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c", 40 | "iss": "https://example.com/issuer", 41 | "iat": 1683000000, 42 | "exp": 1883000000, 43 | "addresses": [ 44 | { 45 | "street_address": "Schulstr. 12", 46 | "locality": "Schulpforta", 47 | "region": "Sachsen-Anhalt", 48 | "country": "DE" 49 | }, 50 | { 51 | "street_address": "456 Main St", 52 | "locality": "Anytown", 53 | "region": "NY", 54 | "country": "US" 55 | } 56 | ], 57 | "nationalities": [ 58 | "US", 59 | "CA" 60 | ] 61 | }"#; 62 | pub const ARRAYED_CLAIMS_JSONPATH: [&str; 3] = [ 63 | "$.addresses[1]", 64 | "$.addresses[1].country", 65 | "$.nationalities[0]", 66 | ]; 67 | pub const NESTED_ARRAY_CLAIMS: &str = r#"{ 68 | "iss": "https://example.com/issuer", 69 | "iat": 1683000000, 70 | "exp": 1883000000, 71 | "nationalities": [ 72 | ["IT", "UZ"], 73 | ["DE", "US"] 74 | ] 75 | }"#; 76 | pub const NESTED_ARRAY_JSONPATH: [&str; 3] = [ 77 | "$.nationalities[0]", 78 | "$.nationalities[0][0]", 79 | "$.nationalities[0][1]", 80 | ]; 81 | pub const COMPLEX_EIDAS_CLAIMS: &str = r#"{ 82 | "iss": "https://example.com/issuer", 83 | "iat": 1683000000, 84 | "exp": 1883000000, 85 | "verified_claims": { 86 | "verification": { 87 | "trust_framework": "eidas", 88 | "assurance_level": "high", 89 | "evidence": [ 90 | { 91 | "type": "document", 92 | "time": "2022-04-22T11:30Z", 93 | "document": { 94 | "type": "idcard", 95 | "issuer": { 96 | "name": "c_d612", 97 | "country": "IT" 98 | }, 99 | "number": "154554", 100 | "date_of_issuance": "2021-03-23", 101 | "date_of_expiry": "2031-03-22" 102 | } 103 | } 104 | ] 105 | }, 106 | "claims": { 107 | "person_unique_identifier": 108 | "TINIT-fc0d9684-1bf0-4220-9642-8fe652c8c040", 109 | "given_name": "Raffaello", 110 | "family_name": "Mascetti", 111 | "date_of_birth": "1922-03-13", 112 | "gender": "M", 113 | "place_of_birth": { 114 | "country": "IT", 115 | "locality": "Firenze" 116 | }, 117 | "nationalities": [ 118 | "IT" 119 | ] 120 | } 121 | }, 122 | "birth_middle_name": "Lello" 123 | }"#; 124 | pub const COMPLEX_EIDAS_JSONPATH: [&str; 7] = [ 125 | "$.verified_claims.verification.evidence[0].document.issuer", 126 | "$.verified_claims.verification.evidence[0].document", 127 | "$.verified_claims.verification.evidence", 128 | "$.verified_claims.claims.date_of_birth", 129 | "$.verified_claims.claims.gender", 130 | "$.verified_claims.claims.place_of_birth", 131 | "$.verified_claims.claims.nationalities", 132 | ]; 133 | pub const W3C_VC_CLAIMS: &str = r#"{ 134 | "iss": "https://example.com", 135 | "jti": "http://example.com/credentials/3732", 136 | "iat": 1683000000, 137 | "exp": 1883000000, 138 | "vct": "IdentityCredential", 139 | "credentialSubject": { 140 | "given_name": "John", 141 | "family_name": "Doe", 142 | "email": "johndoe@example.com", 143 | "phone_number": "+1-202-555-0101", 144 | "address": { 145 | "street_address": "123 Main St", 146 | "locality": "Anytown", 147 | "region": "Anystate", 148 | "country": "US" 149 | }, 150 | "birthdate": "1940-01-01", 151 | "is_over_18": true, 152 | "is_over_21": true, 153 | "is_over_65": true 154 | } 155 | }"#; 156 | pub const W3C_VC_JSONPATH: [&str; 9] = [ 157 | "$.credentialSubject.given_name", 158 | "$.credentialSubject.family_name", 159 | "$.credentialSubject.email", 160 | "$.credentialSubject.phone_number", 161 | "$.credentialSubject.address", 162 | "$.credentialSubject.birthdate", 163 | "$.credentialSubject.is_over_18", 164 | "$.credentialSubject.is_over_21", 165 | "$.credentialSubject.is_over_65" 166 | ]; -------------------------------------------------------------------------------- /tests/utils/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 DSR Corporation, Denver, Colorado. 2 | // https://www.dsr-corporation.com 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | #![allow(unused)] 6 | 7 | pub mod fixtures; 8 | --------------------------------------------------------------------------------