mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:21:50 +08:00
983 lines
30 KiB
JavaScript
983 lines
30 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
'use strict';
|
|
|
|
const crypto = require('node:crypto');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const { spawnSync } = require('node:child_process');
|
|
const { TextDecoder } = require('node:util');
|
|
|
|
const {
|
|
ceremonyCommands,
|
|
validateWorkerCredentialManagementLiveCeremony,
|
|
} = require('./ql3-worker-credential-management-live-ceremony.cjs');
|
|
|
|
const FIXTURE =
|
|
'qinglong/worker-credential-management-durable-audit-evidence@v1';
|
|
const MAX_FILE_BYTES = 1024 * 1024;
|
|
const CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
|
|
const TOKEN_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/;
|
|
const SERVICE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/;
|
|
const UUID_V4_PATTERN =
|
|
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
const SHA256_PATTERN = /^sha256:[a-f0-9]{64}$/;
|
|
const HEX_DIGEST_PATTERN = /^[a-f0-9]{64}$/;
|
|
const MINIMUM_POSTGRES_VERSION_NUMBER = 180004;
|
|
const NEXT_POSTGRES_MAJOR_VERSION_NUMBER = 190000;
|
|
const TARGET_TABLES = Object.freeze([
|
|
'approval_requests',
|
|
'security_audit_events',
|
|
'worker_credential_management_plans',
|
|
]);
|
|
const PRIVILEGED_ROLES = Object.freeze([
|
|
'ql3_admin',
|
|
'ql3_migration',
|
|
'ql3_package_executor',
|
|
'ql3_package_manager',
|
|
'ql3_runtime',
|
|
'ql3_worker_credential_executor',
|
|
'ql3_worker_credential_manager',
|
|
'ql3_worker_ingress',
|
|
]);
|
|
const BANNED_KEYS = new Set([
|
|
'assertion',
|
|
'authorization',
|
|
'bearer',
|
|
'connectionstring',
|
|
'dsn',
|
|
'password',
|
|
'privatekey',
|
|
'secret',
|
|
'tlskey',
|
|
'token',
|
|
]);
|
|
|
|
class WorkerCredentialManagementDurableAuditEvidenceError extends Error {
|
|
constructor(message) {
|
|
super(
|
|
`Worker credential management durable audit evidence failed: ${message}`,
|
|
);
|
|
this.name = 'WorkerCredentialManagementDurableAuditEvidenceError';
|
|
}
|
|
}
|
|
|
|
function fail(message) {
|
|
throw new WorkerCredentialManagementDurableAuditEvidenceError(message);
|
|
}
|
|
|
|
function exactKeys(value, expected) {
|
|
return (
|
|
value !== null &&
|
|
typeof value === 'object' &&
|
|
!Array.isArray(value) &&
|
|
JSON.stringify(Object.keys(value).sort()) ===
|
|
JSON.stringify([...expected].sort())
|
|
);
|
|
}
|
|
|
|
function canonicalFile(filePath, label, privateFile = true) {
|
|
if (
|
|
typeof filePath !== 'string' ||
|
|
!path.isAbsolute(filePath) ||
|
|
filePath.length > 4096 ||
|
|
CONTROL_PATTERN.test(filePath)
|
|
) {
|
|
fail(`${label} path is invalid`);
|
|
}
|
|
let stat;
|
|
try {
|
|
stat = fs.lstatSync(filePath);
|
|
} catch {
|
|
fail(`${label} is unavailable`);
|
|
}
|
|
const uid = typeof process.getuid === 'function' ? process.getuid() : null;
|
|
if (
|
|
!stat.isFile() ||
|
|
stat.isSymbolicLink() ||
|
|
stat.size < 1 ||
|
|
stat.size > MAX_FILE_BYTES ||
|
|
fs.realpathSync(filePath) !== filePath ||
|
|
(privateFile && (uid === null || stat.uid !== uid)) ||
|
|
(stat.mode & (privateFile ? 0o077 : 0o022)) !== 0
|
|
) {
|
|
fail(`${label} must be one canonical bounded private regular file`);
|
|
}
|
|
return stat;
|
|
}
|
|
|
|
function readPrivateBuffer(filePath, label) {
|
|
const before = canonicalFile(filePath, label);
|
|
let descriptor = -1;
|
|
let bytes;
|
|
try {
|
|
descriptor = fs.openSync(
|
|
filePath,
|
|
fs.constants.O_RDONLY |
|
|
(fs.constants.O_CLOEXEC ?? 0) |
|
|
(fs.constants.O_NOFOLLOW ?? 0),
|
|
);
|
|
const opened = fs.fstatSync(descriptor);
|
|
if (
|
|
opened.dev !== before.dev ||
|
|
opened.ino !== before.ino ||
|
|
opened.uid !== before.uid ||
|
|
opened.mode !== before.mode ||
|
|
opened.size !== before.size
|
|
) {
|
|
fail(`${label} changed before it was opened`);
|
|
}
|
|
bytes = Buffer.alloc(opened.size);
|
|
let offset = 0;
|
|
while (offset < bytes.length) {
|
|
const count = fs.readSync(
|
|
descriptor,
|
|
bytes,
|
|
offset,
|
|
bytes.length - offset,
|
|
offset,
|
|
);
|
|
if (count < 1) fail(`${label} could not be read completely`);
|
|
offset += count;
|
|
}
|
|
const after = fs.fstatSync(descriptor);
|
|
if (
|
|
after.dev !== opened.dev ||
|
|
after.ino !== opened.ino ||
|
|
after.uid !== opened.uid ||
|
|
after.mode !== opened.mode ||
|
|
after.size !== opened.size
|
|
) {
|
|
fail(`${label} changed while it was read`);
|
|
}
|
|
return Buffer.from(bytes);
|
|
} catch (error) {
|
|
if (error instanceof WorkerCredentialManagementDurableAuditEvidenceError) {
|
|
throw error;
|
|
}
|
|
fail(`${label} could not be read safely`);
|
|
} finally {
|
|
bytes?.fill(0);
|
|
if (descriptor >= 0) fs.closeSync(descriptor);
|
|
}
|
|
}
|
|
|
|
function jsonFromBytes(bytes, label) {
|
|
try {
|
|
return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
} catch {
|
|
fail(`${label} must contain UTF-8 JSON`);
|
|
}
|
|
}
|
|
|
|
function rawDigest(value) {
|
|
return `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`;
|
|
}
|
|
|
|
function digest(domain, value) {
|
|
return `sha256:${crypto
|
|
.createHash('sha256')
|
|
.update(domain)
|
|
.update('\0')
|
|
.update(value)
|
|
.digest('hex')}`;
|
|
}
|
|
|
|
function sqlLiteral(value, label) {
|
|
if (typeof value !== 'string' || !TOKEN_PATTERN.test(value)) {
|
|
fail(`${label} is invalid`);
|
|
}
|
|
return `'${value}'`;
|
|
}
|
|
|
|
function validateCeremony(value) {
|
|
const commands = ceremonyCommands(value, (command) => command);
|
|
const auditIds = [
|
|
value.approvalAuditEventId,
|
|
value.requesterDecisionAuditEventId,
|
|
value.reviewerDecisionAuditEventId,
|
|
];
|
|
if (
|
|
!auditIds.every((entry) => UUID_V4_PATTERN.test(entry)) ||
|
|
new Set(auditIds).size !== auditIds.length
|
|
) {
|
|
fail('ceremony audit event identities must be distinct UUIDv4 values');
|
|
}
|
|
return Object.freeze({ value, commands });
|
|
}
|
|
|
|
function databaseSql(ceremony) {
|
|
const value = ceremony.value;
|
|
const actionRef = sqlLiteral(value.planRequest.actionRef, 'actionRef');
|
|
const projectId = sqlLiteral(
|
|
value.planRequest.authorityProjectId,
|
|
'authorityProjectId',
|
|
);
|
|
const approvalId = sqlLiteral(value.approvalRequestId, 'approvalRequestId');
|
|
const approvalAudit = sqlLiteral(
|
|
value.approvalAuditEventId,
|
|
'approvalAuditEventId',
|
|
);
|
|
const requesterAudit = sqlLiteral(
|
|
value.requesterDecisionAuditEventId,
|
|
'requesterDecisionAuditEventId',
|
|
);
|
|
const reviewerAudit = sqlLiteral(
|
|
value.reviewerDecisionAuditEventId,
|
|
'reviewerDecisionAuditEventId',
|
|
);
|
|
const roles = PRIVILEGED_ROLES.map((role) => `'${role}'`).join(', ');
|
|
return `
|
|
BEGIN TRANSACTION READ ONLY;
|
|
WITH expected_audit(event_id, kind) AS (
|
|
VALUES
|
|
(${approvalAudit}::uuid, 'proposal'::text),
|
|
(${reviewerAudit}::uuid, 'reviewer_decision'::text)
|
|
), role_facts AS (
|
|
SELECT
|
|
roles.rolname,
|
|
roles.rolcanlogin,
|
|
roles.rolsuper,
|
|
roles.rolcreatedb,
|
|
roles.rolcreaterole,
|
|
roles.rolreplication,
|
|
roles.rolbypassrls
|
|
FROM pg_catalog.pg_roles AS roles
|
|
WHERE roles.rolname = current_user
|
|
), selectable AS (
|
|
SELECT coalesce(json_agg(tables.tablename ORDER BY tables.tablename), '[]'::json) AS names
|
|
FROM pg_catalog.pg_tables AS tables
|
|
WHERE tables.schemaname = 'ql3'
|
|
AND has_table_privilege(
|
|
current_user,
|
|
format('%I.%I', tables.schemaname, tables.tablename),
|
|
'SELECT'
|
|
)
|
|
), writable AS (
|
|
SELECT coalesce(json_agg(tables.tablename ORDER BY tables.tablename), '[]'::json) AS names
|
|
FROM pg_catalog.pg_tables AS tables
|
|
WHERE tables.schemaname = 'ql3'
|
|
AND (
|
|
has_table_privilege(current_user, format('%I.%I', tables.schemaname, tables.tablename), 'INSERT') OR
|
|
has_table_privilege(current_user, format('%I.%I', tables.schemaname, tables.tablename), 'UPDATE') OR
|
|
has_table_privilege(current_user, format('%I.%I', tables.schemaname, tables.tablename), 'DELETE') OR
|
|
has_table_privilege(current_user, format('%I.%I', tables.schemaname, tables.tablename), 'TRUNCATE') OR
|
|
has_table_privilege(current_user, format('%I.%I', tables.schemaname, tables.tablename), 'REFERENCES') OR
|
|
has_table_privilege(current_user, format('%I.%I', tables.schemaname, tables.tablename), 'TRIGGER')
|
|
)
|
|
), privileged_memberships AS (
|
|
SELECT coalesce(json_agg(role_name ORDER BY role_name), '[]'::json) AS names
|
|
FROM unnest(ARRAY[${roles}]::text[]) AS role_name
|
|
WHERE pg_has_role(current_user, role_name, 'MEMBER')
|
|
OR pg_has_role(current_user, role_name, 'SET')
|
|
), plan_row AS (
|
|
SELECT
|
|
plan.action_ref,
|
|
plan.authority_project_id,
|
|
plan.action,
|
|
plan.plan_digest,
|
|
plan.preview_digest,
|
|
plan.requested_by_type,
|
|
plan.requested_by_id,
|
|
plan.planned_at_ms
|
|
FROM ql3.worker_credential_management_plans AS plan
|
|
WHERE plan.action_ref = ${actionRef}
|
|
AND plan.authority_project_id = ${projectId}
|
|
), approval_row AS (
|
|
SELECT
|
|
approval.request_id,
|
|
approval.project_id,
|
|
approval.version,
|
|
approval.state,
|
|
approval.action_type,
|
|
approval.action_ref,
|
|
approval.action_digest,
|
|
approval.preview_digest,
|
|
approval.requested_by_type,
|
|
approval.requested_by_id,
|
|
approval.decision_id,
|
|
approval.consumption_id,
|
|
approval.dispatch_id,
|
|
approval.request_json -> 'decidedBy' ->> 'type' AS decided_by_type,
|
|
approval.request_json -> 'decidedBy' ->> 'id' AS decided_by_id,
|
|
approval.request_json ->> 'decisionReasonCode' AS decision_reason_code,
|
|
approval.request_json ->> 'decisionAuthenticationId' AS decision_authentication_id
|
|
FROM ql3.approval_requests AS approval
|
|
WHERE approval.request_id = ${approvalId}
|
|
), audit_rows AS (
|
|
SELECT
|
|
expected.kind,
|
|
audit.event_id,
|
|
audit.request_id,
|
|
audit.operation_id,
|
|
audit.project_id,
|
|
audit.subject_type,
|
|
audit.subject_id,
|
|
audit.authentication_id,
|
|
audit.outcome,
|
|
audit.reasons,
|
|
audit.project_version,
|
|
audit.binding_version,
|
|
audit.occurred_at_ms
|
|
FROM expected_audit AS expected
|
|
JOIN ql3.security_audit_events AS audit
|
|
ON audit.event_id = expected.event_id
|
|
)
|
|
SELECT json_build_object(
|
|
'serverVersionNumber', current_setting('server_version_num')::integer,
|
|
'transactionReadOnly', current_setting('transaction_read_only')::boolean,
|
|
'role', (
|
|
SELECT json_build_object(
|
|
'name', role_facts.rolname,
|
|
'canLogin', role_facts.rolcanlogin,
|
|
'superuser', role_facts.rolsuper,
|
|
'createDatabase', role_facts.rolcreatedb,
|
|
'createRole', role_facts.rolcreaterole,
|
|
'replication', role_facts.rolreplication,
|
|
'bypassRls', role_facts.rolbypassrls,
|
|
'schemaUsage', has_schema_privilege(current_user, 'ql3', 'USAGE'),
|
|
'selectableTables', selectable.names,
|
|
'writableTables', writable.names,
|
|
'privilegedMemberships', privileged_memberships.names
|
|
)
|
|
FROM role_facts, selectable, writable, privileged_memberships
|
|
),
|
|
'plan', (
|
|
SELECT row_to_json(plan_row) FROM plan_row
|
|
),
|
|
'approval', (
|
|
SELECT row_to_json(approval_row) FROM approval_row
|
|
),
|
|
'audits', (
|
|
SELECT coalesce(json_agg(row_to_json(audit_rows) ORDER BY audit_rows.occurred_at_ms, audit_rows.kind), '[]'::json)
|
|
FROM audit_rows
|
|
),
|
|
'requesterDecisionAuditRows', (
|
|
SELECT count(*)::integer
|
|
FROM ql3.security_audit_events
|
|
WHERE event_id = ${requesterAudit}::uuid
|
|
)
|
|
)::text;
|
|
COMMIT;
|
|
`.trim();
|
|
}
|
|
|
|
function jsonOutput(result) {
|
|
if (!result || result.status !== 0) {
|
|
fail(
|
|
`PostgreSQL evidence query failed with status ${String(result?.status)}`,
|
|
);
|
|
}
|
|
try {
|
|
return JSON.parse(result.stdout.trim());
|
|
} catch {
|
|
fail('PostgreSQL evidence query did not return one JSON value');
|
|
}
|
|
}
|
|
|
|
function defaultRunPsql(serviceFile, args, sql) {
|
|
return spawnSync('psql', args, {
|
|
encoding: 'utf8',
|
|
input: sql,
|
|
timeout: 20_000,
|
|
maxBuffer: 2 * 1024 * 1024,
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
LANG: process.env.LANG ?? 'C.UTF-8',
|
|
LC_ALL: process.env.LC_ALL ?? 'C.UTF-8',
|
|
PGSERVICEFILE: serviceFile,
|
|
PGAPPNAME: 'ql3-worker-management-evidence',
|
|
PGOPTIONS:
|
|
'-c default_transaction_read_only=on -c statement_timeout=10000 -c lock_timeout=2000 -c idle_in_transaction_session_timeout=5000',
|
|
},
|
|
});
|
|
}
|
|
|
|
function collectDatabaseSnapshot(ceremony, options, runPsql = defaultRunPsql) {
|
|
const result = runPsql(
|
|
options.pgServiceFile,
|
|
[
|
|
'--no-psqlrc',
|
|
'--quiet',
|
|
'--no-align',
|
|
'--tuples-only',
|
|
'--set=ON_ERROR_STOP=1',
|
|
`--dbname=service=${options.pgService}`,
|
|
'--file=-',
|
|
],
|
|
databaseSql(ceremony),
|
|
);
|
|
return jsonOutput(result);
|
|
}
|
|
|
|
function hashSubject(value) {
|
|
return digest('qinglong3.worker-management.subject.v1', value);
|
|
}
|
|
|
|
function auditEvidence(row, expected) {
|
|
if (
|
|
!row ||
|
|
row.kind !== expected.kind ||
|
|
row.event_id !== expected.eventId ||
|
|
row.request_id !== expected.approvalId ||
|
|
row.operation_id !== expected.operationId ||
|
|
row.project_id !== expected.projectId ||
|
|
row.subject_type !== 'user' ||
|
|
row.subject_id !== expected.subjectId ||
|
|
typeof row.authentication_id !== 'string' ||
|
|
!TOKEN_PATTERN.test(row.authentication_id) ||
|
|
row.outcome !== expected.outcome ||
|
|
JSON.stringify(row.reasons) !==
|
|
JSON.stringify(['worker_credential_review']) ||
|
|
!Number.isSafeInteger(row.project_version) ||
|
|
row.project_version < 1 ||
|
|
(row.binding_version !== null &&
|
|
(!Number.isSafeInteger(row.binding_version) ||
|
|
row.binding_version < 1)) ||
|
|
!Number.isSafeInteger(Number(row.occurred_at_ms)) ||
|
|
Number(row.occurred_at_ms) < 0
|
|
) {
|
|
fail(`${expected.kind} durable audit row is invalid`);
|
|
}
|
|
return Object.freeze({
|
|
kind: row.kind,
|
|
eventIdSha256: digest(
|
|
'qinglong3.worker-management.audit-event.v1',
|
|
row.event_id,
|
|
),
|
|
operationId: row.operation_id,
|
|
outcome: row.outcome,
|
|
subjectSha256: hashSubject(row.subject_id),
|
|
authenticationIdSha256: digest(
|
|
'qinglong3.worker-management.authentication-id.v1',
|
|
row.authentication_id,
|
|
),
|
|
reasonCode: row.reasons[0],
|
|
policyFencePresent: true,
|
|
});
|
|
}
|
|
|
|
function buildReport({
|
|
ceremony,
|
|
ceremonyBytes,
|
|
ceremonyReport,
|
|
reportBytes,
|
|
snapshot,
|
|
nowMs,
|
|
}) {
|
|
const value = ceremony.value;
|
|
const plan = snapshot?.plan;
|
|
const approval = snapshot?.approval;
|
|
const role = snapshot?.role;
|
|
if (
|
|
!Number.isSafeInteger(snapshot?.serverVersionNumber) ||
|
|
snapshot.serverVersionNumber < MINIMUM_POSTGRES_VERSION_NUMBER ||
|
|
snapshot.serverVersionNumber >= NEXT_POSTGRES_MAJOR_VERSION_NUMBER ||
|
|
snapshot?.transactionReadOnly !== true ||
|
|
!role ||
|
|
typeof role.name !== 'string' ||
|
|
!TOKEN_PATTERN.test(role.name) ||
|
|
role.canLogin !== true ||
|
|
role.superuser !== false ||
|
|
role.createDatabase !== false ||
|
|
role.createRole !== false ||
|
|
role.replication !== false ||
|
|
role.bypassRls !== false ||
|
|
role.schemaUsage !== true ||
|
|
JSON.stringify(role.selectableTables) !== JSON.stringify(TARGET_TABLES) ||
|
|
!Array.isArray(role.writableTables) ||
|
|
role.writableTables.length !== 0 ||
|
|
!Array.isArray(role.privilegedMemberships) ||
|
|
role.privilegedMemberships.length !== 0
|
|
) {
|
|
fail('PostgreSQL evidence role is not an exact read-only authority');
|
|
}
|
|
if (
|
|
!plan ||
|
|
plan.action_ref !== value.planRequest.actionRef ||
|
|
plan.authority_project_id !== value.planRequest.authorityProjectId ||
|
|
plan.action !== value.planRequest.action ||
|
|
plan.plan_digest !== ceremonyReport.ceremony.planDigest ||
|
|
plan.preview_digest !== ceremonyReport.ceremony.previewDigest ||
|
|
plan.requested_by_type !== 'user' ||
|
|
hashSubject(plan.requested_by_id) !==
|
|
ceremonyReport.identity.requesterSubjectSha256
|
|
) {
|
|
fail('durable management plan does not match the ceremony');
|
|
}
|
|
if (
|
|
!approval ||
|
|
approval.request_id !== value.approvalRequestId ||
|
|
approval.project_id !== value.planRequest.authorityProjectId ||
|
|
approval.version !== 2 ||
|
|
approval.state !== 'approved' ||
|
|
approval.action_type !==
|
|
`worker_credential.delivery.${value.planRequest.action}` ||
|
|
approval.action_ref !== value.planRequest.actionRef ||
|
|
approval.action_digest !== plan.plan_digest ||
|
|
approval.preview_digest !== plan.preview_digest ||
|
|
approval.requested_by_type !== 'user' ||
|
|
hashSubject(approval.requested_by_id) !==
|
|
ceremonyReport.identity.requesterSubjectSha256 ||
|
|
approval.decision_id !== value.reviewerDecisionId ||
|
|
approval.consumption_id !== null ||
|
|
approval.dispatch_id !== null ||
|
|
approval.decided_by_type !== 'user' ||
|
|
hashSubject(approval.decided_by_id) !==
|
|
ceremonyReport.identity.reviewerSubjectSha256 ||
|
|
approval.decision_reason_code !== value.decisionReasonCode ||
|
|
typeof approval.decision_authentication_id !== 'string' ||
|
|
!TOKEN_PATTERN.test(approval.decision_authentication_id)
|
|
) {
|
|
fail('durable approval does not match the reviewed ceremony');
|
|
}
|
|
if (
|
|
snapshot.requesterDecisionAuditRows !== 0 ||
|
|
!Array.isArray(snapshot.audits) ||
|
|
snapshot.audits.length !== 2
|
|
) {
|
|
fail('durable audit cardinality does not prove self-decision rejection');
|
|
}
|
|
const expected = new Map([
|
|
[
|
|
'proposal',
|
|
{
|
|
kind: 'proposal',
|
|
eventId: value.approvalAuditEventId,
|
|
approvalId: value.approvalRequestId,
|
|
operationId: 'approval.request',
|
|
projectId: value.planRequest.authorityProjectId,
|
|
subjectId: plan.requested_by_id,
|
|
outcome: 'approval_required',
|
|
},
|
|
],
|
|
[
|
|
'reviewer_decision',
|
|
{
|
|
kind: 'reviewer_decision',
|
|
eventId: value.reviewerDecisionAuditEventId,
|
|
approvalId: value.approvalRequestId,
|
|
operationId: 'approval.decide',
|
|
projectId: value.planRequest.authorityProjectId,
|
|
subjectId: approval.decided_by_id,
|
|
outcome: 'allowed',
|
|
},
|
|
],
|
|
]);
|
|
const auditRows = snapshot.audits.map((row) => {
|
|
const expectedRow = expected.get(row?.kind);
|
|
if (!expectedRow) fail('durable audit kind is invalid');
|
|
return auditEvidence(row, expectedRow);
|
|
});
|
|
if (
|
|
new Set(snapshot.audits.map((row) => row.kind)).size !== 2 ||
|
|
Number(snapshot.audits[0].occurred_at_ms) >
|
|
Number(snapshot.audits[1].occurred_at_ms)
|
|
) {
|
|
fail('durable audit ordering is invalid');
|
|
}
|
|
if (
|
|
digest(
|
|
'qinglong3.worker-management.action-ref.v1',
|
|
value.planRequest.actionRef,
|
|
) !== ceremonyReport.ceremony.actionRefSha256 ||
|
|
digest(
|
|
'qinglong3.worker-management.project.v1',
|
|
value.planRequest.authorityProjectId,
|
|
) !== ceremonyReport.ceremony.authorityProjectIdSha256
|
|
) {
|
|
fail('private ceremony does not match the ceremony report');
|
|
}
|
|
return Object.freeze({
|
|
schemaVersion: 1,
|
|
fixture: FIXTURE,
|
|
observedAt: new Date(nowMs).toISOString(),
|
|
source: Object.freeze({
|
|
ceremonyReportSha256: rawDigest(reportBytes),
|
|
ceremonyDefinitionSha256: rawDigest(ceremonyBytes),
|
|
ceremonyFixture: ceremonyReport.fixture,
|
|
}),
|
|
database: Object.freeze({
|
|
postgresVersionNumber: snapshot.serverVersionNumber,
|
|
transactionReadOnly: true,
|
|
roleNameSha256: digest(
|
|
'qinglong3.worker-management.evidence-role.v1',
|
|
role.name,
|
|
),
|
|
roleCanLogin: true,
|
|
privilegedAttributesDenied: true,
|
|
privilegedMembershipDenied: true,
|
|
exactTargetSelect: true,
|
|
ql3TableMutationDenied: true,
|
|
}),
|
|
durableState: Object.freeze({
|
|
actionRefSha256: ceremonyReport.ceremony.actionRefSha256,
|
|
authorityProjectIdSha256:
|
|
ceremonyReport.ceremony.authorityProjectIdSha256,
|
|
approvalRequestIdSha256: digest(
|
|
'qinglong3.worker-management.approval-request.v1',
|
|
value.approvalRequestId,
|
|
),
|
|
reviewerDecisionIdSha256: digest(
|
|
'qinglong3.worker-management.decision.v1',
|
|
value.reviewerDecisionId,
|
|
),
|
|
planDigest: plan.plan_digest,
|
|
previewDigest: plan.preview_digest,
|
|
approvalVersion: approval.version,
|
|
approvalState: approval.state,
|
|
requesterSubjectSha256: hashSubject(plan.requested_by_id),
|
|
reviewerSubjectSha256: hashSubject(approval.decided_by_id),
|
|
dispatchCreated: false,
|
|
approvalConsumed: false,
|
|
requesterSelfDecisionAuditAbsent: true,
|
|
auditRows: Object.freeze(auditRows),
|
|
}),
|
|
gates: Object.freeze({
|
|
sourceBound: true,
|
|
readOnlyEvidenceRole: true,
|
|
immutablePlanObserved: true,
|
|
reviewedApprovalObserved: true,
|
|
requesterSelfDecisionLeftNoAudit: true,
|
|
proposalAndDecisionAuditObserved: true,
|
|
noExecutionOrConsumption: true,
|
|
passed: true,
|
|
}),
|
|
});
|
|
}
|
|
|
|
function containsSensitiveMaterial(value, key = '') {
|
|
if (BANNED_KEYS.has(key.toLowerCase())) return true;
|
|
if (typeof value === 'string') {
|
|
return (
|
|
/-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/.test(value) ||
|
|
/postgres(?:ql)?:\/\/[^/\s]+:[^@\s]+@/i.test(value) ||
|
|
/\beyJ[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/.test(
|
|
value,
|
|
)
|
|
);
|
|
}
|
|
if (Array.isArray(value)) {
|
|
return value.some((entry) => containsSensitiveMaterial(entry));
|
|
}
|
|
if (value && typeof value === 'object') {
|
|
return Object.entries(value).some(([childKey, child]) =>
|
|
containsSensitiveMaterial(child, childKey),
|
|
);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function isIsoTime(value) {
|
|
return (
|
|
typeof value === 'string' &&
|
|
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/.test(value) &&
|
|
Number.isFinite(Date.parse(value))
|
|
);
|
|
}
|
|
|
|
function validateWorkerCredentialManagementDurableAuditEvidence(report) {
|
|
const findings = [];
|
|
const add = (code) => findings.push(Object.freeze({ code }));
|
|
if (
|
|
!exactKeys(report, [
|
|
'schemaVersion',
|
|
'fixture',
|
|
'observedAt',
|
|
'source',
|
|
'database',
|
|
'durableState',
|
|
'gates',
|
|
]) ||
|
|
report?.schemaVersion !== 1 ||
|
|
report?.fixture !== FIXTURE ||
|
|
!isIsoTime(report?.observedAt)
|
|
) {
|
|
add('QL3_WORKER_MANAGEMENT_DURABLE_EVIDENCE_SHAPE');
|
|
}
|
|
if (
|
|
!exactKeys(report?.source, [
|
|
'ceremonyReportSha256',
|
|
'ceremonyDefinitionSha256',
|
|
'ceremonyFixture',
|
|
]) ||
|
|
!SHA256_PATTERN.test(report?.source?.ceremonyReportSha256) ||
|
|
!SHA256_PATTERN.test(report?.source?.ceremonyDefinitionSha256) ||
|
|
report?.source?.ceremonyFixture !==
|
|
'qinglong/worker-credential-management-live-ceremony@v1'
|
|
) {
|
|
add('QL3_WORKER_MANAGEMENT_DURABLE_EVIDENCE_SOURCE');
|
|
}
|
|
const database = report?.database;
|
|
if (
|
|
!exactKeys(database, [
|
|
'postgresVersionNumber',
|
|
'transactionReadOnly',
|
|
'roleNameSha256',
|
|
'roleCanLogin',
|
|
'privilegedAttributesDenied',
|
|
'privilegedMembershipDenied',
|
|
'exactTargetSelect',
|
|
'ql3TableMutationDenied',
|
|
]) ||
|
|
!Number.isSafeInteger(database?.postgresVersionNumber) ||
|
|
database.postgresVersionNumber < MINIMUM_POSTGRES_VERSION_NUMBER ||
|
|
database.postgresVersionNumber >= NEXT_POSTGRES_MAJOR_VERSION_NUMBER ||
|
|
!SHA256_PATTERN.test(database?.roleNameSha256) ||
|
|
Object.entries(database ?? {}).some(
|
|
([key, value]) =>
|
|
key !== 'postgresVersionNumber' &&
|
|
key !== 'roleNameSha256' &&
|
|
value !== true,
|
|
)
|
|
) {
|
|
add('QL3_WORKER_MANAGEMENT_DURABLE_EVIDENCE_DATABASE');
|
|
}
|
|
const state = report?.durableState;
|
|
if (
|
|
!exactKeys(state, [
|
|
'actionRefSha256',
|
|
'authorityProjectIdSha256',
|
|
'approvalRequestIdSha256',
|
|
'reviewerDecisionIdSha256',
|
|
'planDigest',
|
|
'previewDigest',
|
|
'approvalVersion',
|
|
'approvalState',
|
|
'requesterSubjectSha256',
|
|
'reviewerSubjectSha256',
|
|
'dispatchCreated',
|
|
'approvalConsumed',
|
|
'requesterSelfDecisionAuditAbsent',
|
|
'auditRows',
|
|
]) ||
|
|
![
|
|
state?.actionRefSha256,
|
|
state?.authorityProjectIdSha256,
|
|
state?.approvalRequestIdSha256,
|
|
state?.reviewerDecisionIdSha256,
|
|
state?.requesterSubjectSha256,
|
|
state?.reviewerSubjectSha256,
|
|
].every((value) => SHA256_PATTERN.test(value)) ||
|
|
state?.requesterSubjectSha256 === state?.reviewerSubjectSha256 ||
|
|
!HEX_DIGEST_PATTERN.test(state?.planDigest) ||
|
|
!HEX_DIGEST_PATTERN.test(state?.previewDigest) ||
|
|
state?.approvalVersion !== 2 ||
|
|
state?.approvalState !== 'approved' ||
|
|
state?.dispatchCreated !== false ||
|
|
state?.approvalConsumed !== false ||
|
|
state?.requesterSelfDecisionAuditAbsent !== true ||
|
|
!Array.isArray(state?.auditRows) ||
|
|
state.auditRows.length !== 2 ||
|
|
JSON.stringify(state.auditRows.map((row) => row?.kind).sort()) !==
|
|
JSON.stringify(['proposal', 'reviewer_decision']) ||
|
|
state.auditRows.some(
|
|
(row) =>
|
|
!exactKeys(row, [
|
|
'kind',
|
|
'eventIdSha256',
|
|
'operationId',
|
|
'outcome',
|
|
'subjectSha256',
|
|
'authenticationIdSha256',
|
|
'reasonCode',
|
|
'policyFencePresent',
|
|
]) ||
|
|
!SHA256_PATTERN.test(row?.eventIdSha256) ||
|
|
!SHA256_PATTERN.test(row?.subjectSha256) ||
|
|
!SHA256_PATTERN.test(row?.authenticationIdSha256) ||
|
|
row?.reasonCode !== 'worker_credential_review' ||
|
|
row?.policyFencePresent !== true ||
|
|
(row.kind === 'proposal' &&
|
|
(row.operationId !== 'approval.request' ||
|
|
row.outcome !== 'approval_required' ||
|
|
row.subjectSha256 !== state.requesterSubjectSha256)) ||
|
|
(row.kind === 'reviewer_decision' &&
|
|
(row.operationId !== 'approval.decide' ||
|
|
row.outcome !== 'allowed' ||
|
|
row.subjectSha256 !== state.reviewerSubjectSha256)),
|
|
)
|
|
) {
|
|
add('QL3_WORKER_MANAGEMENT_DURABLE_EVIDENCE_STATE');
|
|
}
|
|
if (
|
|
!exactKeys(report?.gates, [
|
|
'sourceBound',
|
|
'readOnlyEvidenceRole',
|
|
'immutablePlanObserved',
|
|
'reviewedApprovalObserved',
|
|
'requesterSelfDecisionLeftNoAudit',
|
|
'proposalAndDecisionAuditObserved',
|
|
'noExecutionOrConsumption',
|
|
'passed',
|
|
]) ||
|
|
Object.values(report?.gates ?? {}).some((value) => value !== true)
|
|
) {
|
|
add('QL3_WORKER_MANAGEMENT_DURABLE_EVIDENCE_GATES');
|
|
}
|
|
if (containsSensitiveMaterial(report)) {
|
|
add('QL3_WORKER_MANAGEMENT_DURABLE_EVIDENCE_SECRET_EXPOSURE');
|
|
}
|
|
return Object.freeze({
|
|
compatible: findings.length === 0,
|
|
findings: Object.freeze(findings),
|
|
});
|
|
}
|
|
|
|
function unusedOutput(filePath) {
|
|
if (
|
|
typeof filePath !== 'string' ||
|
|
!path.isAbsolute(filePath) ||
|
|
fs.existsSync(filePath) ||
|
|
fs.realpathSync(path.dirname(filePath)) !== path.dirname(filePath)
|
|
) {
|
|
fail('output must be one unused canonical absolute path');
|
|
}
|
|
return filePath;
|
|
}
|
|
|
|
function writeNoReplace(filePath, report) {
|
|
const descriptor = fs.openSync(filePath, 'wx', 0o600);
|
|
try {
|
|
fs.writeFileSync(descriptor, `${JSON.stringify(report, null, 2)}\n`);
|
|
fs.fsyncSync(descriptor);
|
|
} finally {
|
|
fs.closeSync(descriptor);
|
|
}
|
|
}
|
|
|
|
function parseArguments(argv) {
|
|
const values = {};
|
|
for (const argument of argv) {
|
|
if (argument === '--') continue;
|
|
const match = /^--([a-z-]+)=(.+)$/.exec(argument);
|
|
if (!match || Object.hasOwn(values, match[1]))
|
|
fail('arguments are invalid');
|
|
values[match[1]] = match[2];
|
|
}
|
|
const keys = [
|
|
'ceremony-report',
|
|
'ceremony',
|
|
'pg-service-file',
|
|
'pg-service',
|
|
'output',
|
|
];
|
|
if (
|
|
JSON.stringify(Object.keys(values).sort()) !==
|
|
JSON.stringify(keys.sort()) ||
|
|
!SERVICE_PATTERN.test(values['pg-service'])
|
|
) {
|
|
fail('arguments are invalid');
|
|
}
|
|
return Object.freeze({
|
|
ceremonyReportFile: values['ceremony-report'],
|
|
ceremonyFile: values.ceremony,
|
|
pgServiceFile: values['pg-service-file'],
|
|
pgService: values['pg-service'],
|
|
outputFile: values.output,
|
|
});
|
|
}
|
|
|
|
function runWorkerCredentialManagementDurableAuditEvidence(
|
|
options,
|
|
dependencies = {},
|
|
) {
|
|
if (
|
|
!exactKeys(options, [
|
|
'ceremonyReportFile',
|
|
'ceremonyFile',
|
|
'pgServiceFile',
|
|
'pgService',
|
|
'outputFile',
|
|
]) ||
|
|
!SERVICE_PATTERN.test(options.pgService)
|
|
) {
|
|
fail('options shape is invalid');
|
|
}
|
|
unusedOutput(options.outputFile);
|
|
canonicalFile(options.pgServiceFile, 'PostgreSQL service file');
|
|
const reportBytes = readPrivateBuffer(
|
|
options.ceremonyReportFile,
|
|
'ceremony report',
|
|
);
|
|
const ceremonyBytes = readPrivateBuffer(options.ceremonyFile, 'ceremony');
|
|
try {
|
|
const ceremonyReport = jsonFromBytes(reportBytes, 'ceremony report');
|
|
const sourceAudit =
|
|
validateWorkerCredentialManagementLiveCeremony(ceremonyReport);
|
|
if (!sourceAudit.compatible) fail('ceremony report is incompatible');
|
|
const ceremony = validateCeremony(jsonFromBytes(ceremonyBytes, 'ceremony'));
|
|
const snapshot = collectDatabaseSnapshot(
|
|
ceremony,
|
|
options,
|
|
dependencies.runPsql,
|
|
);
|
|
const report = buildReport({
|
|
ceremony,
|
|
ceremonyBytes,
|
|
ceremonyReport,
|
|
reportBytes,
|
|
snapshot,
|
|
nowMs: (dependencies.now ?? Date.now)(),
|
|
});
|
|
const audit =
|
|
validateWorkerCredentialManagementDurableAuditEvidence(report);
|
|
if (!audit.compatible) {
|
|
fail(
|
|
`assembled report failed audit: ${audit.findings
|
|
.map(({ code }) => code)
|
|
.join(',')}`,
|
|
);
|
|
}
|
|
writeNoReplace(options.outputFile, report);
|
|
return report;
|
|
} finally {
|
|
reportBytes.fill(0);
|
|
ceremonyBytes.fill(0);
|
|
}
|
|
}
|
|
|
|
function runCli(argv) {
|
|
if (
|
|
process.env.QL3_WORKER_CREDENTIAL_MANAGEMENT_DURABLE_AUDIT_EVIDENCE !== '1'
|
|
) {
|
|
fail('explicit durable evidence opt-in is required');
|
|
}
|
|
const report = runWorkerCredentialManagementDurableAuditEvidence(
|
|
parseArguments(argv),
|
|
);
|
|
process.stdout.write(
|
|
`${JSON.stringify({
|
|
schemaVersion: 1,
|
|
fixture: report.fixture,
|
|
compatible: true,
|
|
})}\n`,
|
|
);
|
|
}
|
|
|
|
if (require.main === module) {
|
|
try {
|
|
runCli(process.argv.slice(2));
|
|
} catch (error) {
|
|
process.stderr.write(
|
|
`${
|
|
error instanceof Error
|
|
? error.message
|
|
: 'Worker management durable audit evidence failed'
|
|
}\n`,
|
|
);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
FIXTURE,
|
|
WorkerCredentialManagementDurableAuditEvidenceError,
|
|
buildReport,
|
|
collectDatabaseSnapshot,
|
|
databaseSql,
|
|
parseArguments,
|
|
runWorkerCredentialManagementDurableAuditEvidence,
|
|
validateWorkerCredentialManagementDurableAuditEvidence,
|
|
};
|