vouchsafe
Version:
Vouchsafe Decentralized Identity and Trust Verification module
135 lines (113 loc) • 4.07 kB
JavaScript
import { SignJWT, jwtVerify, decodeJwt as joseDecodeJwt, importPKCS8, importSPKI } from 'jose';
import { toBase64, fromBase64 } from './utils.mjs';
import { verifyUrnMatchesKey } from './urn.mjs';
/**
* Create a Vouchsafe identity-bound JWT
* @param {string} iss - URN of the issuer (e.g. "urn:vouchsafe:alice.xxxxxx")
* @param {string} iss_key - base64-encoded public key matching the URN
* @param {Uint8Array|string} privateKey - Private key for signing (raw or PEM)
* @param {object} claims - Custom JWT claims
* @param {object} [options] - Optional fields (e.g., exp, jti)
* @returns {Promise<string>} - Signed JWT string
*/
export async function createJwt(iss, iss_key, privateKey, claims = {}, options = {}) {
const valid = await verifyUrnMatchesKey(iss, iss_key);
if (!valid) {
throw new Error('Provided iss_key does not match issuer URN');
}
//console.warn("privateKey", privateKey);
const key = await toPrivateKey(privateKey);
//console.warn("XXXXXXXXXXXXXXXXXXXXXXXXXkey", key);
const iat = options.iat || Math.floor(Date.now() / 1000);
const payload = {
...claims,
iss,
iss_key,
iat
};
const jwt = await new SignJWT(payload)
.setProtectedHeader({
alg: 'EdDSA'
})
.sign(key);
return jwt;
}
/**
* Verify a Vouchsafe JWT
* @param {string} token - JWT string to verify
* @param {object} [opts]
* - pubKeyOverride: Uint8Array or PEM string
* - verifyIssuerKey: boolean (default: true)
* @returns {Promise<object>} - Decoded payload if valid
* @throws on verification failure
*/
export async function verifyJwt(token, opts = {}) {
const payload = joseDecodeJwt(token);
if (!payload || !payload.iss) throw new Error("Invalid JWT payload");
const pubKey = opts.pubKeyOverride ?
await toPublicKey(opts.pubKeyOverride) :
await extractKeyFromPayload(payload);
const {
payload: verified
} = await jwtVerify(token, pubKey, {
algorithms: ['EdDSA']
});
if (opts.verifyIssuerKey !== false && verified.iss_key) {
const matches = await verifyUrnMatchesKey(verified.iss, verified.iss_key);
if (!matches) throw new Error("iss_key does not match iss URN");
}
return verified;
}
export function decodeJwt(token, {
full = false
} = {}) {
const payload = joseDecodeJwt(token);
if (!full) return payload;
const [headerB64] = token.split('.');
const header = JSON.parse(
new TextDecoder().decode(
Uint8Array.from(atob(headerB64), c => c.charCodeAt(0))
)
);
return {
payload,
header
};
}
// -- Internals --
async function toPrivateKey(input) {
if (typeof input === 'string' && input.includes('BEGIN')) {
return await importPKCS8(input, 'EdDSA');
}
const pem = toPem(input, 'PRIVATE');
//console.warn("pemkey", pem);
return await importPKCS8(pem, 'EdDSA');
}
async function toPublicKey(input) {
if (typeof input === 'string' && input.includes('BEGIN')) {
return await importSPKI(input, 'EdDSA');
}
const pem = toPem(input, 'PUBLIC');
return await importSPKI(pem, 'EdDSA');
}
async function extractKeyFromPayload(payload) {
if (!payload.iss_key) throw new Error("Missing iss_key for verification");
return await toPublicKey(payload.iss_key);
}
function toPem(input, type = 'PRIVATE') {
if (typeof input === 'string') {
if (input.includes('BEGIN')) {
return input; // already PEM
}
// assume it's base64 already (no re-encoding)
return `-----BEGIN ${type} KEY-----\n${chunk(input)}\n-----END ${type} KEY-----`;
}
if (input instanceof Uint8Array) {
const b64 = toBase64(input);
return `-----BEGIN ${type} KEY-----\n${chunk(b64)}\n-----END ${type} KEY-----`;
}
throw new Error(`Unsupported key input type: ${typeof input}`);
}
function chunk(str, len = 64) {
return str.match(new RegExp(`.{1,${len}}`, 'g')).join('\n');
}