mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:32:44 +08:00
141 lines
3.6 KiB
JavaScript
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 };
|