Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interop tool #24

Merged
merged 2 commits into from
Feb 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ lazy_static = { version = "1.4", optional = true }
log = "0.4"
rand = "0.8"
serde = { version = "1.0.193", features = ["derive"] }
serde_json = "1.0"
serde_json = { version = "1.0.113", features = ["preserve_order"] }
sha2 = "0.10"
thiserror = "1.0.51"
strum = { version = "0.25", default-features = false, features = ["std", "derive"] }
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ cargo test
```

### Interoperability testing tool
Coming soon (planned for v0.0.7)
See [Generate tool README](./generate/README.md) document.

## External Dependencies

Expand Down
15 changes: 15 additions & 0 deletions generate/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "sd-jwt-generate"
version = "0.1.0"
edition = "2021"
authors = ["Abdulbois Tursunov <[email protected]>", "Alexander Sukhachev <[email protected]>"]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
clap = { version = "4.4.10", features = ["derive"] }
serde = { version = "1.0.193", features = ["derive"] }
serde_yaml = "0.9.27"
serde_json = { version = "1.0.113", features = ["preserve_order"] }
jsonwebtoken = "9.1"
sd-jwt-rs = {path = "./..", features = ["mock_salts"]}
117 changes: 117 additions & 0 deletions generate/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# SD-JWT Interop tool

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/).

## How does the Interop tool work?

The main idea is to generate data structures (SDJWT/presentation/verified claims) using both implementations and compare them.

The `sd-jwt-python` is used to generate artifacts based on input data (`specification.yml`) and store them as files.
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.

There are some factors that make impossible to compare data due to non-equivalence data generated by different implementations:

- Using random 'salt' in each run that make results different even though they are generated by the same implementation.
- Not equivalent json-serialized strings (different number of spaces) generated under the hood of the different implementations.
- Using 'decoy' digests in the SD-JWT payload.

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).

### Deterministic 'salt'

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.

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).


### Similar json serialization

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:

```rust
value_str = value_str
.replace(":[", ": [")
.replace(',', ", ")
.replace("\":", "\": ")
.replace("\": ", "\": ");
```

### 'Decoy' SD items

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.


## How to use the interop tool?

1. Install the prerequisites
2. Clone and build the `sd-jwt-rust` project
3. Clone and build the `sd-jwt-python` project
4. Generate artifacts using the `sd-jwt-python` project
5. Run the interop tool

### Install the prerequisites

In order to be able to build both implementations it is required to setup following tools:

- `Rust`/`cargo`
- `poetry`


### Clone and build the `sd-jwt-rust` project

```shell
git clone [email protected]:openwallet-foundation-labs/sd-jwt-rust.git
cd sd-jwt-rust/generate
cargo build
```


### Clone and build the `sd-jwt-python` project

Once the project repo is cloned to local directory it is necessary to apply special patch.
This patch is required to have some additional files as artifacts generated by the `sd-jwt-python` project.

Files:

- `claims_vs_salts.json` file contains values of so called 'salt' that have been used during `SDJWT` issuance.
- `issuer_key.pem` file contains the issuer's private key.
- `issuer_public_key.pem` file contains the issuer's public key.
- `holder_key.pem` file contains the holder's private key.

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`.


```shell
git clone [email protected]:openwallet-foundation-labs/sd-jwt-python.git
cd sd-jwt-python

# apply the patch
git apply ../sd-jwt-rust/generate/sd_jwt_python.patch

# build
poetry install && poetry build
```



### Generate artifacts using the `sd-jwt-python` project

```shell
pushd sd-jwt-python/tests/testcases && poetry run ../../src/sd_jwt/bin/generate.py -- example && popd
pushd sd-jwt-python/examples && poetry run ../src/sd_jwt/bin/generate.py -- example && popd
```


### Run the interop tool

```shell
cd sd-jwt-rust/generate
sd_jwt_py="../../sd-jwt-python"
for cases_dir in $sd_jwt_py/examples $sd_jwt_py/tests/testcases; do
for test_case_dir in $(ls $cases_dir); do
if [[ -d $cases_dir/$test_case_dir ]]; then
./target/debug/sd-jwt-generate -p $cases_dir/$test_case_dir
fi
done
done
```
97 changes: 97 additions & 0 deletions generate/sd_jwt_python.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
diff --git a/.gitignore b/.gitignore
index 1874e26..72ff453 100644
--- a/.gitignore
+++ b/.gitignore
@@ -157,7 +157,7 @@ cython_debug/
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
-#.idea/
+.idea/


# Ignore output of test cases except for specification.yml
diff --git a/pyproject.toml b/pyproject.toml
index 4294e64..47c9281 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -12,7 +12,7 @@ jwcrypto = ">=1.3.1"
pyyaml = ">=5.4"

[tool.poetry.group.dev.dependencies]
-flake8 = "^6.0.0"
+# flake8 = "^6.0.0"
black = "^23.3.0"

[build-system]
diff --git a/src/sd_jwt/bin/generate.py b/src/sd_jwt/bin/generate.py
index ad00641..d0299ea 100755
--- a/src/sd_jwt/bin/generate.py
+++ b/src/sd_jwt/bin/generate.py
@@ -105,12 +105,36 @@ def generate_test_case_data(settings: Dict, testcase_path: Path, type: str):

# Write the test case data to the directory of the test case

+ claims_vs_salts = []
+ for disclosure in sdjwt_at_issuer.ii_disclosures:
+ claims_vs_salts.append(disclosure.salt)
+
_artifacts = {
"user_claims": (
remove_sdobj_wrappers(testcase["user_claims"]),
"User Claims",
"json",
),
+ "issuer_key": (
+ demo_keys["issuer_key"].export_to_pem(True, None).decode("utf-8"),
+ "Issuer private key",
+ "pem",
+ ),
+ "issuer_public_key": (
+ demo_keys["issuer_public_key"].export_to_pem(False, None).decode("utf-8"),
+ "Issuer public key",
+ "pem",
+ ),
+ "holder_key": (
+ demo_keys["holder_key"].export_to_pem(True, None).decode("utf-8"),
+ "Issuer private key",
+ "pem",
+ ),
+ "claims_vs_salts": (
+ claims_vs_salts,
+ "Claims with Salts",
+ "json",
+ ),
"sd_jwt_payload": (
sdjwt_at_issuer.sd_jwt_payload,
"Payload of the SD-JWT",
diff --git a/src/sd_jwt/disclosure.py b/src/sd_jwt/disclosure.py
index a9727c4..d1f983a 100644
--- a/src/sd_jwt/disclosure.py
+++ b/src/sd_jwt/disclosure.py
@@ -15,11 +15,11 @@ class SDJWTDisclosure:
self._hash()

def _hash(self):
- salt = self.issuer._generate_salt()
+ self._salt = self.issuer._generate_salt()
if self.key is None:
- data = [salt, self.value]
+ data = [self._salt, self.value]
else:
- data = [salt, self.key, self.value]
+ data = [self._salt, self.key, self.value]

self._json = dumps(data).encode("utf-8")

@@ -30,6 +30,10 @@ class SDJWTDisclosure:
def hash(self):
return self._hash

+ @property
+ def salt(self):
+ return self._salt
+
@property
def b64(self):
return self._raw_b64
Loading
Loading