Files
qinglong/scripts/lib/ql3-management-live-identity.cjs
T

141 lines
3.6 KiB
JavaScript

#!/usr/bin/env node
'use strict';
const assert = require('node:assert/strict');
const crypto = require('node:crypto');
function createManagementIdentityCeremony(options) {
assert.match(options.issuer, /^https:\/\/[A-Za-z0-9.-]+\/$/);
for (const value of [
options.audience,
options.purpose,
options.tokenType,
options.subject,
options.jtiPrefix,
]) {
assert.match(value, /^[A-Za-z0-9][A-Za-z0-9._:+-]{0,127}$/);
}
function reviewedKey(kid) {
assert.match(kid, /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/);
const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519');
return Object.freeze({
kid,
privateKey,
publicJwk: Object.freeze({
...publicKey.export({ format: 'jwk' }),
alg: 'EdDSA',
kid,
use: 'sig',
}),
});
}
function keyset(generation, keys, revokedKids = []) {
assert.ok(Number.isSafeInteger(generation) && generation >= 1);
return Object.freeze({
schemaVersion: 1,
generation,
issuer: options.issuer,
audience: options.audience,
keys: keys.map((key) => key.publicJwk),
revokedKids: [...revokedKids],
assuranceMappings: [
{
acr: 'urn:ql3:mfa',
assurance: 'multi_factor',
requiredAmr: ['pwd', 'otp'],
},
],
constraints: {
maxAssertionBytes: 8 * 1024,
maxLifetimeMs: 5 * 60 * 1000,
maxAuthenticationAgeMs: 5 * 60 * 1000,
clockSkewMs: 5 * 1000,
},
});
}
function assertionForSubject(key, subject, suffix = crypto.randomUUID()) {
assert.match(subject, /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/);
const now = Math.floor(Date.now() / 1_000);
const header = Buffer.from(
JSON.stringify({
alg: 'EdDSA',
kid: key.kid,
typ: options.tokenType,
}),
).toString('base64url');
const payload = Buffer.from(
JSON.stringify({
acr: 'urn:ql3:mfa',
amr: ['pwd', 'otp'],
aud: options.audience,
auth_time: now - 1,
exp: now + 290,
iat: now,
iss: options.issuer,
jti: options.jtiPrefix + '-' + suffix,
ql3_purpose: options.purpose,
sub: subject,
}),
).toString('base64url');
const signed = header + '.' + payload;
return (
signed +
'.' +
crypto
.sign(null, Buffer.from(signed, 'ascii'), key.privateKey)
.toString('base64url')
);
}
function assertion(key, suffix = crypto.randomUUID()) {
return assertionForSubject(key, options.subject, suffix);
}
function weakAssertion(key, suffix = crypto.randomUUID()) {
const now = Math.floor(Date.now() / 1_000);
const header = Buffer.from(
JSON.stringify({
alg: 'EdDSA',
kid: key.kid,
typ: options.tokenType,
}),
).toString('base64url');
const payload = Buffer.from(
JSON.stringify({
acr: 'urn:ql3:password',
amr: ['pwd'],
aud: options.audience,
auth_time: now - 1,
exp: now + 290,
iat: now,
iss: options.issuer,
jti: options.jtiPrefix + '-weak-' + suffix,
ql3_purpose: options.purpose,
sub: options.subject,
}),
).toString('base64url');
const signed = header + '.' + payload;
return (
signed +
'.' +
crypto
.sign(null, Buffer.from(signed, 'ascii'), key.privateKey)
.toString('base64url')
);
}
return Object.freeze({
assertion,
assertionForSubject,
keyset,
reviewedKey,
weakAssertion,
});
}
module.exports = { createManagementIdentityCeremony };