Skip to content

Commit

Permalink
Finalize interop tool impl.
Browse files Browse the repository at this point in the history
Signed-off-by: Alexander Sukhachev <[email protected]>
  • Loading branch information
alexsdsr authored and jovfer committed Feb 18, 2024
1 parent 424a4f0 commit a90017d
Show file tree
Hide file tree
Showing 17 changed files with 696 additions and 163 deletions.
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
5 changes: 3 additions & 2 deletions generate/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
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 = "1.0.108"
serde_json = { version = "1.0.113", features = ["preserve_order"] }
jsonwebtoken = "9.1"
sd-jwt-rs = {path = "./.."}
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
6 changes: 3 additions & 3 deletions generate/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,23 @@
use std::error::Error as StdError;
use std::fmt::{self, Display, Formatter};
use std::result::Result as StdResult;
use serde_json;
use serde_yaml;

pub type Result<T> = std::result::Result<T, Error>;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ErrorKind {
Input,
IOError,
DataNotEqual,
}

impl ErrorKind {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Input => "Input error",
Self::IOError => "IO error"
Self::IOError => "IO error",
Self::DataNotEqual => "Data not equal error",
}
}
}
Expand Down
Loading

0 comments on commit a90017d

Please sign in to comment.