blob: 22c0ae1374219d9781fafca0a10b7bf60bc724b6 [file]
<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>