|
| 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 | +} |
0 commit comments