Skip to content

Commit 8b6d7b8

Browse files
UMR1352wulfraem
andauthored
SD-JWT VC implementation (#1413)
* 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 <wulfraem@users.noreply.github.com> * Update bindings/wasm/src/sd_jwt_vc/claims.rs Co-authored-by: wulfraem <wulfraem@users.noreply.github.com> * Update bindings/wasm/src/sd_jwt_vc/metadata/vc_type.rs Co-authored-by: wulfraem <wulfraem@users.noreply.github.com> * Update bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/builder.rs Co-authored-by: wulfraem <wulfraem@users.noreply.github.com> * Update bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/sd_jwt.rs Co-authored-by: wulfraem <wulfraem@users.noreply.github.com> * review comments --------- Co-authored-by: wulfraem <wulfraem@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: wulfraem <wulfraem@users.noreply.github.com> * review comments * clippy & fmt * clippy & fmt * dprint fmt --------- Co-authored-by: wulfraem <wulfraem@users.noreply.github.com>
1 parent 7a4deee commit 8b6d7b8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+4342
-48
lines changed

Cargo.toml

-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ edition = "2021"
3434
homepage = "https://www.iota.org"
3535
license = "Apache-2.0"
3636
repository = "https://github.com/iotaledger/identity.rs"
37-
rust-version = "1.65"
3837

3938
[workspace.lints.clippy]
4039
result_large_err = "allow"

bindings/wasm/Cargo.toml

+12-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ description = "Web Assembly bindings for the identity-rs crate."
1616
crate-type = ["cdylib", "rlib"]
1717

1818
[dependencies]
19+
anyhow = { version = "1.0.94", features = ["std"] }
1920
async-trait = { version = "0.1", default-features = false }
2021
bls12_381_plus = "0.8.17"
2122
console_error_panic_hook = { version = "0.1" }
@@ -26,6 +27,7 @@ js-sys = { version = "0.3.61" }
2627
json-proof-token = "0.3.4"
2728
proc_typescript = { version = "0.1.0", path = "./proc_typescript" }
2829
serde = { version = "1.0", features = ["derive"] }
30+
serde-wasm-bindgen = "0.6.5"
2931
serde_json = { version = "1.0", default-features = false }
3032
serde_repr = { version = "0.1", default-features = false }
3133
# Want to use the nice API of tokio::sync::RwLock for now even though we can't use threads.
@@ -37,7 +39,16 @@ zkryptium = "0.2.2"
3739
[dependencies.identity_iota]
3840
path = "../../identity_iota"
3941
default-features = false
40-
features = ["client", "revocation-bitmap", "resolver", "domain-linkage", "sd-jwt", "status-list-2021", "jpt-bbs-plus"]
42+
features = [
43+
"client",
44+
"revocation-bitmap",
45+
"resolver",
46+
"domain-linkage",
47+
"sd-jwt",
48+
"status-list-2021",
49+
"jpt-bbs-plus",
50+
"sd-jwt-vc",
51+
]
4152

4253
[dev-dependencies]
4354
rand = "0.8.5"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
// Copyright 2020-2024 IOTA Stiftung
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import {
5+
IJwk,
6+
IJwkParams,
7+
IResolver,
8+
IssuerMetadata,
9+
Jwk,
10+
JwkType,
11+
JwsVerificationOptions,
12+
KeyBindingJwtBuilder,
13+
KeyBindingJWTValidationOptions,
14+
SdJwtVcBuilder,
15+
Sha256Hasher,
16+
Timestamp,
17+
TypeMetadataHelper,
18+
} from "@iota/identity-wasm/node";
19+
import { exportJWK, generateKeyPair, JWK, JWTHeaderParameters, JWTPayload, SignJWT } from "jose";
20+
21+
const vc_metadata: TypeMetadataHelper = JSON.parse(`{
22+
"vct": "https://example.com/education_credential",
23+
"name": "Betelgeuse Education Credential - Preliminary Version",
24+
"description": "This is our development version of the education credential. Don't panic.",
25+
"claims": [
26+
{
27+
"path": ["name"],
28+
"display": [
29+
{
30+
"lang": "de-DE",
31+
"label": "Vor- und Nachname",
32+
"description": "Der Name des Studenten"
33+
},
34+
{
35+
"lang": "en-US",
36+
"label": "Name",
37+
"description": "The name of the student"
38+
}
39+
],
40+
"sd": "allowed"
41+
},
42+
{
43+
"path": ["address"],
44+
"display": [
45+
{
46+
"lang": "de-DE",
47+
"label": "Adresse",
48+
"description": "Adresse zum Zeitpunkt des Abschlusses"
49+
},
50+
{
51+
"lang": "en-US",
52+
"label": "Address",
53+
"description": "Address at the time of graduation"
54+
}
55+
],
56+
"sd": "always"
57+
},
58+
{
59+
"path": ["address", "street_address"],
60+
"display": [
61+
{
62+
"lang": "de-DE",
63+
"label": "Straße"
64+
},
65+
{
66+
"lang": "en-US",
67+
"label": "Street Address"
68+
}
69+
],
70+
"sd": "always",
71+
"svg_id": "address_street_address"
72+
},
73+
{
74+
"path": ["degrees", null],
75+
"display": [
76+
{
77+
"lang": "de-DE",
78+
"label": "Abschluss",
79+
"description": "Der Abschluss des Studenten"
80+
},
81+
{
82+
"lang": "en-US",
83+
"label": "Degree",
84+
"description": "Degree earned by the student"
85+
}
86+
],
87+
"sd": "allowed"
88+
}
89+
]
90+
}`);
91+
92+
const keypair_jwk = async (): Promise<[JWK, JWK]> => {
93+
const [sk, pk] = await generateKeyPair("ES256").then(res => [res.privateKey, res.publicKey]);
94+
const sk_jwk = await exportJWK(sk);
95+
const pk_jwk = await exportJWK(pk);
96+
97+
return [sk_jwk, pk_jwk];
98+
};
99+
100+
const signer = async (header: object, payload: object, sk_jwk: JWK) => {
101+
return new SignJWT(payload as JWTPayload)
102+
.setProtectedHeader(header as JWTHeaderParameters)
103+
.sign(sk_jwk)
104+
.then(jws => new TextEncoder().encode(jws));
105+
};
106+
107+
export async function sdJwtVc() {
108+
const hasher = new Sha256Hasher();
109+
const issuer = "https://example.com/";
110+
const [sk_jwk, pk_jwk] = await keypair_jwk();
111+
const issuer_public_jwk = { ...pk_jwk, kty: JwkType.Ec, kid: "key1" } as IJwk;
112+
const issuer_signer = (header: object, payload: object) => signer(header, payload, sk_jwk);
113+
const issuer_metadata = new IssuerMetadata(issuer, { jwks: { keys: [issuer_public_jwk] } });
114+
const dummy_resolver = {
115+
resolve: async (input: string) => {
116+
if (input == "https://example.com/.well-known/jwt-vc-issuer/") {
117+
return new TextEncoder().encode(JSON.stringify(issuer_metadata.toJSON()));
118+
}
119+
if (input == "https://example.com/.well-known/vct/education_credential") {
120+
return new TextEncoder().encode(JSON.stringify(vc_metadata));
121+
}
122+
},
123+
} as IResolver<string, Uint8Array>;
124+
const [holder_sk, holder_pk] = await keypair_jwk();
125+
const holder_public_jwk = { ...holder_pk, kty: JwkType.Ec, kid: "key2" } as IJwk;
126+
const holder_signer = (header: object, payload: object) => signer(header, payload, holder_sk);
127+
128+
/// Issuer creates an SD-JWT VC.
129+
let sd_jwt_vc = await new SdJwtVcBuilder({
130+
name: "John Doe",
131+
address: {
132+
street_address: "A random street",
133+
number: "3a",
134+
},
135+
degree: [],
136+
}, hasher)
137+
.header({ kid: "key1" })
138+
.vct("https://example.com/education_credential")
139+
.iat(Timestamp.nowUTC())
140+
.iss(issuer)
141+
.requireKeyBinding({ kid: holder_public_jwk.kid })
142+
.makeConcealable("/address/street_address")
143+
.makeConcealable("/address")
144+
.finish({ sign: issuer_signer }, "ES256");
145+
146+
console.log(`issued SD-JWT VC: ${sd_jwt_vc.toString()}`);
147+
148+
// Holder receives its SD-JWT VC and attaches its keybinding JWT.
149+
const kb_jwt = await new KeyBindingJwtBuilder()
150+
.iat(Timestamp.nowUTC())
151+
.header({ kid: holder_public_jwk.kid })
152+
.nonce("abcdefghi")
153+
.aud("https://example.com/verify")
154+
.finish(sd_jwt_vc.asSdJwt(), "ES256", { sign: holder_signer });
155+
const { disclosures, sdJwtVc } = sd_jwt_vc.intoPresentation(hasher).attachKeyBindingJwt(kb_jwt).finish();
156+
console.log(`presented SD-JWT VC: ${sdJwtVc}`);
157+
158+
// Verifier checks the presented sdJwtVc.
159+
await sdJwtVc.validate(dummy_resolver, hasher);
160+
sdJwtVc.validateKeyBinding(
161+
new Jwk(holder_public_jwk as IJwkParams),
162+
hasher,
163+
new KeyBindingJWTValidationOptions({ nonce: "abcdefghi", jwsOptions: new JwsVerificationOptions() }),
164+
);
165+
166+
console.log("The presented SdJwtVc is valid!");
167+
}

bindings/wasm/examples/src/main.ts

+3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { createVC } from "./0_basic/5_create_vc";
1010
import { createVP } from "./0_basic/6_create_vp";
1111
import { revokeVC } from "./0_basic/7_revoke_vc";
1212
import { didControlsDid } from "./1_advanced/0_did_controls_did";
13+
import { sdJwtVc } from "./1_advanced/10_sd_jwt_vc";
1314
import { didIssuesNft } from "./1_advanced/1_did_issues_nft";
1415
import { nftOwnsDid } from "./1_advanced/2_nft_owns_did";
1516
import { didIssuesTokens } from "./1_advanced/3_did_issues_tokens";
@@ -64,6 +65,8 @@ async function main() {
6465
return await zkp();
6566
case "9_zkp_revocation":
6667
return await zkp_revocation();
68+
case "10_sd_jwt_vc":
69+
return await sdJwtVc();
6770
default:
6871
throw "Unknown example name: '" + argument + "'";
6972
}

0 commit comments

Comments
 (0)