Skip to content

Commit

Permalink
WASM Bindings for SD-JWT VC (#1493)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
UMR1352 and wulfraem authored Jan 10, 2025
1 parent f2c0704 commit 9f8f997
Show file tree
Hide file tree
Showing 36 changed files with 1,747 additions and 36 deletions.
4 changes: 3 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,7 @@ 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
145 changes: 145 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,145 @@
// Copyright 2020-2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import { IssuerMetadata, SdJwtVcBuilder, Sha256Hasher, Timestamp, TypeMetadataHelper, IResolver, JwkType, IJwk, KeyBindingJwtBuilder, Jwk, IJwkParams, KeyBindingJWTValidationOptions, JwsVerificationOptions } 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 @@ -19,6 +19,7 @@ import { sdJwt } from "./1_advanced/6_sd_jwt";
import { statusList2021 } from "./1_advanced/7_status_list_2021";
import { zkp } from "./1_advanced/8_zkp";
import { zkp_revocation } from "./1_advanced/9_zkp_revocation";
import { sdJwtVc } from "./1_advanced/10_sd_jwt_vc";

async function main() {
// Extract example name.
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 9f8f997

Please sign in to comment.