mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 18:08:20 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
// Authentication owns credential verification and bounded Principal issuance.
|
||||
import { createHmac, timingSafeEqual } from 'node:crypto';
|
||||
import {
|
||||
ApiCredentialUnavailableError,
|
||||
LEGACY_API_CREDENTIAL_PEPPER_KEY_ID,
|
||||
assertApiCredentialPepperKeyId,
|
||||
normalizeApiCredentialRecord,
|
||||
type ApiCredentialRepository,
|
||||
} from '@qinglong/runtime-core/api-credential';
|
||||
import {
|
||||
normalizeSecurityPrincipal,
|
||||
type SecurityPrincipal,
|
||||
} from '@qinglong/runtime-core/security';
|
||||
import type { ClusterControlAdmissionMetadata } from '../transport/httpSurface';
|
||||
import type { ClusterControlRequestAuthenticator } from '../transport/admissionPipeline';
|
||||
|
||||
export const CLUSTER_CONTROL_API_CREDENTIAL_LIMITS = Object.freeze({
|
||||
principalTtlMs: 60_000,
|
||||
maxPrincipalTtlMs: 300_000,
|
||||
secretBytes: 32,
|
||||
});
|
||||
|
||||
export class ClusterControlApiCredentialConfigurationError extends TypeError {
|
||||
constructor(message: string) {
|
||||
super(
|
||||
`Cluster-control API credential configuration is invalid: ${message}`,
|
||||
);
|
||||
this.name = 'ClusterControlApiCredentialConfigurationError';
|
||||
}
|
||||
}
|
||||
|
||||
export class ClusterControlApiCredentialUnavailableError extends Error {
|
||||
readonly code = 'CLUSTER_CONTROL_API_CREDENTIAL_UNAVAILABLE';
|
||||
|
||||
constructor() {
|
||||
super('Cluster-control API credential authentication is unavailable');
|
||||
this.name = 'ClusterControlApiCredentialUnavailableError';
|
||||
}
|
||||
}
|
||||
|
||||
export interface ClusterControlApiCredentialAuthenticatorOptions {
|
||||
readonly principalTtlMs?: number;
|
||||
readonly pepperKeyId?: string;
|
||||
readonly now?: () => number;
|
||||
}
|
||||
|
||||
const AUTHORIZATION_PATTERN =
|
||||
/^Bearer ql3c_([A-Za-z0-9][A-Za-z0-9._:-]{0,63})_([A-Za-z0-9_-]{43})$/;
|
||||
const PEPPER_PATTERN = /^[A-Za-z0-9_-]{43}$/;
|
||||
const DIGEST_DOMAIN = Buffer.from('qinglong-api-credential-v1\0', 'utf8');
|
||||
|
||||
function decodeSecret(name: string, value: string): Buffer {
|
||||
if (typeof value !== 'string' || !PEPPER_PATTERN.test(value)) {
|
||||
throw new ClusterControlApiCredentialConfigurationError(
|
||||
`${name} must be canonical base64url for 32 bytes`,
|
||||
);
|
||||
}
|
||||
const decoded = Buffer.from(value, 'base64url');
|
||||
if (
|
||||
decoded.byteLength !== CLUSTER_CONTROL_API_CREDENTIAL_LIMITS.secretBytes ||
|
||||
decoded.toString('base64url') !== value
|
||||
) {
|
||||
throw new ClusterControlApiCredentialConfigurationError(
|
||||
`${name} must be canonical base64url for 32 bytes`,
|
||||
);
|
||||
}
|
||||
return decoded;
|
||||
}
|
||||
|
||||
export function assertClusterControlApiCredentialPepper(value: string): void {
|
||||
const decoded = decodeSecret('pepper', value);
|
||||
decoded.fill(0);
|
||||
}
|
||||
|
||||
function principalTtl(value: number | undefined): number {
|
||||
const resolved =
|
||||
value ?? CLUSTER_CONTROL_API_CREDENTIAL_LIMITS.principalTtlMs;
|
||||
if (
|
||||
!Number.isSafeInteger(resolved) ||
|
||||
resolved < 1_000 ||
|
||||
resolved > CLUSTER_CONTROL_API_CREDENTIAL_LIMITS.maxPrincipalTtlMs
|
||||
) {
|
||||
throw new ClusterControlApiCredentialConfigurationError(
|
||||
'principalTtlMs is invalid',
|
||||
);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function digest(pepper: Buffer, credentialId: string, secret: Buffer): Buffer {
|
||||
return createHmac('sha256', pepper)
|
||||
.update(DIGEST_DOMAIN)
|
||||
.update(credentialId, 'utf8')
|
||||
.update('\0', 'utf8')
|
||||
.update(secret)
|
||||
.digest();
|
||||
}
|
||||
|
||||
export function apiCredentialSecretDigest(
|
||||
pepperBase64Url: string,
|
||||
credentialId: string,
|
||||
secretBase64Url: string,
|
||||
): string {
|
||||
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/.test(credentialId)) {
|
||||
throw new ClusterControlApiCredentialConfigurationError(
|
||||
'credentialId is invalid',
|
||||
);
|
||||
}
|
||||
const pepper = decodeSecret('pepper', pepperBase64Url);
|
||||
const secret = decodeSecret('secret', secretBase64Url);
|
||||
let result: Buffer | undefined;
|
||||
try {
|
||||
result = digest(pepper, credentialId, secret);
|
||||
return result.toString('hex');
|
||||
} finally {
|
||||
result?.fill(0);
|
||||
pepper.fill(0);
|
||||
secret.fill(0);
|
||||
}
|
||||
}
|
||||
|
||||
function parseAuthorization(
|
||||
metadata: ClusterControlAdmissionMetadata,
|
||||
): { readonly credentialId: string; readonly secret: Buffer } | null {
|
||||
const value = metadata.headers.authorization;
|
||||
if (typeof value !== 'string') return null;
|
||||
const match = AUTHORIZATION_PATTERN.exec(value);
|
||||
if (!match) return null;
|
||||
let secret: Buffer;
|
||||
try {
|
||||
secret = decodeSecret('bearer secret', match[2]!);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return Object.freeze({ credentialId: match[1]!, secret });
|
||||
}
|
||||
|
||||
export function createClusterControlApiCredentialAuthenticator(
|
||||
repository: ApiCredentialRepository,
|
||||
pepperBase64Url: string,
|
||||
options: ClusterControlApiCredentialAuthenticatorOptions = {},
|
||||
): ClusterControlRequestAuthenticator {
|
||||
if (!repository || typeof repository.resolve !== 'function') {
|
||||
throw new ClusterControlApiCredentialConfigurationError(
|
||||
'repository is invalid',
|
||||
);
|
||||
}
|
||||
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
||||
throw new ClusterControlApiCredentialConfigurationError(
|
||||
'options are invalid',
|
||||
);
|
||||
}
|
||||
const keys = Object.keys(options);
|
||||
if (
|
||||
keys.some(
|
||||
(key) =>
|
||||
key !== 'principalTtlMs' && key !== 'pepperKeyId' && key !== 'now',
|
||||
)
|
||||
) {
|
||||
throw new ClusterControlApiCredentialConfigurationError(
|
||||
'options shape is invalid',
|
||||
);
|
||||
}
|
||||
if (options.now !== undefined && typeof options.now !== 'function') {
|
||||
throw new ClusterControlApiCredentialConfigurationError('now is invalid');
|
||||
}
|
||||
const pepperKeyId =
|
||||
options.pepperKeyId ?? LEGACY_API_CREDENTIAL_PEPPER_KEY_ID;
|
||||
try {
|
||||
assertApiCredentialPepperKeyId(pepperKeyId);
|
||||
} catch {
|
||||
throw new ClusterControlApiCredentialConfigurationError(
|
||||
'pepperKeyId is invalid',
|
||||
);
|
||||
}
|
||||
const pepper = decodeSecret('pepper', pepperBase64Url);
|
||||
const ttlMs = principalTtl(options.principalTtlMs);
|
||||
const now = options.now ?? Date.now;
|
||||
|
||||
return Object.freeze({
|
||||
async authenticate(
|
||||
metadata: ClusterControlAdmissionMetadata,
|
||||
): Promise<Readonly<SecurityPrincipal> | null> {
|
||||
const parsed = parseAuthorization(metadata);
|
||||
if (!parsed) return null;
|
||||
const presentedDigest = digest(
|
||||
pepper,
|
||||
parsed.credentialId,
|
||||
parsed.secret,
|
||||
);
|
||||
parsed.secret.fill(0);
|
||||
let candidate;
|
||||
try {
|
||||
candidate = await repository.resolve(parsed.credentialId);
|
||||
} catch (error) {
|
||||
presentedDigest.fill(0);
|
||||
if (error instanceof ApiCredentialUnavailableError) {
|
||||
throw new ClusterControlApiCredentialUnavailableError();
|
||||
}
|
||||
throw new ClusterControlApiCredentialUnavailableError();
|
||||
}
|
||||
if (metadata.signal.aborted) {
|
||||
presentedDigest.fill(0);
|
||||
throw new ClusterControlApiCredentialUnavailableError();
|
||||
}
|
||||
let record;
|
||||
try {
|
||||
record = candidate ? normalizeApiCredentialRecord(candidate) : null;
|
||||
} catch {
|
||||
presentedDigest.fill(0);
|
||||
throw new ClusterControlApiCredentialUnavailableError();
|
||||
}
|
||||
if (record && record.pepperKeyId !== pepperKeyId) {
|
||||
presentedDigest.fill(0);
|
||||
throw new ClusterControlApiCredentialUnavailableError();
|
||||
}
|
||||
const storedDigest = record
|
||||
? Buffer.from(record.secretDigest, 'hex')
|
||||
: Buffer.alloc(32);
|
||||
const matches = timingSafeEqual(presentedDigest, storedDigest);
|
||||
presentedDigest.fill(0);
|
||||
storedDigest.fill(0);
|
||||
if (!record || !matches) return null;
|
||||
const nowMs = now();
|
||||
if (
|
||||
!Number.isSafeInteger(nowMs) ||
|
||||
nowMs < 0 ||
|
||||
record.state !== 'active' ||
|
||||
record.subjectStatus !== 'active' ||
|
||||
record.notBeforeAtMs > nowMs ||
|
||||
record.expiresAtMs <= nowMs
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const expiresAtMs = Math.min(record.expiresAtMs, nowMs + ttlMs);
|
||||
try {
|
||||
return normalizeSecurityPrincipal(
|
||||
{
|
||||
subject: record.subject,
|
||||
authenticationId: `api_credential:${record.credentialId}:${record.version}`,
|
||||
authenticatedAtMs: nowMs,
|
||||
expiresAtMs,
|
||||
assurance:
|
||||
record.subject.type === 'user' ? 'single_factor' : 'service',
|
||||
},
|
||||
nowMs,
|
||||
);
|
||||
} catch {
|
||||
throw new ClusterControlApiCredentialUnavailableError();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
// Authentication owns its bounded pre-body overload shield.
|
||||
import { createHmac, randomBytes } from 'node:crypto';
|
||||
import { performance } from 'node:perf_hooks';
|
||||
|
||||
export interface ClusterControlAuthenticationShieldOptions {
|
||||
readonly windowMs: number;
|
||||
readonly maxRequestsPerPeer: number;
|
||||
readonly maxRequestsGlobal: number;
|
||||
readonly maxTrackedPeers: number;
|
||||
readonly now?: () => number;
|
||||
}
|
||||
|
||||
export type ClusterControlAuthenticationShieldRejectionReason =
|
||||
| 'capacity'
|
||||
| 'clock'
|
||||
| 'global'
|
||||
| 'peer';
|
||||
|
||||
export type ClusterControlAuthenticationShieldResult =
|
||||
| {
|
||||
readonly allowed: true;
|
||||
/**
|
||||
* Returns this provisional attempt budget after the pre-body admission
|
||||
* preflight has succeeded. Idempotent and scoped to the exact windows
|
||||
* consumed by this result.
|
||||
*/
|
||||
refund(): void;
|
||||
}
|
||||
| {
|
||||
readonly allowed: false;
|
||||
readonly reason: ClusterControlAuthenticationShieldRejectionReason;
|
||||
readonly retryAfterMs: number;
|
||||
};
|
||||
|
||||
export interface ClusterControlAuthenticationShield {
|
||||
consume(
|
||||
peerAddress: string | undefined,
|
||||
): ClusterControlAuthenticationShieldResult;
|
||||
close(): void;
|
||||
}
|
||||
|
||||
interface PeerWindow {
|
||||
readonly startedAt: number;
|
||||
readonly count: number;
|
||||
}
|
||||
|
||||
const FINGERPRINT_KEY_BYTES = 32;
|
||||
const MAX_PEER_ADDRESS_BYTES = 128;
|
||||
const MAX_PRUNE_PER_ATTEMPT = 64;
|
||||
const UNKNOWN_PEER = '<unknown-transport-peer>';
|
||||
|
||||
function normalizedPeerAddress(peerAddress: string | undefined): string {
|
||||
if (
|
||||
typeof peerAddress !== 'string' ||
|
||||
peerAddress.length === 0 ||
|
||||
Buffer.byteLength(peerAddress) > MAX_PEER_ADDRESS_BYTES ||
|
||||
/[\0\r\n]/.test(peerAddress)
|
||||
) {
|
||||
return UNKNOWN_PEER;
|
||||
}
|
||||
return peerAddress;
|
||||
}
|
||||
|
||||
function remainingWindow(
|
||||
now: number,
|
||||
startedAt: number,
|
||||
windowMs: number,
|
||||
): number {
|
||||
return Math.max(1, Math.ceil(windowMs - (now - startedAt)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a process-local overload shield for authentication attempts. It is
|
||||
* deliberately not an authorization or distributed quota authority: every
|
||||
* cluster-control replica owns a bounded, disposable window.
|
||||
*/
|
||||
export function createClusterControlAuthenticationShield(
|
||||
options: ClusterControlAuthenticationShieldOptions,
|
||||
): ClusterControlAuthenticationShield {
|
||||
const now = options.now ?? (() => performance.now());
|
||||
const fingerprintKey = randomBytes(FINGERPRINT_KEY_BYTES);
|
||||
const peers = new Map<string, PeerWindow>();
|
||||
let globalWindow: PeerWindow | undefined;
|
||||
let lastNow = 0;
|
||||
let closed = false;
|
||||
|
||||
const fingerprint = (peerAddress: string | undefined): string =>
|
||||
createHmac('sha256', fingerprintKey)
|
||||
.update('qinglong.cluster-control.authentication-peer\0')
|
||||
.update(normalizedPeerAddress(peerAddress))
|
||||
.digest('base64url');
|
||||
|
||||
const pruneExpired = (currentTime: number): void => {
|
||||
let scanned = 0;
|
||||
for (const [key, window] of peers) {
|
||||
if (scanned >= MAX_PRUNE_PER_ATTEMPT) return;
|
||||
scanned += 1;
|
||||
if (currentTime - window.startedAt >= options.windowMs) {
|
||||
peers.delete(key);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const accepted = (
|
||||
peer: string,
|
||||
peerStartedAt: number,
|
||||
globalStartedAt: number,
|
||||
): ClusterControlAuthenticationShieldResult => {
|
||||
let completed = false;
|
||||
return Object.freeze({
|
||||
allowed: true as const,
|
||||
refund() {
|
||||
if (completed || closed) return;
|
||||
completed = true;
|
||||
if (
|
||||
globalWindow?.startedAt === globalStartedAt &&
|
||||
globalWindow.count > 0
|
||||
) {
|
||||
globalWindow = {
|
||||
startedAt: globalWindow.startedAt,
|
||||
count: globalWindow.count - 1,
|
||||
};
|
||||
}
|
||||
const currentPeerWindow = peers.get(peer);
|
||||
if (
|
||||
currentPeerWindow?.startedAt === peerStartedAt &&
|
||||
currentPeerWindow.count > 0
|
||||
) {
|
||||
if (currentPeerWindow.count === 1) peers.delete(peer);
|
||||
else {
|
||||
peers.set(peer, {
|
||||
startedAt: currentPeerWindow.startedAt,
|
||||
count: currentPeerWindow.count - 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
consume(peerAddress) {
|
||||
if (closed) {
|
||||
return Object.freeze({
|
||||
allowed: false,
|
||||
reason: 'clock',
|
||||
retryAfterMs: options.windowMs,
|
||||
});
|
||||
}
|
||||
|
||||
let currentTime: number;
|
||||
try {
|
||||
currentTime = now();
|
||||
} catch {
|
||||
return Object.freeze({
|
||||
allowed: false,
|
||||
reason: 'clock',
|
||||
retryAfterMs: options.windowMs,
|
||||
});
|
||||
}
|
||||
if (
|
||||
!Number.isFinite(currentTime) ||
|
||||
currentTime < 0 ||
|
||||
currentTime < lastNow
|
||||
) {
|
||||
return Object.freeze({
|
||||
allowed: false,
|
||||
reason: 'clock',
|
||||
retryAfterMs: options.windowMs,
|
||||
});
|
||||
}
|
||||
lastNow = currentTime;
|
||||
|
||||
if (
|
||||
!globalWindow ||
|
||||
currentTime - globalWindow.startedAt >= options.windowMs
|
||||
) {
|
||||
globalWindow = { startedAt: currentTime, count: 0 };
|
||||
}
|
||||
if (globalWindow.count >= options.maxRequestsGlobal) {
|
||||
return Object.freeze({
|
||||
allowed: false,
|
||||
reason: 'global',
|
||||
retryAfterMs: remainingWindow(
|
||||
currentTime,
|
||||
globalWindow.startedAt,
|
||||
options.windowMs,
|
||||
),
|
||||
});
|
||||
}
|
||||
globalWindow = {
|
||||
startedAt: globalWindow.startedAt,
|
||||
count: globalWindow.count + 1,
|
||||
};
|
||||
|
||||
const peer = fingerprint(peerAddress);
|
||||
let peerWindow = peers.get(peer);
|
||||
if (
|
||||
peerWindow &&
|
||||
currentTime - peerWindow.startedAt >= options.windowMs
|
||||
) {
|
||||
peers.delete(peer);
|
||||
peerWindow = undefined;
|
||||
}
|
||||
if (peerWindow) {
|
||||
if (peerWindow.count >= options.maxRequestsPerPeer) {
|
||||
return Object.freeze({
|
||||
allowed: false,
|
||||
reason: 'peer',
|
||||
retryAfterMs: remainingWindow(
|
||||
currentTime,
|
||||
peerWindow.startedAt,
|
||||
options.windowMs,
|
||||
),
|
||||
});
|
||||
}
|
||||
peers.delete(peer);
|
||||
peers.set(peer, {
|
||||
startedAt: peerWindow.startedAt,
|
||||
count: peerWindow.count + 1,
|
||||
});
|
||||
return accepted(peer, peerWindow.startedAt, globalWindow.startedAt);
|
||||
}
|
||||
|
||||
if (peers.size >= options.maxTrackedPeers) pruneExpired(currentTime);
|
||||
if (peers.size >= options.maxTrackedPeers) {
|
||||
return Object.freeze({
|
||||
allowed: false,
|
||||
reason: 'capacity',
|
||||
retryAfterMs: options.windowMs,
|
||||
});
|
||||
}
|
||||
peers.set(peer, { startedAt: currentTime, count: 1 });
|
||||
return accepted(peer, currentTime, globalWindow.startedAt);
|
||||
},
|
||||
close() {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
peers.clear();
|
||||
globalWindow = undefined;
|
||||
fingerprintKey.fill(0);
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user