mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:00:24 +08:00
575 lines
16 KiB
JavaScript
575 lines
16 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { generateKeyPairSync } = require('node:crypto');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { test } = require('node:test');
|
|
|
|
const {
|
|
LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_SCHEMA,
|
|
LocalPluginPackagePublisherTrustConfigurationError,
|
|
LocalPluginPackagePublisherTrustConflictError,
|
|
assertLocalPluginPackagePublisherKeyPublicationAllowed,
|
|
confirmLocalPluginPackagePublisherKeyRevocation,
|
|
inspectLocalPluginPackagePublisherTrust,
|
|
localPluginPackagePublisherKeyRevocationImpactDigest,
|
|
publishLocalPluginPackagePublisherTrust,
|
|
proposeLocalPluginPackagePublisherKeyRevocation,
|
|
retireLocalPluginPackagePublisherKey,
|
|
} = require('@qinglong/local-admin/package-publisher-trust');
|
|
|
|
function key(keyId, notBeforeMs = 0, notAfterMs = 1_000) {
|
|
const { publicKey } = generateKeyPairSync('ed25519');
|
|
return {
|
|
publisher: 'packages.example.com',
|
|
keyId,
|
|
publicKeyPem: publicKey.export({ format: 'pem', type: 'spki' }),
|
|
notBeforeMs,
|
|
notAfterMs,
|
|
};
|
|
}
|
|
|
|
function trust(keys) {
|
|
return {
|
|
schema: LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_SCHEMA,
|
|
keys,
|
|
};
|
|
}
|
|
|
|
function fixture(t) {
|
|
const unresolved = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'ql3-publisher-trust-'),
|
|
);
|
|
const trustRoot = fs.realpathSync(unresolved);
|
|
fs.chmodSync(trustRoot, 0o700);
|
|
t.after(() => fs.rmSync(trustRoot, { recursive: true, force: true }));
|
|
return trustRoot;
|
|
}
|
|
|
|
test('provisions and overlap-rotates one immutable trust chain', async (t) => {
|
|
const trustRoot = fixture(t);
|
|
const first = key('release-1');
|
|
const second = key('release-2', 50, 2_000);
|
|
let fences = 0;
|
|
|
|
const provisioned = await publishLocalPluginPackagePublisherTrust({
|
|
trustRoot,
|
|
mode: 'provision',
|
|
expectedGeneration: 0,
|
|
mutationId: 'trust-provision-v1',
|
|
occurredAtMs: 100,
|
|
trust: trust([first]),
|
|
beforePublish() {
|
|
fences += 1;
|
|
},
|
|
});
|
|
assert.equal(provisioned.status, 'published');
|
|
assert.equal(provisioned.generation, 1);
|
|
assert.equal(
|
|
fs.statSync(path.join(trustRoot, 'current.json')).mode & 0o777,
|
|
0o600,
|
|
);
|
|
assert.deepEqual(
|
|
inspectLocalPluginPackagePublisherTrust({
|
|
trustRoot,
|
|
observedAtMs: 100,
|
|
}),
|
|
{
|
|
generation: 1,
|
|
keyCount: 1,
|
|
activeKeyCount: 1,
|
|
snapshotCount: 1,
|
|
retirementCount: 0,
|
|
pendingRetirementCount: 0,
|
|
revocationCount: 0,
|
|
pendingRevocationCount: 0,
|
|
quarantinedLockCount: 0,
|
|
recoveryRequired: false,
|
|
pendingGeneration: null,
|
|
pendingMutationId: null,
|
|
unresolvedTransactions: 0,
|
|
trustDigest: provisioned.trustDigest,
|
|
},
|
|
);
|
|
assert.equal(
|
|
(
|
|
await publishLocalPluginPackagePublisherTrust({
|
|
trustRoot,
|
|
mode: 'provision',
|
|
expectedGeneration: 0,
|
|
mutationId: 'trust-provision-v1',
|
|
occurredAtMs: 100,
|
|
trust: trust([first]),
|
|
beforePublish() {
|
|
fences += 1;
|
|
},
|
|
})
|
|
).status,
|
|
'existing',
|
|
);
|
|
|
|
const rotated = await publishLocalPluginPackagePublisherTrust({
|
|
trustRoot,
|
|
mode: 'rotate',
|
|
expectedGeneration: 1,
|
|
mutationId: 'trust-rotate-v2',
|
|
occurredAtMs: 100,
|
|
trust: trust([second, first]),
|
|
beforePublish() {
|
|
fences += 1;
|
|
},
|
|
});
|
|
assert.equal(rotated.status, 'published');
|
|
assert.equal(rotated.generation, 2);
|
|
assert.equal(rotated.keyCount, 2);
|
|
assert.equal(fences, 3);
|
|
assert.deepEqual(
|
|
JSON.parse(
|
|
fs.readFileSync(path.join(trustRoot, 'current.json'), 'utf8'),
|
|
).keys.map((item) => item.keyId),
|
|
['release-1', 'release-2'],
|
|
);
|
|
|
|
await assert.rejects(
|
|
publishLocalPluginPackagePublisherTrust({
|
|
trustRoot,
|
|
mode: 'rotate',
|
|
expectedGeneration: 2,
|
|
mutationId: 'trust-remove-v3',
|
|
occurredAtMs: 100,
|
|
trust: trust([second]),
|
|
}),
|
|
LocalPluginPackagePublisherTrustConfigurationError,
|
|
);
|
|
});
|
|
|
|
test('exact replay promotes a snapshot left durable before current', async (t) => {
|
|
const trustRoot = fixture(t);
|
|
const first = key('release-1');
|
|
const command = {
|
|
trustRoot,
|
|
mode: 'provision',
|
|
expectedGeneration: 0,
|
|
mutationId: 'trust-crash-v1',
|
|
occurredAtMs: 100,
|
|
trust: trust([first]),
|
|
};
|
|
await assert.rejects(
|
|
publishLocalPluginPackagePublisherTrust({
|
|
...command,
|
|
afterSnapshotPublished() {
|
|
throw new Error('simulated current promotion failure');
|
|
},
|
|
}),
|
|
/simulated current promotion failure/,
|
|
);
|
|
assert.equal(fs.existsSync(path.join(trustRoot, 'current.json')), false);
|
|
assert.deepEqual(
|
|
inspectLocalPluginPackagePublisherTrust({
|
|
trustRoot,
|
|
observedAtMs: 100,
|
|
}),
|
|
{
|
|
generation: 0,
|
|
keyCount: 0,
|
|
activeKeyCount: 0,
|
|
snapshotCount: 1,
|
|
retirementCount: 0,
|
|
pendingRetirementCount: 0,
|
|
revocationCount: 0,
|
|
pendingRevocationCount: 0,
|
|
quarantinedLockCount: 0,
|
|
recoveryRequired: true,
|
|
pendingGeneration: 1,
|
|
pendingMutationId: 'trust-crash-v1',
|
|
unresolvedTransactions: 0,
|
|
trustDigest: null,
|
|
},
|
|
);
|
|
|
|
const recovered = await publishLocalPluginPackagePublisherTrust(command);
|
|
assert.equal(recovered.status, 'recovered');
|
|
assert.equal(recovered.generation, 1);
|
|
assert.equal(fs.existsSync(path.join(trustRoot, 'current.json')), true);
|
|
});
|
|
|
|
test('rejects broad roots, unknown files and non-overlap rotation', async (t) => {
|
|
const trustRoot = fixture(t);
|
|
const first = key('release-1');
|
|
await publishLocalPluginPackagePublisherTrust({
|
|
trustRoot,
|
|
mode: 'provision',
|
|
expectedGeneration: 0,
|
|
mutationId: 'trust-provision-v1',
|
|
occurredAtMs: 100,
|
|
trust: trust([first]),
|
|
});
|
|
fs.writeFileSync(path.join(trustRoot, 'unknown'), '', { mode: 0o600 });
|
|
assert.throws(
|
|
() =>
|
|
inspectLocalPluginPackagePublisherTrust({
|
|
trustRoot,
|
|
observedAtMs: 100,
|
|
}),
|
|
/unknown entries/,
|
|
);
|
|
fs.unlinkSync(path.join(trustRoot, 'unknown'));
|
|
const overflow = Array.from({ length: 33 }, (_, index) =>
|
|
path.join(
|
|
trustRoot,
|
|
`retirement-${index.toString(16).padStart(64, '0')}.json`,
|
|
),
|
|
);
|
|
for (const filePath of overflow) {
|
|
fs.writeFileSync(filePath, '', { mode: 0o600 });
|
|
}
|
|
assert.throws(
|
|
() =>
|
|
inspectLocalPluginPackagePublisherTrust({
|
|
trustRoot,
|
|
observedAtMs: 100,
|
|
}),
|
|
/unbounded or unknown entries/,
|
|
);
|
|
for (const filePath of overflow) fs.unlinkSync(filePath);
|
|
const revocationOverflow = Array.from({ length: 33 }, (_, index) =>
|
|
path.join(
|
|
trustRoot,
|
|
`revocation-${index.toString(16).padStart(64, '0')}.json`,
|
|
),
|
|
);
|
|
for (const filePath of revocationOverflow) {
|
|
fs.writeFileSync(filePath, '', { mode: 0o600 });
|
|
}
|
|
assert.throws(
|
|
() =>
|
|
inspectLocalPluginPackagePublisherTrust({
|
|
trustRoot,
|
|
observedAtMs: 100,
|
|
}),
|
|
/unbounded or unknown entries/,
|
|
);
|
|
for (const filePath of revocationOverflow) fs.unlinkSync(filePath);
|
|
fs.chmodSync(trustRoot, 0o755);
|
|
assert.throws(
|
|
() =>
|
|
inspectLocalPluginPackagePublisherTrust({
|
|
trustRoot,
|
|
observedAtMs: 100,
|
|
}),
|
|
/owner-only/,
|
|
);
|
|
});
|
|
|
|
test('retires only an unreferenced key and exact replay recovers durable evidence', async (t) => {
|
|
const trustRoot = fixture(t);
|
|
const first = key('release-1');
|
|
const second = key('release-2');
|
|
await publishLocalPluginPackagePublisherTrust({
|
|
trustRoot,
|
|
mode: 'provision',
|
|
expectedGeneration: 0,
|
|
mutationId: 'trust-provision-v1',
|
|
occurredAtMs: 100,
|
|
trust: trust([first]),
|
|
});
|
|
await publishLocalPluginPackagePublisherTrust({
|
|
trustRoot,
|
|
mode: 'rotate',
|
|
expectedGeneration: 1,
|
|
mutationId: 'trust-rotate-v2',
|
|
occurredAtMs: 100,
|
|
trust: trust([first, second]),
|
|
});
|
|
const command = {
|
|
trustRoot,
|
|
expectedGeneration: 2,
|
|
mutationId: 'trust-retire-v3',
|
|
occurredAtMs: 100,
|
|
publisher: first.publisher,
|
|
keyId: first.keyId,
|
|
proveRetirement() {
|
|
return {
|
|
catalogEntryCount: 0,
|
|
bundleCount: 0,
|
|
matchingEntryCount: 0,
|
|
unresolvedTransactions: 0,
|
|
};
|
|
},
|
|
};
|
|
await assert.rejects(
|
|
retireLocalPluginPackagePublisherKey({
|
|
...command,
|
|
afterReceiptPublished() {
|
|
throw new Error('simulated retirement snapshot failure');
|
|
},
|
|
}),
|
|
/simulated retirement snapshot failure/,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
assertLocalPluginPackagePublisherKeyPublicationAllowed({
|
|
trustRoot,
|
|
publisher: first.publisher,
|
|
keyId: first.keyId,
|
|
}),
|
|
LocalPluginPackagePublisherTrustConflictError,
|
|
);
|
|
assert.doesNotThrow(() =>
|
|
assertLocalPluginPackagePublisherKeyPublicationAllowed({
|
|
trustRoot,
|
|
publisher: second.publisher,
|
|
keyId: second.keyId,
|
|
}),
|
|
);
|
|
const pending = inspectLocalPluginPackagePublisherTrust({
|
|
trustRoot,
|
|
observedAtMs: 100,
|
|
});
|
|
assert.equal(pending.recoveryRequired, true);
|
|
assert.equal(pending.pendingRetirementCount, 1);
|
|
assert.equal(pending.retirementCount, 0);
|
|
|
|
const recovered = await retireLocalPluginPackagePublisherKey(command);
|
|
assert.equal(recovered.status, 'recovered');
|
|
assert.equal(recovered.generation, 3);
|
|
assert.equal(recovered.keyCount, 1);
|
|
assert.equal(
|
|
(await retireLocalPluginPackagePublisherKey(command)).status,
|
|
'existing',
|
|
);
|
|
assert.deepEqual(
|
|
JSON.parse(
|
|
fs.readFileSync(path.join(trustRoot, 'current.json'), 'utf8'),
|
|
).keys.map((item) => item.keyId),
|
|
['release-2'],
|
|
);
|
|
});
|
|
|
|
test('retirement intent blocks publication while catalog coverage remains', async (t) => {
|
|
const trustRoot = fixture(t);
|
|
const first = key('release-1');
|
|
const second = key('release-2');
|
|
await publishLocalPluginPackagePublisherTrust({
|
|
trustRoot,
|
|
mode: 'provision',
|
|
expectedGeneration: 0,
|
|
mutationId: 'trust-provision-v1',
|
|
occurredAtMs: 100,
|
|
trust: trust([first]),
|
|
});
|
|
await publishLocalPluginPackagePublisherTrust({
|
|
trustRoot,
|
|
mode: 'rotate',
|
|
expectedGeneration: 1,
|
|
mutationId: 'trust-rotate-v2',
|
|
occurredAtMs: 100,
|
|
trust: trust([first, second]),
|
|
});
|
|
const command = {
|
|
trustRoot,
|
|
expectedGeneration: 2,
|
|
mutationId: 'trust-retire-blocked-v3',
|
|
occurredAtMs: 100,
|
|
publisher: first.publisher,
|
|
keyId: first.keyId,
|
|
};
|
|
await assert.rejects(
|
|
retireLocalPluginPackagePublisherKey({
|
|
...command,
|
|
proveRetirement() {
|
|
return {
|
|
catalogEntryCount: 1,
|
|
bundleCount: 1,
|
|
matchingEntryCount: 1,
|
|
unresolvedTransactions: 0,
|
|
};
|
|
},
|
|
}),
|
|
/still block retirement/,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
assertLocalPluginPackagePublisherKeyPublicationAllowed({
|
|
trustRoot,
|
|
publisher: first.publisher,
|
|
keyId: first.keyId,
|
|
}),
|
|
/blocked by a durable lifecycle mutation/,
|
|
);
|
|
await assert.rejects(
|
|
retireLocalPluginPackagePublisherKey({
|
|
...command,
|
|
proveRetirement() {
|
|
return {
|
|
catalogEntryCount: 0,
|
|
bundleCount: 0,
|
|
matchingEntryCount: 0,
|
|
unresolvedTransactions: 1,
|
|
};
|
|
},
|
|
}),
|
|
/still block retirement/,
|
|
);
|
|
const recovered = await retireLocalPluginPackagePublisherKey({
|
|
...command,
|
|
proveRetirement() {
|
|
return {
|
|
catalogEntryCount: 0,
|
|
bundleCount: 0,
|
|
matchingEntryCount: 0,
|
|
unresolvedTransactions: 0,
|
|
};
|
|
},
|
|
});
|
|
assert.equal(recovered.status, 'recovered');
|
|
});
|
|
|
|
test('blocks a compromised signer at proposal and requires dual-control confirmation', async (t) => {
|
|
const trustRoot = fixture(t);
|
|
const first = key('release-1');
|
|
const second = key('release-2');
|
|
await publishLocalPluginPackagePublisherTrust({
|
|
trustRoot,
|
|
mode: 'provision',
|
|
expectedGeneration: 0,
|
|
mutationId: 'trust-provision-v1',
|
|
occurredAtMs: 100,
|
|
trust: trust([first]),
|
|
});
|
|
await publishLocalPluginPackagePublisherTrust({
|
|
trustRoot,
|
|
mode: 'rotate',
|
|
expectedGeneration: 1,
|
|
mutationId: 'trust-rotate-v2',
|
|
occurredAtMs: 100,
|
|
trust: trust([first, second]),
|
|
});
|
|
const impactedLockDigests = ['a'.repeat(64)];
|
|
const impact = {
|
|
catalogEntryCount: 1,
|
|
bundleCount: 1,
|
|
matchingEntryCount: 1,
|
|
unresolvedTransactions: 1,
|
|
impactedLockDigests,
|
|
impactDigest: localPluginPackagePublisherKeyRevocationImpactDigest({
|
|
publisher: first.publisher,
|
|
keyId: first.keyId,
|
|
catalogEntryCount: 1,
|
|
bundleCount: 1,
|
|
matchingEntryCount: 1,
|
|
unresolvedTransactions: 1,
|
|
impactedLockDigests,
|
|
}),
|
|
};
|
|
const proposalCommand = {
|
|
trustRoot,
|
|
expectedGeneration: 2,
|
|
mutationId: 'trust-revoke-v3',
|
|
occurredAtMs: 200,
|
|
publisher: first.publisher,
|
|
keyId: first.keyId,
|
|
proposerSubjectId: 'owner-a',
|
|
impact,
|
|
};
|
|
const proposed = await proposeLocalPluginPackagePublisherKeyRevocation(
|
|
proposalCommand,
|
|
);
|
|
assert.equal(proposed.status, 'proposed');
|
|
assert.equal(proposed.runtimeAction, 'stop_required');
|
|
assert.equal(
|
|
(await proposeLocalPluginPackagePublisherKeyRevocation(proposalCommand))
|
|
.status,
|
|
'existing',
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
assertLocalPluginPackagePublisherKeyPublicationAllowed({
|
|
trustRoot,
|
|
publisher: first.publisher,
|
|
keyId: first.keyId,
|
|
}),
|
|
/blocked by a durable lifecycle mutation/,
|
|
);
|
|
const pending = inspectLocalPluginPackagePublisherTrust({
|
|
trustRoot,
|
|
observedAtMs: 200,
|
|
});
|
|
assert.equal(pending.pendingRevocationCount, 1);
|
|
assert.equal(pending.quarantinedLockCount, 1);
|
|
assert.equal(pending.recoveryRequired, true);
|
|
|
|
await assert.rejects(
|
|
confirmLocalPluginPackagePublisherKeyRevocation({
|
|
trustRoot,
|
|
expectedGeneration: 2,
|
|
mutationId: 'trust-revoke-v3',
|
|
confirmedAtMs: 300,
|
|
publisher: first.publisher,
|
|
keyId: first.keyId,
|
|
proposerSubjectId: 'owner-a',
|
|
confirmerSubjectId: 'owner-a',
|
|
authorizationMode: 'dual_control',
|
|
reasonCode: 'confirmed_key_compromise',
|
|
expectedImpactDigest: impact.impactDigest,
|
|
confirmAuthorization() {},
|
|
}),
|
|
/distinct Owner/,
|
|
);
|
|
let authorizationFences = 0;
|
|
const confirmation = {
|
|
trustRoot,
|
|
expectedGeneration: 2,
|
|
mutationId: 'trust-revoke-v3',
|
|
confirmedAtMs: 300,
|
|
publisher: first.publisher,
|
|
keyId: first.keyId,
|
|
proposerSubjectId: 'owner-a',
|
|
confirmerSubjectId: 'owner-b',
|
|
authorizationMode: 'dual_control',
|
|
reasonCode: 'confirmed_key_compromise',
|
|
expectedImpactDigest: impact.impactDigest,
|
|
confirmAuthorization() {
|
|
authorizationFences += 1;
|
|
},
|
|
};
|
|
await assert.rejects(
|
|
confirmLocalPluginPackagePublisherKeyRevocation({
|
|
...confirmation,
|
|
afterSnapshotPublished() {
|
|
throw new Error('simulated revocation promotion failure');
|
|
},
|
|
}),
|
|
/simulated revocation promotion failure/,
|
|
);
|
|
const recovered = await confirmLocalPluginPackagePublisherKeyRevocation(
|
|
confirmation,
|
|
);
|
|
assert.equal(recovered.status, 'recovered');
|
|
assert.equal(recovered.generation, 3);
|
|
assert.equal(recovered.keyCount, 1);
|
|
assert.equal(recovered.quarantinedLockCount, 1);
|
|
assert.equal(recovered.runtimeAction, 'restart_required');
|
|
assert.equal(
|
|
(await confirmLocalPluginPackagePublisherKeyRevocation(confirmation))
|
|
.status,
|
|
'existing',
|
|
);
|
|
assert.equal(authorizationFences, 3);
|
|
assert.deepEqual(
|
|
JSON.parse(
|
|
fs.readFileSync(path.join(trustRoot, 'current.json'), 'utf8'),
|
|
).keys.map((item) => item.keyId),
|
|
['release-2'],
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
assertLocalPluginPackagePublisherKeyPublicationAllowed({
|
|
trustRoot,
|
|
publisher: first.publisher,
|
|
keyId: first.keyId,
|
|
}),
|
|
/blocked by a durable lifecycle mutation/,
|
|
);
|
|
});
|