const assert = require('node:assert/strict'); const { createHash } = require('node:crypto'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const { DatabaseSync } = require('node:sqlite'); const { test } = require('node:test'); const { LocalOwnerBootstrapMutationConflictError, } = require('@qinglong/runtime-core/local-owner-bootstrap'); const { MIN_LOCAL_OWNER_ACKNOWLEDGEMENT_AUDIT_RETENTION_MS, MIN_LOCAL_OWNER_ACKNOWLEDGEMENT_REPLAY_RETENTION_MS, } = require('@qinglong/runtime-core/local-owner-delivery-acknowledgement-gc'); const { migrateLocalSqlitePath } = require('@qinglong/local-sqlite/migration'); const { openLocalSqliteAcknowledgementGcDatabase, } = require('@qinglong/local-sqlite/acknowledgement-gc'); const { openLocalSqliteBootstrapDatabase, } = require('@qinglong/local-sqlite/bootstrap'); const { LocalOwnerBootstrapConfigurationError, LocalOwnerBootstrapRejectedError, LocalOwnerBootstrapServiceUnavailableError, createLocalOwnerBootstrapService, } = require('../dist/bootstrap'); const NOW = 1_760_000_000_000; const PEPPER = Buffer.alloc(32, 91).toString('base64url'); function fixture(t) { const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-owner-')); t.after(() => fs.rmSync(directory, { recursive: true, force: true })); return { databasePath: path.join(directory, 'qinglong3.sqlite'), profile: 'edge', }; } function issuer() { return { subject: { type: 'system', id: 'owner-bootstrap' }, authenticationId: 'local-console-test', authenticatedAtMs: NOW - 1_000, expiresAtMs: NOW + 60_000, assurance: 'local_console', }; } function restartedIssuer() { return { ...issuer(), authenticatedAtMs: NOW + 1_000, expiresAtMs: NOW + 61_000, }; } function entropy(start = 1) { let value = start; return (size) => Buffer.alloc(size, value++); } async function opened(t, start = 1) { const options = fixture(t); await migrateLocalSqlitePath(options); const database = await openLocalSqliteBootstrapDatabase(options); const materialDigest = createHash('sha256') .update('qinglong.local-owner-pepper.summary.v1\0', 'utf8') .update(PEPPER, 'utf8') .digest('hex'); await database.ownerPepper.register({ mutationId: '00000000-0000-4000-8000-000000000091', pepperKeyId: 'legacy-v1', materialDigest, backupDigest: 'b'.repeat(64), registeredAtMs: NOW - 2_000, }); await database.ownerPepper.activate({ mutationId: '00000000-0000-4000-8000-000000000092', pepperKeyId: 'legacy-v1', expectedGeneration: 0, activatedAtMs: NOW - 1_500, }); t.after(() => database.close()); return { options, database, service: createLocalOwnerBootstrapService( database.ownerBootstrap, database.apiCredentials, PEPPER, issuer(), { now: () => NOW, randomBytes: entropy(start) }, ), }; } function provisionRequest(overrides = {}) { return { mutationId: '00000000-0000-4000-8000-000000000101', requestId: 'provision-101', ...overrides, }; } function issueRequest(overrides = {}) { return { projectId: 'default', mutationId: '00000000-0000-4000-8000-000000000102', requestId: 'issue-102', ...overrides, }; } function claimRequest(provisioned, challenge, overrides = {}) { return { projectId: 'default', mutationId: '00000000-0000-4000-8000-000000000103', requestId: 'claim-103', challengeId: challenge.challengeId, challengeToken: challenge.challengeToken, credentialToken: provisioned.credentialToken, ...overrides, }; } test('provisions stable identity and claims one Owner without persisting plaintext', async (t) => { const value = await opened(t); const provisioned = await value.service.provision(provisionRequest()); const challenge = await value.service.issue(issueRequest()); const claimed = await value.service.claim( claimRequest(provisioned, challenge), ); assert.equal(provisioned.status, 'inserted'); assert.match(provisioned.credentialToken, /^ql3c_/); assert.equal(challenge.status, 'inserted'); assert.equal(challenge.challengeToken.length, 43); assert.equal(claimed.status, 'inserted'); assert.equal(claimed.binding.role, 'owner'); const client = new DatabaseSync(value.options.databasePath, { readOnly: true, }); try { const credential = client .prepare('SELECT secret_digest FROM "QingLong3ApiCredentials" LIMIT 1') .get(); const storedChallenge = client .prepare( 'SELECT token_digest, consumed_at_ms FROM "QingLong3LocalOwnerBootstrapChallenges" LIMIT 1', ) .get(); assert.match(credential.secret_digest, /^[0-9a-f]{64}$/); assert.match(storedChallenge.token_digest, /^[0-9a-f]{64}$/); assert.equal(storedChallenge.consumed_at_ms, NOW); const bytes = fs.readFileSync(value.options.databasePath); assert.equal( bytes.includes(Buffer.from(provisioned.credentialToken)), false, ); assert.equal(bytes.includes(Buffer.from(challenge.challengeToken)), false); } finally { client.close(); } const replayProvision = await value.service.provision(provisionRequest()); const replayIssue = await value.service.issue(issueRequest()); const replayClaim = await value.service.claim( claimRequest(provisioned, challenge), ); assert.equal(replayProvision.status, 'existing'); assert.equal(replayProvision.credentialToken, null); assert.equal(replayIssue.status, 'existing'); assert.equal(replayIssue.challengeToken, null); assert.equal(replayClaim.status, 'existing'); }); test('replays provisioning and issue across a fresh console authentication', async (t) => { const value = await opened(t); const provisioned = await value.service.provision(provisionRequest()); const challenge = await value.service.issue(issueRequest()); const restarted = createLocalOwnerBootstrapService( value.database.ownerBootstrap, value.database.apiCredentials, PEPPER, restartedIssuer(), { now: () => NOW + 2_000, randomBytes: entropy(90) }, ); const replayProvision = await restarted.provision(provisionRequest()); const replayIssue = await restarted.issue(issueRequest()); assert.equal(replayProvision.status, 'existing'); assert.equal(replayProvision.subjectId, provisioned.subjectId); assert.equal(replayProvision.credentialToken, null); assert.equal(replayIssue.status, 'existing'); assert.equal(replayIssue.challengeId, challenge.challengeId); assert.equal(replayIssue.challengeToken, null); const foreignProof = createLocalOwnerBootstrapService( value.database.ownerBootstrap, value.database.apiCredentials, PEPPER, { ...restartedIssuer(), authenticationId: 'different-local-console' }, { now: () => NOW + 2_000, randomBytes: entropy(100) }, ); await assert.rejects( foreignProof.provision(provisionRequest()), LocalOwnerBootstrapMutationConflictError, ); await assert.rejects( foreignProof.issue(issueRequest()), LocalOwnerBootstrapMutationConflictError, ); }); test('replays a compacted acknowledgement without regenerating entropy', async (t) => { const value = await opened(t); const request = provisionRequest(); const provisioned = await value.service.provision(request); const source = await value.database.ownerBootstrap.resolveProvisioning( request.mutationId, ); const acknowledgement = { kind: 'credential', mutationId: request.mutationId, requestId: request.requestId, subjectId: provisioned.subjectId, credentialId: provisioned.credentialId, factDigest: source.credential.secretDigest, ttlMs: source.credential.expiresAtMs - source.credential.notBeforeAtMs, deliveryDigest: 'd'.repeat(64), acknowledgedAtMs: NOW + 1, }; await value.database.ownerBootstrap.recordDeliveryAcknowledgement( acknowledgement, ); const compactedAtMs = Math.max( source.credential.expiresAtMs, NOW + MIN_LOCAL_OWNER_ACKNOWLEDGEMENT_AUDIT_RETENTION_MS, acknowledgement.acknowledgedAtMs + MIN_LOCAL_OWNER_ACKNOWLEDGEMENT_REPLAY_RETENTION_MS, ); const gc = await openLocalSqliteAcknowledgementGcDatabase(value.options); const gcMutationId = '00000000-0000-4000-8000-0000000001f1'; const gcRequestId = 'ack-gc-1f1'; await gc.acknowledgementGc.compact({ mutationId: gcMutationId, requestId: gcRequestId, acknowledgementMutationId: request.mutationId, expectedKind: 'credential', expectedDeliveryDigest: acknowledgement.deliveryDigest, bridgeClearEvidence: { kind: 'credential', acknowledgementMutationId: request.mutationId, inspectedAtMs: compactedAtMs, evidenceDigest: 'e'.repeat(64), }, retentionPolicy: { version: 1, replayRetentionMs: MIN_LOCAL_OWNER_ACKNOWLEDGEMENT_REPLAY_RETENTION_MS, auditRetentionMs: MIN_LOCAL_OWNER_ACKNOWLEDGEMENT_AUDIT_RETENTION_MS, }, compactedAtMs, audit: { eventId: gcMutationId, requestId: gcRequestId, operationId: 'owner.delivery_acknowledgement.gc', projectId: null, subject: { type: 'system', id: 'owner-acknowledgement-gc' }, authenticationId: 'local-owner-console', outcome: 'allowed', reasons: ['delivery_acknowledgement_gc'], fence: null, occurredAtMs: compactedAtMs, }, }); await gc.close(); const restarted = createLocalOwnerBootstrapService( value.database.ownerBootstrap, value.database.apiCredentials, PEPPER, restartedIssuer(), { now: () => NOW + 2_000, randomBytes: () => { throw new Error('entropy must not be requested for a tombstone replay'); }, }, ); assert.deepEqual(await restarted.provision(request), { status: 'existing', subjectId: provisioned.subjectId, credentialId: provisioned.credentialId, credentialToken: null, expiresAtMs: provisioned.expiresAtMs, }); }); test('replays a compacted challenge acknowledgement without regenerating entropy', async (t) => { const value = await opened(t); await value.service.provision(provisionRequest()); const request = issueRequest(); const issued = await value.service.issue(request); const source = await value.database.ownerBootstrap.resolveIssuedChallenge( request.mutationId, ); const acknowledgement = { kind: 'challenge', mutationId: request.mutationId, requestId: request.requestId, projectId: request.projectId, challengeId: issued.challengeId, factDigest: source.tokenDigest, ttlMs: source.expiresAtMs - source.issuedAtMs, deliveryDigest: 'f'.repeat(64), acknowledgedAtMs: NOW + 1, }; await value.database.ownerBootstrap.recordDeliveryAcknowledgement( acknowledgement, ); const compactedAtMs = Math.max( source.expiresAtMs, NOW + MIN_LOCAL_OWNER_ACKNOWLEDGEMENT_AUDIT_RETENTION_MS, acknowledgement.acknowledgedAtMs + MIN_LOCAL_OWNER_ACKNOWLEDGEMENT_REPLAY_RETENTION_MS, ); const gc = await openLocalSqliteAcknowledgementGcDatabase(value.options); const gcMutationId = '00000000-0000-4000-8000-0000000001f2'; const gcRequestId = 'ack-gc-1f2'; await gc.acknowledgementGc.compact({ mutationId: gcMutationId, requestId: gcRequestId, acknowledgementMutationId: request.mutationId, expectedKind: 'challenge', expectedDeliveryDigest: acknowledgement.deliveryDigest, bridgeClearEvidence: { kind: 'challenge', acknowledgementMutationId: request.mutationId, inspectedAtMs: compactedAtMs, evidenceDigest: '1'.repeat(64), }, retentionPolicy: { version: 1, replayRetentionMs: MIN_LOCAL_OWNER_ACKNOWLEDGEMENT_REPLAY_RETENTION_MS, auditRetentionMs: MIN_LOCAL_OWNER_ACKNOWLEDGEMENT_AUDIT_RETENTION_MS, }, compactedAtMs, audit: { eventId: gcMutationId, requestId: gcRequestId, operationId: 'owner.delivery_acknowledgement.gc', projectId: null, subject: { type: 'system', id: 'owner-acknowledgement-gc' }, authenticationId: 'local-owner-console', outcome: 'allowed', reasons: ['delivery_acknowledgement_gc'], fence: null, occurredAtMs: compactedAtMs, }, }); await gc.close(); const restarted = createLocalOwnerBootstrapService( value.database.ownerBootstrap, value.database.apiCredentials, PEPPER, restartedIssuer(), { now: () => NOW + 2_000, randomBytes: () => { throw new Error('entropy must not be requested for a tombstone replay'); }, }, ); assert.deepEqual(await restarted.issue(request), { status: 'existing', challengeId: issued.challengeId, challengeToken: null, expiresAtMs: issued.expiresAtMs, }); }); test('public requests reject caller-supplied identity fields', async (t) => { const value = await opened(t); await assert.rejects( value.service.provision( provisionRequest({ issuer: issuer(), userId: 'chosen-user', credentialId: 'chosen-key', }), ), LocalOwnerBootstrapConfigurationError, ); await assert.rejects( value.service.claim({ ...claimRequest( { credentialToken: 'x' }, { challengeId: 'A'.repeat(22), challengeToken: 'B'.repeat(43) }, ), principal: issuer(), }), LocalOwnerBootstrapConfigurationError, ); }); test('authentication rejection is audited and consumes the mutation identity', async (t) => { const value = await opened(t); const provisioned = await value.service.provision(provisionRequest()); const challenge = await value.service.issue(issueRequest()); const request = claimRequest(provisioned, challenge, { credentialToken: `${provisioned.credentialToken.slice(0, -1)}A`, }); await assert.rejects( value.service.claim(request), LocalOwnerBootstrapRejectedError, ); await assert.rejects( value.service.claim({ ...request, credentialToken: provisioned.credentialToken, }), LocalOwnerBootstrapMutationConflictError, ); const client = new DatabaseSync(value.options.databasePath, { readOnly: true, }); try { const event = client .prepare( 'SELECT outcome, reasons_json FROM "QingLong3SecurityAuditEvents" WHERE event_id = ?', ) .get(request.mutationId); assert.equal(event.outcome, 'authentication_rejected'); assert.equal(event.reasons_json, '["credential_rejected"]'); assert.equal( client .prepare('SELECT COUNT(*) AS count FROM "QingLong3ProjectRoleBindings"') .get().count, 0, ); } finally { client.close(); } }); test('two independent connections have exactly one claim winner', async (t) => { const value = await opened(t); const provisioned = await value.service.provision(provisionRequest()); const challenge = await value.service.issue(issueRequest()); const secondDatabase = await openLocalSqliteBootstrapDatabase(value.options); t.after(() => secondDatabase.close()); const secondService = createLocalOwnerBootstrapService( secondDatabase.ownerBootstrap, secondDatabase.apiCredentials, PEPPER, issuer(), { now: () => NOW, randomBytes: entropy(40) }, ); const results = await Promise.allSettled([ value.service.claim(claimRequest(provisioned, challenge)), secondService.claim( claimRequest(provisioned, challenge, { mutationId: '00000000-0000-4000-8000-000000000104', requestId: 'claim-104', }), ), ]); assert.equal( results.filter(({ status }) => status === 'fulfilled').length, 1, ); const client = new DatabaseSync(value.options.databasePath, { readOnly: true, }); try { assert.equal( client .prepare('SELECT COUNT(*) AS count FROM "QingLong3ProjectRoleBindings"') .get().count, 1, ); } finally { client.close(); } }); test('any historical binding permanently closes the bootstrap bypass', async (t) => { const value = await opened(t); const provisioned = await value.service.provision(provisionRequest()); const challenge = await value.service.issue(issueRequest()); await value.service.claim(claimRequest(provisioned, challenge)); const client = new DatabaseSync(value.options.databasePath); try { client .prepare( `INSERT INTO "QingLong3ProjectRoleBindings" ( project_id, subject_type, subject_id, version, state, role, mutation_id, changed_by_type, changed_by_id, created_at_ms ) VALUES ('default', 'user', ?, 2, 'revoked', NULL, 'binding-revoke-2', 'system', 'owner-bootstrap', ?)`, ) .run(provisioned.subjectId, NOW + 1); } finally { client.close(); } await assert.rejects( value.service.issue( issueRequest({ mutationId: '00000000-0000-4000-8000-000000000105', requestId: 'issue-105', }), ), ); }); test('claim audit failure rolls back binding and challenge consumption', async (t) => { const value = await opened(t); const provisioned = await value.service.provision(provisionRequest()); const challenge = await value.service.issue(issueRequest()); const trigger = new DatabaseSync(value.options.databasePath); trigger.exec(` CREATE TRIGGER fail_owner_claim_audit BEFORE INSERT ON "QingLong3SecurityAuditEvents" WHEN NEW."operation_id" = 'project.owner_bootstrap_claim' BEGIN SELECT RAISE(ABORT, 'injected audit failure'); END `); trigger.close(); await assert.rejects( value.service.claim(claimRequest(provisioned, challenge)), LocalOwnerBootstrapServiceUnavailableError, ); const client = new DatabaseSync(value.options.databasePath, { readOnly: true, }); try { assert.equal( client .prepare('SELECT COUNT(*) AS count FROM "QingLong3ProjectRoleBindings"') .get().count, 0, ); assert.equal( client .prepare( 'SELECT consumed_at_ms FROM "QingLong3LocalOwnerBootstrapChallenges" LIMIT 1', ) .get().consumed_at_ms, null, ); assert.equal( client .prepare( `SELECT COUNT(*) AS count FROM "QingLong3SecurityAuditEvents" WHERE event_id = '00000000-0000-4000-8000-000000000103'`, ) .get().count, 0, ); } finally { client.close(); } }); test('bootstrap authority closes once and rejects later work', async (t) => { const value = await opened(t); await Promise.all([value.database.close(), value.database.close()]); await assert.rejects( value.database.ownerBootstrap.resolveProjectVersion('default'), ); await assert.rejects(value.database.apiCredentials.resolve('missing')); const root = require('@qinglong/local-sqlite'); const runtime = require('@qinglong/local-sqlite/runtime'); assert.equal('openLocalSqliteBootstrapDatabase' in root, false); assert.equal('openLocalSqliteBootstrapDatabase' in runtime, false); });