Skip to content

Commit

Permalink
SD-JWT VC implementation (#1413)
Browse files Browse the repository at this point in the history
* Resolver trait and CompoundResolver macro

* invert Resolver type parameters

* associated type Target instead of type parameter T

* fix type issue in #[resolver(..)] annotation, support for multiple resolvers with the same signature

* resolver integration

* feature gate resolver-v2

* structures & basic operations

* SdJwtVc behaves as a superset of SdJwt

* issuer's metadata fetching & validation

* type metadata & credential verification

* change resolver's constraints

* integrity metadata

* display metadata

* claim metadata

* fetch issuer's JWK (to ease verification)

* validate claim disclosability

* add missing license header

* resolver change, validation

* SdJwtVcBuilder & tests

* validation test

* KB-JWT validation

* review comment

* undo resolver-v2

* fix CI errors

* make clippy happy

* add missing license headers

* add 'SdJwtVcBuilder::from_credential' to easily convert into a SD-JWT VC

* cargo clippy

* fix wasm compilation errors, clippy

* WASM Bindings for SD-JWT VC (#1493)

* reworked sd-jwt bindings

* SdJwtVc WASM bindings

* small example, many small fixes

* example & small fixes

* restore package.json

* Update bindings/wasm/src/sd_jwt_vc/builder.rs

Co-authored-by: wulfraem <[email protected]>

* Update bindings/wasm/src/sd_jwt_vc/claims.rs

Co-authored-by: wulfraem <[email protected]>

* Update bindings/wasm/src/sd_jwt_vc/metadata/vc_type.rs

Co-authored-by: wulfraem <[email protected]>

* Update bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/builder.rs

Co-authored-by: wulfraem <[email protected]>

* Update bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/sd_jwt.rs

Co-authored-by: wulfraem <[email protected]>

* review comments

---------

Co-authored-by: wulfraem <[email protected]>

* Apply suggestions from code review

Co-authored-by: wulfraem <[email protected]>

* review comments

* clippy & fmt

* clippy & fmt

* dprint fmt

---------

Co-authored-by: wulfraem <[email protected]>
  • Loading branch information
UMR1352 and wulfraem authored Jan 20, 2025
1 parent 7a4deee commit 8b6d7b8
Show file tree
Hide file tree
Showing 66 changed files with 4,342 additions and 48 deletions.
1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ edition = "2021"
homepage = "https://www.iota.org"
license = "Apache-2.0"
repository = "https://github.com/iotaledger/identity.rs"
rust-version = "1.65"

[workspace.lints.clippy]
result_large_err = "allow"
13 changes: 12 additions & 1 deletion bindings/wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ description = "Web Assembly bindings for the identity-rs crate."
crate-type = ["cdylib", "rlib"]

[dependencies]
anyhow = { version = "1.0.94", features = ["std"] }
async-trait = { version = "0.1", default-features = false }
bls12_381_plus = "0.8.17"
console_error_panic_hook = { version = "0.1" }
Expand All @@ -26,6 +27,7 @@ js-sys = { version = "0.3.61" }
json-proof-token = "0.3.4"
proc_typescript = { version = "0.1.0", path = "./proc_typescript" }
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.6.5"
serde_json = { version = "1.0", default-features = false }
serde_repr = { version = "0.1", default-features = false }
# Want to use the nice API of tokio::sync::RwLock for now even though we can't use threads.
Expand All @@ -37,7 +39,16 @@ zkryptium = "0.2.2"
[dependencies.identity_iota]
path = "../../identity_iota"
default-features = false
features = ["client", "revocation-bitmap", "resolver", "domain-linkage", "sd-jwt", "status-list-2021", "jpt-bbs-plus"]
features = [
"client",
"revocation-bitmap",
"resolver",
"domain-linkage",
"sd-jwt",
"status-list-2021",
"jpt-bbs-plus",
"sd-jwt-vc",
]

[dev-dependencies]
rand = "0.8.5"
Expand Down
167 changes: 167 additions & 0 deletions bindings/wasm/examples/src/1_advanced/10_sd_jwt_vc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// Copyright 2020-2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import {
IJwk,
IJwkParams,
IResolver,
IssuerMetadata,
Jwk,
JwkType,
JwsVerificationOptions,
KeyBindingJwtBuilder,
KeyBindingJWTValidationOptions,
SdJwtVcBuilder,
Sha256Hasher,
Timestamp,
TypeMetadataHelper,
} from "@iota/identity-wasm/node";
import { exportJWK, generateKeyPair, JWK, JWTHeaderParameters, JWTPayload, SignJWT } from "jose";

const vc_metadata: TypeMetadataHelper = JSON.parse(`{
"vct": "https://example.com/education_credential",
"name": "Betelgeuse Education Credential - Preliminary Version",
"description": "This is our development version of the education credential. Don't panic.",
"claims": [
{
"path": ["name"],
"display": [
{
"lang": "de-DE",
"label": "Vor- und Nachname",
"description": "Der Name des Studenten"
},
{
"lang": "en-US",
"label": "Name",
"description": "The name of the student"
}
],
"sd": "allowed"
},
{
"path": ["address"],
"display": [
{
"lang": "de-DE",
"label": "Adresse",
"description": "Adresse zum Zeitpunkt des Abschlusses"
},
{
"lang": "en-US",
"label": "Address",
"description": "Address at the time of graduation"
}
],
"sd": "always"
},
{
"path": ["address", "street_address"],
"display": [
{
"lang": "de-DE",
"label": "Straße"
},
{
"lang": "en-US",
"label": "Street Address"
}
],
"sd": "always",
"svg_id": "address_street_address"
},
{
"path": ["degrees", null],
"display": [
{
"lang": "de-DE",
"label": "Abschluss",
"description": "Der Abschluss des Studenten"
},
{
"lang": "en-US",
"label": "Degree",
"description": "Degree earned by the student"
}
],
"sd": "allowed"
}
]
}`);

const keypair_jwk = async (): Promise<[JWK, JWK]> => {
const [sk, pk] = await generateKeyPair("ES256").then(res => [res.privateKey, res.publicKey]);
const sk_jwk = await exportJWK(sk);
const pk_jwk = await exportJWK(pk);

return [sk_jwk, pk_jwk];
};

const signer = async (header: object, payload: object, sk_jwk: JWK) => {
return new SignJWT(payload as JWTPayload)
.setProtectedHeader(header as JWTHeaderParameters)
.sign(sk_jwk)
.then(jws => new TextEncoder().encode(jws));
};

export async function sdJwtVc() {
const hasher = new Sha256Hasher();
const issuer = "https://example.com/";
const [sk_jwk, pk_jwk] = await keypair_jwk();
const issuer_public_jwk = { ...pk_jwk, kty: JwkType.Ec, kid: "key1" } as IJwk;
const issuer_signer = (header: object, payload: object) => signer(header, payload, sk_jwk);
const issuer_metadata = new IssuerMetadata(issuer, { jwks: { keys: [issuer_public_jwk] } });
const dummy_resolver = {
resolve: async (input: string) => {
if (input == "https://example.com/.well-known/jwt-vc-issuer/") {
return new TextEncoder().encode(JSON.stringify(issuer_metadata.toJSON()));
}
if (input == "https://example.com/.well-known/vct/education_credential") {
return new TextEncoder().encode(JSON.stringify(vc_metadata));
}
},
} as IResolver<string, Uint8Array>;
const [holder_sk, holder_pk] = await keypair_jwk();
const holder_public_jwk = { ...holder_pk, kty: JwkType.Ec, kid: "key2" } as IJwk;
const holder_signer = (header: object, payload: object) => signer(header, payload, holder_sk);

/// Issuer creates an SD-JWT VC.
let sd_jwt_vc = await new SdJwtVcBuilder({
name: "John Doe",
address: {
street_address: "A random street",
number: "3a",
},
degree: [],
}, hasher)
.header({ kid: "key1" })
.vct("https://example.com/education_credential")
.iat(Timestamp.nowUTC())
.iss(issuer)
.requireKeyBinding({ kid: holder_public_jwk.kid })
.makeConcealable("/address/street_address")
.makeConcealable("/address")
.finish({ sign: issuer_signer }, "ES256");

console.log(`issued SD-JWT VC: ${sd_jwt_vc.toString()}`);

// Holder receives its SD-JWT VC and attaches its keybinding JWT.
const kb_jwt = await new KeyBindingJwtBuilder()
.iat(Timestamp.nowUTC())
.header({ kid: holder_public_jwk.kid })
.nonce("abcdefghi")
.aud("https://example.com/verify")
.finish(sd_jwt_vc.asSdJwt(), "ES256", { sign: holder_signer });
const { disclosures, sdJwtVc } = sd_jwt_vc.intoPresentation(hasher).attachKeyBindingJwt(kb_jwt).finish();
console.log(`presented SD-JWT VC: ${sdJwtVc}`);

// Verifier checks the presented sdJwtVc.
await sdJwtVc.validate(dummy_resolver, hasher);
sdJwtVc.validateKeyBinding(
new Jwk(holder_public_jwk as IJwkParams),
hasher,
new KeyBindingJWTValidationOptions({ nonce: "abcdefghi", jwsOptions: new JwsVerificationOptions() }),
);

console.log("The presented SdJwtVc is valid!");
}
3 changes: 3 additions & 0 deletions bindings/wasm/examples/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { createVC } from "./0_basic/5_create_vc";
import { createVP } from "./0_basic/6_create_vp";
import { revokeVC } from "./0_basic/7_revoke_vc";
import { didControlsDid } from "./1_advanced/0_did_controls_did";
import { sdJwtVc } from "./1_advanced/10_sd_jwt_vc";
import { didIssuesNft } from "./1_advanced/1_did_issues_nft";
import { nftOwnsDid } from "./1_advanced/2_nft_owns_did";
import { didIssuesTokens } from "./1_advanced/3_did_issues_tokens";
Expand Down Expand Up @@ -64,6 +65,8 @@ async function main() {
return await zkp();
case "9_zkp_revocation":
return await zkp_revocation();
case "10_sd_jwt_vc":
return await sdJwtVc();
default:
throw "Unknown example name: '" + argument + "'";
}
Expand Down
Loading

0 comments on commit 8b6d7b8

Please sign in to comment.