| <html> |
| <head><title>OK</title></head> |
| <body> |
| A basic SD-JWT verifier. |
| <script> |
| // This is a basic SD-JWT verifier based on Web Crypto |
| // that is intended to be used in tests. |
| // |
| // You can call main() to verify a SD-JWT+KB. It checks |
| // if: |
| // |
| // (a) The issued JWT's signature is derived from the |
| // public key passed as a parameter |
| // (b) The key binding JWT's signature is derived from |
| // the public key in the issued JWT's payload |
| // (c) The key binding JWT was addressed to the audience |
| // (d) The key binding JWT contains the challenge nonce |
| // (e) The disclosures digests are included in the |
| // issued JWT's payload _sd array. |
| // |
| // It returns false if verification fails and the list |
| // of all disclosured fields is it succeeds. |
| |
| function main(token, key, aud, nonce) { |
| return check(token, key, aud, nonce); |
| } |
| |
| async function check(token, key, aud, nonce) { |
| console.log("checking sdjwtkb"); |
| |
| // Can we parse the SD-JWT+KB? |
| const [issued, disclosures, kb] = parse(token); |
| |
| const signed = await verify(issued, key); |
| |
| // Does the signature of the issued JWT match? |
| if (!signed) { |
| console.log("signature doesn't match"); |
| return false; |
| } |
| |
| console.log("signatures match"); |
| |
| const payload = JSON.parse(base64decode(issued.payload)); |
| |
| console.log("verifying. bound?"); |
| const bound = await verify(kb, payload.cnf.jwk); |
| |
| console.log(bound); |
| |
| // Does the signature of the key binding JWT match? |
| if (!bound) { |
| return false; |
| } |
| |
| const binding = JSON.parse(base64decode(kb.payload)); |
| |
| console.log("aud checks out?" + binding.aud); |
| |
| // Was the presentation intended for me? |
| if (binding.aud != aud) { |
| return false; |
| } |
| |
| console.log("nonce checks out?"); |
| |
| // Was my challenge signed? |
| if (binding.nonce != nonce) { |
| return false; |
| } |
| |
| console.log("issued recently?"); |
| |
| // Was this issued recently? |
| if (!binding.iat) { |
| return false; |
| } |
| |
| console.log("lets look at disclosures!"); |
| |
| for (const disclosure of disclosures) { |
| console.log("Parsing a disclosure"); |
| console.log(disclosure); |
| const serialization = base64UrlEncode(JSON.stringify(disclosure)); |
| const digest = await sha256(serialization); |
| |
| // Was the disclosure included in the digest? |
| if (!payload._sd.includes(digest)) { |
| return false; |
| } |
| } |
| |
| console.log("done!"); |
| |
| // Ok, everything checks out. |
| return disclosures.map(([salt, name, value]) => value); |
| } |
| |
| function jwt(str) { |
| const header = str.substring(0, str.indexOf(".")); |
| str = str.substring(header.length + 1); |
| |
| const payload = str.substring(0, str.indexOf(".")); |
| str = str.substring(payload.length + 1); |
| |
| return { |
| header: header, |
| payload: payload, |
| signature: str |
| } |
| } |
| |
| function parse(token) { |
| let str = token; |
| |
| const first = str.substring(0, str.indexOf("~")); |
| str = str.substring(first.length + 1); |
| |
| const issued = jwt(first); |
| |
| const disclosures = []; |
| while (str.indexOf("~") > 0) { |
| const disclosure = str.substring(0, str.indexOf("~")); |
| str = str.substring(disclosure.length + 1); |
| console.log("Parsing disclosure " + disclosure); |
| disclosures.push(JSON.parse(base64decode(disclosure))); |
| } |
| |
| const kb = jwt(str); |
| |
| return [issued, disclosures, kb]; |
| } |
| |
| async function verify(jwt, jwk) { |
| const {header, payload, signature} = jwt; |
| const bufSignature = base64ToArrayBuffer(stripurlencoding(signature)); |
| |
| const data = header + "." + payload; |
| const bufData = textToArrayBuffer(data); |
| |
| const algo = { |
| name: "ECDSA", |
| namedCurve: "P-256", // secp256r1 |
| }; |
| const hash = {name: "SHA-256"}; |
| const signAlgo = {...algo, hash}; |
| |
| const key = await crypto.subtle.importKey("jwk", jwk, { |
| name: "ECDSA", |
| namedCurve: "P-256", |
| }, true, ["verify"]); |
| |
| return await crypto.subtle.verify( |
| signAlgo, key, bufSignature, bufData); |
| } |
| |
| function stripurlencoding(b64) { |
| return b64.replace(/_/g, '/').replace(/-/g, '+'); |
| } |
| |
| function base64ToArrayBuffer(b64) { |
| var byteString = atob(b64); |
| var byteArray = new Uint8Array(byteString.length); |
| for (var i = 0; i < byteString.length; i++) { |
| byteArray[i] = byteString.charCodeAt(i); |
| } |
| return byteArray.buffer; |
| } |
| |
| function textToArrayBuffer(str) { |
| var buf = unescape(encodeURIComponent(str)) // 2 bytes for each char |
| var bufView = new Uint8Array(buf.length) |
| for (var i=0; i < buf.length; i++) { |
| bufView[i] = buf.charCodeAt(i) |
| } |
| return bufView |
| } |
| |
| function base64decode(base64) { |
| return atob(base64.replace(/_/g, '/').replace(/-/g, '+')); |
| } |
| |
| function urlEncode(str) { |
| return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); |
| } |
| |
| function base64UrlEncode(str) { |
| const base64Encoded = btoa(str); |
| return urlEncode(base64Encoded); |
| } |
| |
| async function sha256(message) { |
| const encoder = new TextEncoder(); |
| const data = encoder.encode(message); |
| const hash = await window.crypto.subtle.digest("SHA-256", data); |
| const hashArray = Array.from(new Uint8Array(hash)); |
| return base64UrlEncode(String.fromCharCode(...hashArray)); |
| } |
| </script> |
| </body> |
| </html> |