feat(ql3): establish 3.0 incubation baseline

This commit is contained in:
whyour
2026-08-12 00:25:26 +08:00
parent 4bf92dcfeb
commit c699c32461
2817 changed files with 779642 additions and 653 deletions
@@ -0,0 +1,828 @@
import { createHash } from 'node:crypto';
import {
PluginPackagePublisherTrustRegistry,
type PluginPackagePublisherKeyDefinition,
} from '@qinglong/runtime-core/plugin-package-bundle';
import {
LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_RETIREMENT_INTENT_SCHEMA,
LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_RETIREMENT_RECEIPT_SCHEMA,
LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_REVOCATION_PROPOSAL_SCHEMA,
LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_REVOCATION_RECEIPT_SCHEMA,
LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_SCHEMA,
LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_SNAPSHOT_SCHEMA,
LocalPluginPackagePublisherTrustConfigurationError,
type LocalPluginPackagePublisherKeyRevocationReceipt,
type LocalPluginPackagePublisherTrustDocument,
} from './contracts';
export const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
export const MUTATION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
export interface TrustSnapshot {
readonly schema: typeof LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_SNAPSHOT_SCHEMA;
readonly generation: number;
readonly previousSnapshotDigest: string | null;
readonly previousTrustDigest: string | null;
readonly trustDigest: string;
readonly mutationId: string;
readonly occurredAtMs: number;
readonly mode: 'provision' | 'rotate' | 'retire' | 'revoke';
readonly trust: Readonly<LocalPluginPackagePublisherTrustDocument>;
readonly snapshotDigest: string;
}
export interface RetirementIntent {
readonly schema: typeof LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_RETIREMENT_INTENT_SCHEMA;
readonly publisher: string;
readonly keyId: string;
readonly expectedGeneration: number;
readonly previousTrustDigest: string;
readonly mutationId: string;
readonly occurredAtMs: number;
readonly intentDigest: string;
}
export interface RetirementReceipt {
readonly schema: typeof LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_RETIREMENT_RECEIPT_SCHEMA;
readonly publisher: string;
readonly keyId: string;
readonly expectedGeneration: number;
readonly mutationId: string;
readonly intentDigest: string;
readonly catalogEntryCount: number;
readonly bundleCount: number;
readonly matchingEntryCount: 0;
readonly unresolvedTransactions: 0;
readonly occurredAtMs: number;
readonly receiptDigest: string;
}
export interface RevocationProposal {
readonly schema: typeof LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_REVOCATION_PROPOSAL_SCHEMA;
readonly publisher: string;
readonly keyId: string;
readonly expectedGeneration: number;
readonly previousTrustDigest: string;
readonly mutationId: string;
readonly occurredAtMs: number;
readonly proposerSubjectId: string;
readonly catalogEntryCount: number;
readonly bundleCount: number;
readonly matchingEntryCount: number;
readonly unresolvedTransactions: number;
readonly impactedLockDigests: readonly string[];
readonly impactDigest: string;
readonly proposalDigest: string;
}
export interface RevocationReceipt
extends LocalPluginPackagePublisherKeyRevocationReceipt {
readonly schema: typeof LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_REVOCATION_RECEIPT_SCHEMA;
readonly publisher: string;
readonly keyId: string;
readonly expectedGeneration: number;
readonly mutationId: string;
readonly proposalDigest: string;
readonly proposerSubjectId: string;
readonly confirmerSubjectId: string;
readonly authorizationMode: 'dual_control' | 'break_glass';
readonly reasonCode: 'suspected_key_compromise' | 'confirmed_key_compromise';
readonly confirmedAtMs: number;
readonly impactDigest: string;
readonly impactedLockDigests: readonly string[];
readonly receiptDigest: string;
}
export function activeKeyCount(
trust: Readonly<LocalPluginPackagePublisherTrustDocument> | undefined,
observedAtMs: number,
): number {
return (
trust?.keys.filter(
(key) => key.notBeforeMs <= observedAtMs && observedAtMs < key.notAfterMs,
).length ?? 0
);
}
export function keyMap(
trust: Readonly<LocalPluginPackagePublisherTrustDocument>,
): ReadonlyMap<string, Readonly<PluginPackagePublisherKeyDefinition>> {
return new Map(
trust.keys.map((key) => [`${key.publisher}\0${key.keyId}`, key]),
);
}
export function sameSnapshot(
left: Readonly<TrustSnapshot>,
right: Readonly<TrustSnapshot>,
): boolean {
return left.snapshotDigest === right.snapshotDigest;
}
export function dataRecord(value: unknown, label: string): Record<string, unknown> {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
(Object.getPrototypeOf(value) !== Object.prototype &&
Object.getPrototypeOf(value) !== null)
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
`${label} must be an object`,
);
}
const descriptors = Object.getOwnPropertyDescriptors(value);
if (
Object.values(descriptors).some(
(descriptor) =>
descriptor.get !== undefined ||
descriptor.set !== undefined ||
descriptor.enumerable !== true,
)
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
`${label} must contain enumerable data properties`,
);
}
return value as Record<string, unknown>;
}
export function exactKeys(
value: Record<string, unknown>,
expected: readonly string[],
label: string,
): void {
const actual = Object.keys(value).sort();
const canonical = [...expected].sort();
if (
actual.length !== canonical.length ||
actual.some((key, index) => key !== canonical[index])
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
`${label} shape is invalid`,
);
}
}
export function integer(value: unknown, minimum: number, label: string): number {
if (!Number.isSafeInteger(value) || (value as number) < minimum) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
`${label} is invalid`,
);
}
return value as number;
}
export function digest(value: string): string {
return createHash('sha256').update(value, 'utf8').digest('hex');
}
export function canonicalKey(
value: unknown,
): Readonly<PluginPackagePublisherKeyDefinition> {
const key = dataRecord(value, 'publisher key');
exactKeys(
key,
['keyId', 'notAfterMs', 'notBeforeMs', 'publicKeyPem', 'publisher'],
'publisher key',
);
return Object.freeze({
publisher: key.publisher as string,
keyId: key.keyId as string,
publicKeyPem: key.publicKeyPem as string,
notBeforeMs: key.notBeforeMs as number,
notAfterMs: key.notAfterMs as number,
});
}
export function normalizeLocalPluginPackagePublisherTrustDocument(
value: unknown,
): Readonly<LocalPluginPackagePublisherTrustDocument> {
const document = dataRecord(value, 'publisher trust');
exactKeys(document, ['keys', 'schema'], 'publisher trust');
if (
document.schema !== LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_SCHEMA ||
!Array.isArray(document.keys)
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'publisher trust shape is invalid',
);
}
const keys = document.keys.map(canonicalKey);
if (keys.length > 0) {
try {
new PluginPackagePublisherTrustRegistry(keys);
} catch (error) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'publisher trust keys are invalid',
error,
);
}
}
keys.sort((left, right) =>
`${left.publisher}\0${left.keyId}`.localeCompare(
`${right.publisher}\0${right.keyId}`,
),
);
return Object.freeze({
schema: LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_SCHEMA,
keys: Object.freeze(keys),
});
}
export function createLocalPluginPackagePublisherTrustRegistry(
value: unknown,
): PluginPackagePublisherTrustRegistry {
const trust = normalizeLocalPluginPackagePublisherTrustDocument(value);
return new PluginPackagePublisherTrustRegistry(trust.keys);
}
export function canonicalTrust(
trust: Readonly<LocalPluginPackagePublisherTrustDocument>,
): string {
return `${JSON.stringify(trust)}\n`;
}
export function boundedIdentity(value: unknown, label: string): string {
if (
typeof value !== 'string' ||
value.length === 0 ||
Buffer.byteLength(value, 'utf8') > 256 ||
value.includes('\0')
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
`${label} is invalid`,
);
}
return value;
}
export function retirementIdentityDigest(publisher: string, keyId: string): string {
return digest(`${publisher}\0${keyId}`);
}
export function retirementIntentMaterial(
value: Omit<RetirementIntent, 'intentDigest'>,
): string {
return JSON.stringify(value);
}
export function retirementReceiptMaterial(
value: Omit<RetirementReceipt, 'receiptDigest'>,
): string {
return JSON.stringify(value);
}
export function normalizeRetirementIntent(value: unknown): Readonly<RetirementIntent> {
const intent = dataRecord(value, 'publisher key retirement intent');
exactKeys(
intent,
[
'expectedGeneration',
'intentDigest',
'keyId',
'mutationId',
'occurredAtMs',
'previousTrustDigest',
'publisher',
'schema',
],
'publisher key retirement intent',
);
const publisher = boundedIdentity(intent.publisher, 'retirement publisher');
const keyId = boundedIdentity(intent.keyId, 'retirement keyId');
const expectedGeneration = integer(
intent.expectedGeneration,
1,
'retirement expectedGeneration',
);
const occurredAtMs = integer(
intent.occurredAtMs,
0,
'retirement occurredAtMs',
);
if (
intent.schema !==
LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_RETIREMENT_INTENT_SCHEMA ||
typeof intent.mutationId !== 'string' ||
!MUTATION_ID_PATTERN.test(intent.mutationId) ||
typeof intent.previousTrustDigest !== 'string' ||
!DIGEST_PATTERN.test(intent.previousTrustDigest) ||
typeof intent.intentDigest !== 'string' ||
!DIGEST_PATTERN.test(intent.intentDigest)
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'publisher key retirement intent fields are invalid',
);
}
const material = Object.freeze({
schema: LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_RETIREMENT_INTENT_SCHEMA,
publisher,
keyId,
expectedGeneration,
previousTrustDigest: intent.previousTrustDigest,
mutationId: intent.mutationId,
occurredAtMs,
});
if (digest(retirementIntentMaterial(material)) !== intent.intentDigest) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'publisher key retirement intent digest is invalid',
);
}
return Object.freeze({ ...material, intentDigest: intent.intentDigest });
}
export function normalizeRetirementReceipt(
value: unknown,
): Readonly<RetirementReceipt> {
const receipt = dataRecord(value, 'publisher key retirement receipt');
exactKeys(
receipt,
[
'bundleCount',
'catalogEntryCount',
'expectedGeneration',
'intentDigest',
'keyId',
'matchingEntryCount',
'mutationId',
'occurredAtMs',
'publisher',
'receiptDigest',
'schema',
'unresolvedTransactions',
],
'publisher key retirement receipt',
);
const publisher = boundedIdentity(receipt.publisher, 'retirement publisher');
const keyId = boundedIdentity(receipt.keyId, 'retirement keyId');
const expectedGeneration = integer(
receipt.expectedGeneration,
1,
'retirement expectedGeneration',
);
const occurredAtMs = integer(
receipt.occurredAtMs,
0,
'retirement occurredAtMs',
);
const catalogEntryCount = integer(
receipt.catalogEntryCount,
0,
'retirement catalogEntryCount',
);
const bundleCount = integer(receipt.bundleCount, 0, 'retirement bundleCount');
if (
receipt.schema !==
LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_RETIREMENT_RECEIPT_SCHEMA ||
typeof receipt.mutationId !== 'string' ||
!MUTATION_ID_PATTERN.test(receipt.mutationId) ||
typeof receipt.intentDigest !== 'string' ||
!DIGEST_PATTERN.test(receipt.intentDigest) ||
receipt.matchingEntryCount !== 0 ||
receipt.unresolvedTransactions !== 0 ||
typeof receipt.receiptDigest !== 'string' ||
!DIGEST_PATTERN.test(receipt.receiptDigest)
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'publisher key retirement receipt fields are invalid',
);
}
const material = Object.freeze({
schema: LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_RETIREMENT_RECEIPT_SCHEMA,
publisher,
keyId,
expectedGeneration,
mutationId: receipt.mutationId,
intentDigest: receipt.intentDigest,
catalogEntryCount,
bundleCount,
matchingEntryCount: 0 as const,
unresolvedTransactions: 0 as const,
occurredAtMs,
});
if (digest(retirementReceiptMaterial(material)) !== receipt.receiptDigest) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'publisher key retirement receipt digest is invalid',
);
}
return Object.freeze({ ...material, receiptDigest: receipt.receiptDigest });
}
export function lockDigests(value: unknown, label: string): readonly string[] {
if (!Array.isArray(value) || value.length > 64) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
`${label} is invalid`,
);
}
const normalized = value.map((candidate) => {
if (typeof candidate !== 'string' || !DIGEST_PATTERN.test(candidate)) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
`${label} is invalid`,
);
}
return candidate;
});
const sorted = [...normalized].sort();
if (
new Set(sorted).size !== sorted.length ||
normalized.some((candidate, index) => candidate !== sorted[index])
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
`${label} must be unique and sorted`,
);
}
return Object.freeze(sorted);
}
export function localPluginPackagePublisherKeyRevocationImpactDigest(
value: Readonly<{
publisher: string;
keyId: string;
catalogEntryCount: number;
bundleCount: number;
matchingEntryCount: number;
unresolvedTransactions: number;
impactedLockDigests: readonly string[];
}>,
): string {
const publisher = boundedIdentity(value.publisher, 'impact publisher');
const keyId = boundedIdentity(value.keyId, 'impact keyId');
const catalogEntryCount = integer(
value.catalogEntryCount,
0,
'impact catalogEntryCount',
);
const bundleCount = integer(value.bundleCount, 0, 'impact bundleCount');
const matchingEntryCount = integer(
value.matchingEntryCount,
0,
'impact matchingEntryCount',
);
const unresolvedTransactions = integer(
value.unresolvedTransactions,
0,
'impact unresolvedTransactions',
);
const impactedLockDigests = lockDigests(
value.impactedLockDigests,
'impact lock digests',
);
if (
matchingEntryCount !== impactedLockDigests.length ||
catalogEntryCount < matchingEntryCount
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'revocation impact counts are invalid',
);
}
return digest(
JSON.stringify({
publisher,
keyId,
catalogEntryCount,
bundleCount,
matchingEntryCount,
unresolvedTransactions,
impactedLockDigests,
}),
);
}
export function revocationProposalMaterial(
value: Omit<RevocationProposal, 'proposalDigest'>,
): string {
return JSON.stringify(value);
}
export function revocationReceiptMaterial(
value: Omit<RevocationReceipt, 'receiptDigest'>,
): string {
return JSON.stringify(value);
}
export function normalizeRevocationProposal(
value: unknown,
): Readonly<RevocationProposal> {
const proposal = dataRecord(value, 'publisher key revocation proposal');
exactKeys(
proposal,
[
'bundleCount',
'catalogEntryCount',
'expectedGeneration',
'impactDigest',
'impactedLockDigests',
'keyId',
'matchingEntryCount',
'mutationId',
'occurredAtMs',
'previousTrustDigest',
'proposalDigest',
'proposerSubjectId',
'publisher',
'schema',
'unresolvedTransactions',
],
'publisher key revocation proposal',
);
const publisher = boundedIdentity(proposal.publisher, 'revocation publisher');
const keyId = boundedIdentity(proposal.keyId, 'revocation keyId');
const proposerSubjectId = boundedIdentity(
proposal.proposerSubjectId,
'revocation proposer subject',
);
const expectedGeneration = integer(
proposal.expectedGeneration,
1,
'revocation expectedGeneration',
);
const occurredAtMs = integer(
proposal.occurredAtMs,
0,
'revocation occurredAtMs',
);
const catalogEntryCount = integer(
proposal.catalogEntryCount,
0,
'revocation catalogEntryCount',
);
const bundleCount = integer(
proposal.bundleCount,
0,
'revocation bundleCount',
);
const matchingEntryCount = integer(
proposal.matchingEntryCount,
0,
'revocation matchingEntryCount',
);
const unresolvedTransactions = integer(
proposal.unresolvedTransactions,
0,
'revocation unresolvedTransactions',
);
const impactedLockDigests = lockDigests(
proposal.impactedLockDigests,
'revocation impacted lock digests',
);
if (
proposal.schema !==
LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_REVOCATION_PROPOSAL_SCHEMA ||
typeof proposal.mutationId !== 'string' ||
!MUTATION_ID_PATTERN.test(proposal.mutationId) ||
typeof proposal.previousTrustDigest !== 'string' ||
!DIGEST_PATTERN.test(proposal.previousTrustDigest) ||
typeof proposal.impactDigest !== 'string' ||
!DIGEST_PATTERN.test(proposal.impactDigest) ||
typeof proposal.proposalDigest !== 'string' ||
!DIGEST_PATTERN.test(proposal.proposalDigest) ||
localPluginPackagePublisherKeyRevocationImpactDigest({
publisher,
keyId,
catalogEntryCount,
bundleCount,
matchingEntryCount,
unresolvedTransactions,
impactedLockDigests,
}) !== proposal.impactDigest
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'publisher key revocation proposal fields are invalid',
);
}
const material = Object.freeze({
schema: LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_REVOCATION_PROPOSAL_SCHEMA,
publisher,
keyId,
expectedGeneration,
previousTrustDigest: proposal.previousTrustDigest,
mutationId: proposal.mutationId,
occurredAtMs,
proposerSubjectId,
catalogEntryCount,
bundleCount,
matchingEntryCount,
unresolvedTransactions,
impactedLockDigests,
impactDigest: proposal.impactDigest,
});
if (
digest(revocationProposalMaterial(material)) !== proposal.proposalDigest
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'publisher key revocation proposal digest is invalid',
);
}
return Object.freeze({
...material,
proposalDigest: proposal.proposalDigest,
});
}
export function normalizeRevocationReceipt(
value: unknown,
): Readonly<RevocationReceipt> {
const receipt = dataRecord(value, 'publisher key revocation receipt');
exactKeys(
receipt,
[
'authorizationMode',
'confirmedAtMs',
'confirmerSubjectId',
'expectedGeneration',
'impactDigest',
'impactedLockDigests',
'keyId',
'mutationId',
'proposalDigest',
'proposerSubjectId',
'publisher',
'reasonCode',
'receiptDigest',
'schema',
],
'publisher key revocation receipt',
);
const publisher = boundedIdentity(receipt.publisher, 'revocation publisher');
const keyId = boundedIdentity(receipt.keyId, 'revocation keyId');
const proposerSubjectId = boundedIdentity(
receipt.proposerSubjectId,
'revocation proposer subject',
);
const confirmerSubjectId = boundedIdentity(
receipt.confirmerSubjectId,
'revocation confirmer subject',
);
const expectedGeneration = integer(
receipt.expectedGeneration,
1,
'revocation expectedGeneration',
);
const confirmedAtMs = integer(
receipt.confirmedAtMs,
0,
'revocation confirmedAtMs',
);
const impactedLockDigests = lockDigests(
receipt.impactedLockDigests,
'revocation impacted lock digests',
);
if (
receipt.schema !==
LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_REVOCATION_RECEIPT_SCHEMA ||
typeof receipt.mutationId !== 'string' ||
!MUTATION_ID_PATTERN.test(receipt.mutationId) ||
typeof receipt.proposalDigest !== 'string' ||
!DIGEST_PATTERN.test(receipt.proposalDigest) ||
typeof receipt.impactDigest !== 'string' ||
!DIGEST_PATTERN.test(receipt.impactDigest) ||
(receipt.authorizationMode !== 'dual_control' &&
receipt.authorizationMode !== 'break_glass') ||
(receipt.authorizationMode === 'dual_control' &&
proposerSubjectId === confirmerSubjectId) ||
(receipt.reasonCode !== 'suspected_key_compromise' &&
receipt.reasonCode !== 'confirmed_key_compromise') ||
typeof receipt.receiptDigest !== 'string' ||
!DIGEST_PATTERN.test(receipt.receiptDigest)
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'publisher key revocation receipt fields are invalid',
);
}
const material = Object.freeze({
schema: LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_REVOCATION_RECEIPT_SCHEMA,
publisher,
keyId,
expectedGeneration,
mutationId: receipt.mutationId,
proposalDigest: receipt.proposalDigest,
proposerSubjectId,
confirmerSubjectId,
authorizationMode: receipt.authorizationMode,
reasonCode: receipt.reasonCode,
confirmedAtMs,
impactDigest: receipt.impactDigest,
impactedLockDigests,
});
if (digest(revocationReceiptMaterial(material)) !== receipt.receiptDigest) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'publisher key revocation receipt digest is invalid',
);
}
return Object.freeze({ ...material, receiptDigest: receipt.receiptDigest });
}
export function snapshotMaterial(
value: Omit<TrustSnapshot, 'snapshotDigest'>,
): string {
return JSON.stringify(value);
}
export function normalizeSnapshot(value: unknown): Readonly<TrustSnapshot> {
const snapshot = dataRecord(value, 'publisher trust snapshot');
exactKeys(
snapshot,
[
'generation',
'mode',
'mutationId',
'occurredAtMs',
'previousSnapshotDigest',
'previousTrustDigest',
'schema',
'snapshotDigest',
'trust',
'trustDigest',
],
'publisher trust snapshot',
);
const generation = integer(snapshot.generation, 1, 'snapshot generation');
const occurredAtMs = integer(
snapshot.occurredAtMs,
0,
'snapshot occurredAtMs',
);
if (
snapshot.schema !== LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_SNAPSHOT_SCHEMA ||
(snapshot.mode !== 'provision' &&
snapshot.mode !== 'rotate' &&
snapshot.mode !== 'retire' &&
snapshot.mode !== 'revoke') ||
typeof snapshot.mutationId !== 'string' ||
!MUTATION_ID_PATTERN.test(snapshot.mutationId) ||
(snapshot.previousSnapshotDigest !== null &&
(typeof snapshot.previousSnapshotDigest !== 'string' ||
!DIGEST_PATTERN.test(snapshot.previousSnapshotDigest))) ||
(snapshot.previousTrustDigest !== null &&
(typeof snapshot.previousTrustDigest !== 'string' ||
!DIGEST_PATTERN.test(snapshot.previousTrustDigest))) ||
typeof snapshot.trustDigest !== 'string' ||
!DIGEST_PATTERN.test(snapshot.trustDigest) ||
typeof snapshot.snapshotDigest !== 'string' ||
!DIGEST_PATTERN.test(snapshot.snapshotDigest)
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'publisher trust snapshot fields are invalid',
);
}
const trust = normalizeLocalPluginPackagePublisherTrustDocument(
snapshot.trust,
);
if (digest(canonicalTrust(trust)) !== snapshot.trustDigest) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'publisher trust snapshot trust digest is invalid',
);
}
const material = Object.freeze({
schema: LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_SNAPSHOT_SCHEMA,
generation,
previousSnapshotDigest: snapshot.previousSnapshotDigest,
previousTrustDigest: snapshot.previousTrustDigest,
trustDigest: snapshot.trustDigest,
mutationId: snapshot.mutationId,
occurredAtMs,
mode: snapshot.mode,
trust,
});
if (digest(snapshotMaterial(material)) !== snapshot.snapshotDigest) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'publisher trust snapshot digest is invalid',
);
}
return Object.freeze({
...material,
snapshotDigest: snapshot.snapshotDigest,
});
}
export function snapshotName(generation: number): string {
return `${String(generation).padStart(20, '0')}.json`;
}
export function createSnapshot(
mode: 'provision' | 'rotate' | 'retire' | 'revoke',
expectedGeneration: number,
mutationId: string,
occurredAtMs: number,
trust: Readonly<LocalPluginPackagePublisherTrustDocument>,
previous: Readonly<TrustSnapshot> | undefined,
): Readonly<TrustSnapshot> {
const material = Object.freeze({
schema: LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_SNAPSHOT_SCHEMA,
generation: expectedGeneration + 1,
previousSnapshotDigest: previous?.snapshotDigest ?? null,
previousTrustDigest: previous?.trustDigest ?? null,
trustDigest: digest(canonicalTrust(trust)),
mutationId,
occurredAtMs,
mode,
trust,
});
return Object.freeze({
...material,
snapshotDigest: digest(snapshotMaterial(material)),
});
}
@@ -0,0 +1,174 @@
import type { PluginPackagePublisherKeyDefinition } from '@qinglong/runtime-core/plugin-package-bundle';
export const LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_SCHEMA =
'qinglong/plugin-package-publisher-trust@v1' as const;
export const LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_SNAPSHOT_SCHEMA =
'qinglong/local-plugin-package-publisher-trust-snapshot@v1' as const;
export const LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_RETIREMENT_INTENT_SCHEMA =
'qinglong/local-plugin-package-publisher-trust-retirement-intent@v1' as const;
export const LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_RETIREMENT_RECEIPT_SCHEMA =
'qinglong/local-plugin-package-publisher-trust-retirement-receipt@v1' as const;
export const LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_REVOCATION_PROPOSAL_SCHEMA =
'qinglong/local-plugin-package-publisher-trust-revocation-proposal@v1' as const;
export const LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_REVOCATION_RECEIPT_SCHEMA =
'qinglong/local-plugin-package-publisher-trust-revocation-receipt@v1' as const;
export const MAX_LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_GENERATIONS = 64;
export const MAX_LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_RETIREMENTS = 32;
export const MAX_LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_REVOCATIONS = 32;
export interface LocalPluginPackagePublisherTrustDocument {
readonly schema: typeof LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_SCHEMA;
readonly keys: readonly Readonly<PluginPackagePublisherKeyDefinition>[];
}
export interface PublishLocalPluginPackagePublisherTrustOptions {
readonly trustRoot: string;
readonly mode: 'provision' | 'rotate';
readonly expectedGeneration: number;
readonly mutationId: string;
readonly occurredAtMs: number;
readonly trust: unknown;
readonly beforePublish?: () => void | Promise<void>;
readonly afterSnapshotPublished?: () => void | Promise<void>;
}
export interface RetireLocalPluginPackagePublisherKeyOptions {
readonly trustRoot: string;
readonly expectedGeneration: number;
readonly mutationId: string;
readonly occurredAtMs: number;
readonly publisher: string;
readonly keyId: string;
readonly proveRetirement: () =>
| Readonly<LocalPluginPackagePublisherKeyRetirementProof>
| Promise<Readonly<LocalPluginPackagePublisherKeyRetirementProof>>;
readonly beforePublish?: () => void | Promise<void>;
readonly afterIntentPublished?: () => void | Promise<void>;
readonly afterReceiptPublished?: () => void | Promise<void>;
readonly afterSnapshotPublished?: () => void | Promise<void>;
}
export interface LocalPluginPackagePublisherKeyRetirementProof {
readonly catalogEntryCount: number;
readonly bundleCount: number;
readonly matchingEntryCount: number;
readonly unresolvedTransactions: number;
}
export interface LocalPluginPackagePublisherKeyRevocationImpact {
readonly catalogEntryCount: number;
readonly bundleCount: number;
readonly matchingEntryCount: number;
readonly unresolvedTransactions: number;
readonly impactedLockDigests: readonly string[];
readonly impactDigest: string;
}
export interface ProposeLocalPluginPackagePublisherKeyRevocationOptions {
readonly trustRoot: string;
readonly expectedGeneration: number;
readonly mutationId: string;
readonly occurredAtMs: number;
readonly publisher: string;
readonly keyId: string;
readonly proposerSubjectId: string;
readonly impact: Readonly<LocalPluginPackagePublisherKeyRevocationImpact>;
readonly beforePublish?: () => void | Promise<void>;
}
export interface LocalPluginPackagePublisherKeyRevocationReceipt {
readonly schema: typeof LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_REVOCATION_RECEIPT_SCHEMA;
readonly publisher: string;
readonly keyId: string;
readonly expectedGeneration: number;
readonly mutationId: string;
readonly proposalDigest: string;
readonly proposerSubjectId: string;
readonly confirmerSubjectId: string;
readonly authorizationMode: 'dual_control' | 'break_glass';
readonly reasonCode: 'suspected_key_compromise' | 'confirmed_key_compromise';
readonly confirmedAtMs: number;
readonly impactDigest: string;
readonly impactedLockDigests: readonly string[];
readonly receiptDigest: string;
}
export interface ConfirmLocalPluginPackagePublisherKeyRevocationOptions {
readonly trustRoot: string;
readonly expectedGeneration: number;
readonly mutationId: string;
readonly confirmedAtMs: number;
readonly publisher: string;
readonly keyId: string;
readonly proposerSubjectId: string;
readonly confirmerSubjectId: string;
readonly authorizationMode: 'dual_control' | 'break_glass';
readonly reasonCode: 'suspected_key_compromise' | 'confirmed_key_compromise';
readonly expectedImpactDigest: string;
readonly confirmAuthorization: () => void | Promise<void>;
readonly beforePublish?: () => void | Promise<void>;
readonly afterReceiptPublished?: (
receipt: Readonly<LocalPluginPackagePublisherKeyRevocationReceipt>,
) => void | Promise<void>;
readonly afterSnapshotPublished?: () => void | Promise<void>;
}
export interface ProposedLocalPluginPackagePublisherKeyRevocation {
readonly status: 'proposed' | 'existing';
readonly generation: number;
readonly proposalDigest: string;
readonly impactDigest: string;
readonly matchingEntryCount: number;
readonly runtimeAction: 'stop_required';
}
export interface PublishedLocalPluginPackagePublisherTrust {
readonly status: 'published' | 'existing' | 'recovered';
readonly generation: number;
readonly keyCount: number;
readonly trustDigest: string;
}
export interface ConfirmedLocalPluginPackagePublisherKeyRevocation
extends PublishedLocalPluginPackagePublisherTrust {
readonly authorizationMode: 'dual_control' | 'break_glass';
readonly quarantinedLockCount: number;
readonly runtimeAction: 'restart_required';
}
export interface LocalPluginPackagePublisherTrustInspection {
readonly generation: number;
readonly keyCount: number;
readonly activeKeyCount: number;
readonly snapshotCount: number;
readonly retirementCount: number;
readonly pendingRetirementCount: number;
readonly revocationCount: number;
readonly pendingRevocationCount: number;
readonly quarantinedLockCount: number;
readonly recoveryRequired: boolean;
readonly pendingGeneration: number | null;
readonly pendingMutationId: string | null;
readonly unresolvedTransactions: number;
readonly trustDigest: string | null;
}
export class LocalPluginPackagePublisherTrustConfigurationError extends TypeError {
readonly code = 'LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_CONFIGURATION_INVALID';
constructor(message: string, readonly cause?: unknown) {
super(`Local Plugin Package publisher trust is invalid: ${message}`);
this.name = 'LocalPluginPackagePublisherTrustConfigurationError';
}
}
export class LocalPluginPackagePublisherTrustConflictError extends Error {
readonly code = 'LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_CONFLICT';
constructor(message: string) {
super(
`Local Plugin Package publisher trust conflicts with current state: ${message}`,
);
this.name = 'LocalPluginPackagePublisherTrustConflictError';
}
}
@@ -0,0 +1,66 @@
import {
type LocalPluginPackagePublisherTrustInspection,
} from '../contracts';
import {
activeKeyCount,
dataRecord,
exactKeys,
integer,
digest,
} from '../codec';
import {
TEMPORARY_PATTERN,
loadState,
} from '../privateFilesystemStore';
export function inspectLocalPluginPackagePublisherTrust(
value: Readonly<{ trustRoot: string; observedAtMs: number }>,
): Readonly<LocalPluginPackagePublisherTrustInspection> {
const options = dataRecord(value, 'inspection options');
exactKeys(options, ['observedAtMs', 'trustRoot'], 'inspection options');
const observedAtMs = integer(
value.observedAtMs,
0,
'inspection observedAtMs',
);
const state = loadState(value.trustRoot);
return Object.freeze({
generation: state.committed?.generation ?? 0,
keyCount: state.current?.trust.keys.length ?? 0,
activeKeyCount: activeKeyCount(state.current?.trust, observedAtMs),
snapshotCount: state.snapshots.length,
retirementCount: state.snapshots.filter(
(snapshot) => snapshot.mode === 'retire',
).length,
pendingRetirementCount: state.pendingRetirement ? 1 : 0,
revocationCount: state.snapshots.filter(
(snapshot) => snapshot.mode === 'revoke',
).length,
pendingRevocationCount: state.pendingRevocation ? 1 : 0,
quarantinedLockCount: new Set(
state.revocationProposals.flatMap(
(proposal) => proposal.impactedLockDigests,
),
).size,
recoveryRequired:
state.pending !== undefined ||
state.pendingRetirement !== undefined ||
state.pendingRevocation !== undefined,
pendingGeneration:
state.pending?.generation ??
(state.pendingRetirement
? state.pendingRetirement.expectedGeneration + 1
: state.pendingRevocation
? state.pendingRevocation.expectedGeneration + 1
: null),
pendingMutationId:
state.pending?.mutationId ??
state.pendingRetirement?.mutationId ??
state.pendingRevocation?.mutationId ??
null,
unresolvedTransactions: state.root.entries.filter((entry) =>
TEMPORARY_PATTERN.test(entry),
).length,
trustDigest: state.current?.digest ?? null,
});
}
@@ -0,0 +1,239 @@
import {
LocalPluginPackagePublisherTrustConfigurationError,
LocalPluginPackagePublisherTrustConflictError,
MAX_LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_GENERATIONS,
type LocalPluginPackagePublisherTrustDocument,
type PublishedLocalPluginPackagePublisherTrust,
type PublishLocalPluginPackagePublisherTrustOptions,
} from '../contracts';
import {
MUTATION_ID_PATTERN,
activeKeyCount,
createSnapshot,
dataRecord,
exactKeys,
integer,
keyMap,
digest,
boundedIdentity,
normalizeLocalPluginPackagePublisherTrustDocument,
sameSnapshot,
} from '../codec';
import {
revalidateDirectory,
loadState,
publishSnapshot,
promoteCurrent,
} from '../privateFilesystemStore';
function assertTransition(
mode: 'provision' | 'rotate',
current: Readonly<LocalPluginPackagePublisherTrustDocument> | undefined,
candidate: Readonly<LocalPluginPackagePublisherTrustDocument>,
occurredAtMs: number,
): void {
if (mode === 'provision') {
if (current !== undefined) {
throw new LocalPluginPackagePublisherTrustConflictError(
'provision requires an empty trust root',
);
}
if (activeKeyCount(candidate, occurredAtMs) < 1) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'provision requires a currently active publisher key',
);
}
return;
}
if (current === undefined) {
throw new LocalPluginPackagePublisherTrustConflictError(
'rotation requires an existing trust generation',
);
}
const existing = keyMap(current);
const next = keyMap(candidate);
for (const [identifier, definition] of existing) {
if (
!next.has(identifier) ||
JSON.stringify(next.get(identifier)) !== JSON.stringify(definition)
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'overlap rotation cannot remove or rewrite an existing key',
);
}
}
const added = [...next.entries()]
.filter(([identifier]) => !existing.has(identifier))
.map(([, definition]) => definition);
if (
added.length === 0 ||
!added.some(
(key) => key.notBeforeMs <= occurredAtMs && occurredAtMs < key.notAfterMs,
)
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'overlap rotation requires a new currently active key',
);
}
}
export async function publishLocalPluginPackagePublisherTrust(
value: PublishLocalPluginPackagePublisherTrustOptions,
): Promise<Readonly<PublishedLocalPluginPackagePublisherTrust>> {
const options = dataRecord(value, 'publication options');
const optional = [
...(Object.hasOwn(options, 'beforePublish') ? ['beforePublish'] : []),
...(Object.hasOwn(options, 'afterSnapshotPublished')
? ['afterSnapshotPublished']
: []),
];
exactKeys(
options,
[
'expectedGeneration',
'mode',
'mutationId',
'occurredAtMs',
'trust',
'trustRoot',
...optional,
],
'publication options',
);
const expectedGeneration = integer(
value.expectedGeneration,
0,
'expectedGeneration',
);
const occurredAtMs = integer(value.occurredAtMs, 0, 'occurredAtMs');
if (
(value.mode !== 'provision' && value.mode !== 'rotate') ||
typeof value.mutationId !== 'string' ||
!MUTATION_ID_PATTERN.test(value.mutationId) ||
(value.beforePublish !== undefined &&
typeof value.beforePublish !== 'function') ||
(value.afterSnapshotPublished !== undefined &&
typeof value.afterSnapshotPublished !== 'function')
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'publication identity is invalid',
);
}
const trust = normalizeLocalPluginPackagePublisherTrustDocument(value.trust);
let state = loadState(value.trustRoot);
const previous = state.snapshots[expectedGeneration - 1];
const requested = createSnapshot(
value.mode,
expectedGeneration,
value.mutationId,
occurredAtMs,
trust,
previous,
);
const replay = state.snapshots.find(
(snapshot) => snapshot.mutationId === value.mutationId,
);
if (replay) {
if (!sameSnapshot(replay, requested)) {
throw new LocalPluginPackagePublisherTrustConflictError(
'mutation identity was reused with different trust',
);
}
await value.beforePublish?.();
if (state.pending?.snapshotDigest === replay.snapshotDigest) {
promoteCurrent(state.root, replay);
return Object.freeze({
status: 'recovered',
generation: replay.generation,
keyCount: replay.trust.keys.length,
trustDigest: replay.trustDigest,
});
}
return Object.freeze({
status: 'existing',
generation: replay.generation,
keyCount: replay.trust.keys.length,
trustDigest: replay.trustDigest,
});
}
if (state.pending) {
throw new LocalPluginPackagePublisherTrustConflictError(
'a trust generation requires exact command replay',
);
}
if (state.pendingRetirement) {
throw new LocalPluginPackagePublisherTrustConflictError(
'a publisher key retirement requires exact command replay',
);
}
if (state.pendingRevocation) {
throw new LocalPluginPackagePublisherTrustConflictError(
'a publisher key revocation requires exact command replay',
);
}
if (
state.snapshots.length >=
MAX_LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_GENERATIONS ||
(state.committed?.generation ?? 0) !== expectedGeneration
) {
throw new LocalPluginPackagePublisherTrustConflictError(
'expected generation is stale or capacity is exhausted',
);
}
assertTransition(value.mode, state.current?.trust, trust, occurredAtMs);
await value.beforePublish?.();
state = loadState(value.trustRoot);
if (
state.pending ||
state.pendingRetirement ||
state.pendingRevocation ||
(state.committed?.generation ?? 0) !== expectedGeneration ||
state.current?.digest !==
(expectedGeneration === 0 ? undefined : requested.previousTrustDigest)
) {
throw new LocalPluginPackagePublisherTrustConflictError(
'trust state changed before publication',
);
}
revalidateDirectory(state.root);
publishSnapshot(state.root, requested);
await value.afterSnapshotPublished?.();
promoteCurrent(state.root, requested);
return Object.freeze({
status: 'published',
generation: requested.generation,
keyCount: requested.trust.keys.length,
trustDigest: requested.trustDigest,
});
}
export function assertLocalPluginPackagePublisherKeyPublicationAllowed(
value: Readonly<{
trustRoot: string;
publisher: string;
keyId: string;
}>,
): void {
const options = dataRecord(value, 'publication guard options');
exactKeys(
options,
['keyId', 'publisher', 'trustRoot'],
'publication guard options',
);
const publisher = boundedIdentity(value.publisher, 'publisher');
const keyId = boundedIdentity(value.keyId, 'keyId');
const state = loadState(value.trustRoot);
if (
state.retirementIntents.some(
(intent) => intent.publisher === publisher && intent.keyId === keyId,
) ||
state.revocationProposals.some(
(proposal) =>
proposal.publisher === publisher && proposal.keyId === keyId,
)
) {
throw new LocalPluginPackagePublisherTrustConflictError(
'publisher key is blocked by a durable lifecycle mutation',
);
}
}
@@ -0,0 +1,318 @@
import {
LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_RETIREMENT_INTENT_SCHEMA,
LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_RETIREMENT_RECEIPT_SCHEMA,
LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_SCHEMA,
LocalPluginPackagePublisherTrustConfigurationError,
LocalPluginPackagePublisherTrustConflictError,
MAX_LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_RETIREMENTS,
type PublishedLocalPluginPackagePublisherTrust,
type RetireLocalPluginPackagePublisherKeyOptions,
} from '../contracts';
import {
MUTATION_ID_PATTERN,
activeKeyCount,
createSnapshot,
dataRecord,
exactKeys,
integer,
keyMap,
digest,
boundedIdentity,
normalizeLocalPluginPackagePublisherTrustDocument,
normalizeRetirementIntent,
normalizeRetirementReceipt,
retirementIdentityDigest,
retirementIntentMaterial,
retirementReceiptMaterial,
} from '../codec';
import {
revalidateDirectory,
loadState,
publishSnapshot,
publishImmutableDocument,
promoteCurrent,
} from '../privateFilesystemStore';
export async function retireLocalPluginPackagePublisherKey(
value: RetireLocalPluginPackagePublisherKeyOptions,
): Promise<Readonly<PublishedLocalPluginPackagePublisherTrust>> {
const options = dataRecord(value, 'retirement options');
const optional = [
...(Object.hasOwn(options, 'beforePublish') ? ['beforePublish'] : []),
...(Object.hasOwn(options, 'afterIntentPublished')
? ['afterIntentPublished']
: []),
...(Object.hasOwn(options, 'afterReceiptPublished')
? ['afterReceiptPublished']
: []),
...(Object.hasOwn(options, 'afterSnapshotPublished')
? ['afterSnapshotPublished']
: []),
];
exactKeys(
options,
[
'expectedGeneration',
'keyId',
'mutationId',
'occurredAtMs',
'proveRetirement',
'publisher',
'trustRoot',
...optional,
],
'retirement options',
);
const expectedGeneration = integer(
value.expectedGeneration,
1,
'expectedGeneration',
);
const occurredAtMs = integer(value.occurredAtMs, 0, 'occurredAtMs');
const publisher = boundedIdentity(value.publisher, 'publisher');
const keyId = boundedIdentity(value.keyId, 'keyId');
if (
typeof value.mutationId !== 'string' ||
!MUTATION_ID_PATTERN.test(value.mutationId) ||
typeof value.proveRetirement !== 'function' ||
[
value.beforePublish,
value.afterIntentPublished,
value.afterReceiptPublished,
value.afterSnapshotPublished,
].some(
(callback) => callback !== undefined && typeof callback !== 'function',
)
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'retirement identity is invalid',
);
}
let state = loadState(value.trustRoot);
const previous = state.snapshots[expectedGeneration - 1];
if (!previous) {
throw new LocalPluginPackagePublisherTrustConflictError(
'expected generation is stale',
);
}
const intentMaterial = Object.freeze({
schema: LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_RETIREMENT_INTENT_SCHEMA,
publisher,
keyId,
expectedGeneration,
previousTrustDigest: previous.trustDigest,
mutationId: value.mutationId,
occurredAtMs,
});
const requestedIntent = Object.freeze({
...intentMaterial,
intentDigest: digest(retirementIntentMaterial(intentMaterial)),
});
const identityDigest = retirementIdentityDigest(publisher, keyId);
let existingIntent = state.retirementIntents.find(
(intent) =>
retirementIdentityDigest(intent.publisher, intent.keyId) ===
identityDigest,
);
if (
existingIntent &&
existingIntent.intentDigest !== requestedIntent.intentDigest
) {
throw new LocalPluginPackagePublisherTrustConflictError(
'publisher key retirement identity was reused',
);
}
let completed = state.snapshots.find(
(snapshot) =>
snapshot.mode === 'retire' &&
snapshot.mutationId === requestedIntent.mutationId,
);
await value.beforePublish?.();
state = loadState(value.trustRoot);
existingIntent = state.retirementIntents.find(
(intent) =>
retirementIdentityDigest(intent.publisher, intent.keyId) ===
identityDigest,
);
if (
existingIntent &&
existingIntent.intentDigest !== requestedIntent.intentDigest
) {
throw new LocalPluginPackagePublisherTrustConflictError(
'publisher key retirement identity was reused',
);
}
completed = state.snapshots.find(
(snapshot) =>
snapshot.mode === 'retire' &&
snapshot.mutationId === requestedIntent.mutationId,
);
if (completed) {
if (state.pending?.snapshotDigest === completed.snapshotDigest) {
promoteCurrent(state.root, completed);
return Object.freeze({
status: 'recovered',
generation: completed.generation,
keyCount: completed.trust.keys.length,
trustDigest: completed.trustDigest,
});
}
return Object.freeze({
status: 'existing',
generation: completed.generation,
keyCount: completed.trust.keys.length,
trustDigest: completed.trustDigest,
});
}
const current = state.current?.trust;
const target = `${publisher}\0${keyId}`;
if (
state.pending ||
state.pendingRevocation ||
(state.pendingRetirement &&
state.pendingRetirement.intentDigest !== requestedIntent.intentDigest) ||
(state.committed?.generation ?? 0) !== expectedGeneration ||
!current ||
!keyMap(current).has(target)
) {
throw new LocalPluginPackagePublisherTrustConflictError(
'retirement does not match the current trust head',
);
}
const remainingTrust = normalizeLocalPluginPackagePublisherTrustDocument({
schema: LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_SCHEMA,
keys: current.keys.filter(
(key) => `${key.publisher}\0${key.keyId}` !== target,
),
});
if (activeKeyCount(remainingTrust, occurredAtMs) < 1) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'retirement must retain a currently active publisher key',
);
}
if (!existingIntent) {
if (
state.retirementIntents.length >=
MAX_LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_RETIREMENTS
) {
throw new LocalPluginPackagePublisherTrustConflictError(
'publisher key retirement capacity is exhausted',
);
}
revalidateDirectory(state.root);
publishImmutableDocument(
state.root,
`retirement-${identityDigest}.json`,
`${JSON.stringify(requestedIntent)}\n`,
normalizeRetirementIntent,
);
await value.afterIntentPublished?.();
}
state = loadState(value.trustRoot);
if (
state.pending ||
state.pendingRevocation ||
state.pendingRetirement?.intentDigest !== requestedIntent.intentDigest ||
(state.committed?.generation ?? 0) !== expectedGeneration ||
state.current?.digest !== previous.trustDigest
) {
throw new LocalPluginPackagePublisherTrustConflictError(
'trust state changed after retirement intent publication',
);
}
let receipt = state.retirementReceipts.find(
(candidate) => candidate.mutationId === requestedIntent.mutationId,
);
if (!receipt) {
const proofValue = await value.proveRetirement();
const proof = dataRecord(proofValue, 'retirement proof');
exactKeys(
proof,
[
'bundleCount',
'catalogEntryCount',
'matchingEntryCount',
'unresolvedTransactions',
],
'retirement proof',
);
const catalogEntryCount = integer(
proof.catalogEntryCount,
0,
'retirement catalogEntryCount',
);
const bundleCount = integer(proof.bundleCount, 0, 'retirement bundleCount');
const matchingEntryCount = integer(
proof.matchingEntryCount,
0,
'retirement matchingEntryCount',
);
const unresolvedTransactions = integer(
proof.unresolvedTransactions,
0,
'retirement unresolvedTransactions',
);
if (matchingEntryCount !== 0 || unresolvedTransactions !== 0) {
throw new LocalPluginPackagePublisherTrustConflictError(
'catalog signer coverage or transactions still block retirement',
);
}
const receiptMaterial = Object.freeze({
schema: LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_RETIREMENT_RECEIPT_SCHEMA,
publisher,
keyId,
expectedGeneration,
mutationId: value.mutationId,
intentDigest: requestedIntent.intentDigest,
catalogEntryCount,
bundleCount,
matchingEntryCount: 0 as const,
unresolvedTransactions: 0 as const,
occurredAtMs,
});
receipt = Object.freeze({
...receiptMaterial,
receiptDigest: digest(retirementReceiptMaterial(receiptMaterial)),
});
publishImmutableDocument(
state.root,
`retirement-receipt-${identityDigest}.json`,
`${JSON.stringify(receipt)}\n`,
normalizeRetirementReceipt,
);
await value.afterReceiptPublished?.();
}
const requestedSnapshot = createSnapshot(
'retire',
expectedGeneration,
value.mutationId,
occurredAtMs,
remainingTrust,
previous,
);
state = loadState(value.trustRoot);
if (
state.pending ||
state.pendingRevocation ||
state.pendingRetirement?.intentDigest !== requestedIntent.intentDigest ||
!state.retirementReceipts.some(
(candidate) => candidate.receiptDigest === receipt.receiptDigest,
) ||
(state.committed?.generation ?? 0) !== expectedGeneration ||
state.current?.digest !== previous.trustDigest
) {
throw new LocalPluginPackagePublisherTrustConflictError(
'trust state changed before retirement publication',
);
}
revalidateDirectory(state.root);
publishSnapshot(state.root, requestedSnapshot);
await value.afterSnapshotPublished?.();
promoteCurrent(state.root, requestedSnapshot);
return Object.freeze({
status: existingIntent ? 'recovered' : 'published',
generation: requestedSnapshot.generation,
keyCount: requestedSnapshot.trust.keys.length,
trustDigest: requestedSnapshot.trustDigest,
});
}
@@ -0,0 +1,484 @@
import {
LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_REVOCATION_PROPOSAL_SCHEMA,
LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_REVOCATION_RECEIPT_SCHEMA,
LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_SCHEMA,
LocalPluginPackagePublisherTrustConfigurationError,
LocalPluginPackagePublisherTrustConflictError,
MAX_LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_REVOCATIONS,
type ConfirmLocalPluginPackagePublisherKeyRevocationOptions,
type ConfirmedLocalPluginPackagePublisherKeyRevocation,
type ProposedLocalPluginPackagePublisherKeyRevocation,
type ProposeLocalPluginPackagePublisherKeyRevocationOptions,
} from '../contracts';
import {
DIGEST_PATTERN,
MUTATION_ID_PATTERN,
createSnapshot,
dataRecord,
exactKeys,
integer,
keyMap,
digest,
boundedIdentity,
localPluginPackagePublisherKeyRevocationImpactDigest,
normalizeLocalPluginPackagePublisherTrustDocument,
normalizeRevocationProposal,
normalizeRevocationReceipt,
retirementIdentityDigest,
lockDigests,
revocationProposalMaterial,
revocationReceiptMaterial,
} from '../codec';
import {
revalidateDirectory,
loadState,
publishSnapshot,
publishImmutableDocument,
promoteCurrent,
} from '../privateFilesystemStore';
export async function proposeLocalPluginPackagePublisherKeyRevocation(
value: ProposeLocalPluginPackagePublisherKeyRevocationOptions,
): Promise<Readonly<ProposedLocalPluginPackagePublisherKeyRevocation>> {
const options = dataRecord(value, 'revocation proposal options');
const optional = Object.hasOwn(options, 'beforePublish')
? ['beforePublish']
: [];
exactKeys(
options,
[
'expectedGeneration',
'impact',
'keyId',
'mutationId',
'occurredAtMs',
'proposerSubjectId',
'publisher',
'trustRoot',
...optional,
],
'revocation proposal options',
);
const expectedGeneration = integer(
value.expectedGeneration,
1,
'expectedGeneration',
);
const occurredAtMs = integer(value.occurredAtMs, 0, 'occurredAtMs');
const publisher = boundedIdentity(value.publisher, 'publisher');
const keyId = boundedIdentity(value.keyId, 'keyId');
const proposerSubjectId = boundedIdentity(
value.proposerSubjectId,
'proposerSubjectId',
);
if (
typeof value.mutationId !== 'string' ||
!MUTATION_ID_PATTERN.test(value.mutationId) ||
(value.beforePublish !== undefined &&
typeof value.beforePublish !== 'function')
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'revocation proposal identity is invalid',
);
}
const impactValue = dataRecord(value.impact, 'revocation impact');
exactKeys(
impactValue,
[
'bundleCount',
'catalogEntryCount',
'impactDigest',
'impactedLockDigests',
'matchingEntryCount',
'unresolvedTransactions',
],
'revocation impact',
);
const impactedLockDigests = lockDigests(
value.impact.impactedLockDigests,
'revocation impacted lock digests',
);
const catalogEntryCount = integer(
value.impact.catalogEntryCount,
0,
'revocation catalogEntryCount',
);
const bundleCount = integer(
value.impact.bundleCount,
0,
'revocation bundleCount',
);
const matchingEntryCount = integer(
value.impact.matchingEntryCount,
0,
'revocation matchingEntryCount',
);
const unresolvedTransactions = integer(
value.impact.unresolvedTransactions,
0,
'revocation unresolvedTransactions',
);
const impactDigest = localPluginPackagePublisherKeyRevocationImpactDigest({
publisher,
keyId,
catalogEntryCount,
bundleCount,
matchingEntryCount,
unresolvedTransactions,
impactedLockDigests,
});
if (value.impact.impactDigest !== impactDigest) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'revocation impact digest is invalid',
);
}
let state = loadState(value.trustRoot);
const previous = state.snapshots[expectedGeneration - 1];
if (!previous) {
throw new LocalPluginPackagePublisherTrustConflictError(
'expected generation is stale',
);
}
const material = Object.freeze({
schema: LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_REVOCATION_PROPOSAL_SCHEMA,
publisher,
keyId,
expectedGeneration,
previousTrustDigest: previous.trustDigest,
mutationId: value.mutationId,
occurredAtMs,
proposerSubjectId,
catalogEntryCount,
bundleCount,
matchingEntryCount,
unresolvedTransactions,
impactedLockDigests,
impactDigest,
});
const requested = Object.freeze({
...material,
proposalDigest: digest(revocationProposalMaterial(material)),
});
const identityDigest = retirementIdentityDigest(publisher, keyId);
let existing = state.revocationProposals.find(
(proposal) =>
retirementIdentityDigest(proposal.publisher, proposal.keyId) ===
identityDigest,
);
if (existing && existing.proposalDigest !== requested.proposalDigest) {
throw new LocalPluginPackagePublisherTrustConflictError(
'publisher key revocation identity was reused',
);
}
await value.beforePublish?.();
state = loadState(value.trustRoot);
existing = state.revocationProposals.find(
(proposal) =>
retirementIdentityDigest(proposal.publisher, proposal.keyId) ===
identityDigest,
);
if (existing) {
if (existing.proposalDigest !== requested.proposalDigest) {
throw new LocalPluginPackagePublisherTrustConflictError(
'publisher key revocation identity was reused',
);
}
return Object.freeze({
status: 'existing',
generation: existing.expectedGeneration,
proposalDigest: existing.proposalDigest,
impactDigest: existing.impactDigest,
matchingEntryCount: existing.matchingEntryCount,
runtimeAction: 'stop_required',
});
}
const current = state.current?.trust;
const target = `${publisher}\0${keyId}`;
if (
state.pending ||
state.pendingRetirement ||
state.pendingRevocation ||
(state.committed?.generation ?? 0) !== expectedGeneration ||
state.current?.digest !== previous.trustDigest ||
!current ||
!keyMap(current).has(target)
) {
throw new LocalPluginPackagePublisherTrustConflictError(
'revocation proposal does not match the current trust head',
);
}
const remainingTrust = normalizeLocalPluginPackagePublisherTrustDocument({
schema: LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_SCHEMA,
keys: current.keys.filter(
(key) => `${key.publisher}\0${key.keyId}` !== target,
),
});
if (
state.revocationProposals.length >=
MAX_LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_REVOCATIONS
) {
throw new LocalPluginPackagePublisherTrustConflictError(
'publisher key revocation capacity is exhausted',
);
}
revalidateDirectory(state.root);
publishImmutableDocument(
state.root,
`revocation-${identityDigest}.json`,
`${JSON.stringify(requested)}\n`,
normalizeRevocationProposal,
);
return Object.freeze({
status: 'proposed',
generation: expectedGeneration,
proposalDigest: requested.proposalDigest,
impactDigest,
matchingEntryCount,
runtimeAction: 'stop_required',
});
}
export async function confirmLocalPluginPackagePublisherKeyRevocation(
value: ConfirmLocalPluginPackagePublisherKeyRevocationOptions,
): Promise<Readonly<ConfirmedLocalPluginPackagePublisherKeyRevocation>> {
const options = dataRecord(value, 'revocation confirmation options');
const optional = [
...(Object.hasOwn(options, 'beforePublish') ? ['beforePublish'] : []),
...(Object.hasOwn(options, 'afterReceiptPublished')
? ['afterReceiptPublished']
: []),
...(Object.hasOwn(options, 'afterSnapshotPublished')
? ['afterSnapshotPublished']
: []),
];
exactKeys(
options,
[
'authorizationMode',
'confirmedAtMs',
'confirmerSubjectId',
'confirmAuthorization',
'expectedGeneration',
'expectedImpactDigest',
'keyId',
'mutationId',
'proposerSubjectId',
'publisher',
'reasonCode',
'trustRoot',
...optional,
],
'revocation confirmation options',
);
const expectedGeneration = integer(
value.expectedGeneration,
1,
'expectedGeneration',
);
const confirmedAtMs = integer(value.confirmedAtMs, 0, 'confirmedAtMs');
const publisher = boundedIdentity(value.publisher, 'publisher');
const keyId = boundedIdentity(value.keyId, 'keyId');
const proposerSubjectId = boundedIdentity(
value.proposerSubjectId,
'proposerSubjectId',
);
const confirmerSubjectId = boundedIdentity(
value.confirmerSubjectId,
'confirmerSubjectId',
);
if (
typeof value.mutationId !== 'string' ||
!MUTATION_ID_PATTERN.test(value.mutationId) ||
typeof value.expectedImpactDigest !== 'string' ||
!DIGEST_PATTERN.test(value.expectedImpactDigest) ||
(value.authorizationMode !== 'dual_control' &&
value.authorizationMode !== 'break_glass') ||
(value.reasonCode !== 'suspected_key_compromise' &&
value.reasonCode !== 'confirmed_key_compromise') ||
typeof value.confirmAuthorization !== 'function' ||
[
value.beforePublish,
value.afterReceiptPublished,
value.afterSnapshotPublished,
].some(
(callback) => callback !== undefined && typeof callback !== 'function',
)
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'revocation confirmation identity is invalid',
);
}
if (
value.authorizationMode === 'dual_control' &&
proposerSubjectId === confirmerSubjectId
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'dual-control revocation requires a distinct Owner',
);
}
let state = loadState(value.trustRoot);
const identityDigest = retirementIdentityDigest(publisher, keyId);
const proposal = state.revocationProposals.find(
(candidate) =>
retirementIdentityDigest(candidate.publisher, candidate.keyId) ===
identityDigest,
);
if (
!proposal ||
proposal.expectedGeneration !== expectedGeneration ||
proposal.mutationId !== value.mutationId ||
proposal.proposerSubjectId !== proposerSubjectId ||
proposal.impactDigest !== value.expectedImpactDigest
) {
throw new LocalPluginPackagePublisherTrustConflictError(
'revocation confirmation does not match its proposal',
);
}
let completed = state.snapshots.find(
(snapshot) =>
snapshot.mode === 'revoke' && snapshot.mutationId === proposal.mutationId,
);
await value.confirmAuthorization();
await value.beforePublish?.();
state = loadState(value.trustRoot);
completed = state.snapshots.find(
(snapshot) =>
snapshot.mode === 'revoke' && snapshot.mutationId === proposal.mutationId,
);
if (completed) {
const receipt = state.revocationReceipts.find(
(candidate) => candidate.mutationId === proposal.mutationId,
)!;
if (
receipt.confirmerSubjectId !== confirmerSubjectId ||
receipt.authorizationMode !== value.authorizationMode ||
receipt.reasonCode !== value.reasonCode ||
receipt.confirmedAtMs !== confirmedAtMs
) {
throw new LocalPluginPackagePublisherTrustConflictError(
'revocation confirmation identity was reused',
);
}
await value.afterReceiptPublished?.(receipt);
if (state.pending?.snapshotDigest === completed.snapshotDigest) {
promoteCurrent(state.root, completed);
return Object.freeze({
status: 'recovered',
generation: completed.generation,
keyCount: completed.trust.keys.length,
trustDigest: completed.trustDigest,
authorizationMode: receipt.authorizationMode,
quarantinedLockCount: receipt.impactedLockDigests.length,
runtimeAction: 'restart_required',
});
}
return Object.freeze({
status: 'existing',
generation: completed.generation,
keyCount: completed.trust.keys.length,
trustDigest: completed.trustDigest,
authorizationMode: receipt.authorizationMode,
quarantinedLockCount: receipt.impactedLockDigests.length,
runtimeAction: 'restart_required',
});
}
const previous = state.snapshots[expectedGeneration - 1];
const current = state.current?.trust;
const target = `${publisher}\0${keyId}`;
if (
state.pending ||
state.pendingRetirement ||
state.pendingRevocation?.proposalDigest !== proposal.proposalDigest ||
!previous ||
previous.trustDigest !== proposal.previousTrustDigest ||
(state.committed?.generation ?? 0) !== expectedGeneration ||
state.current?.digest !== proposal.previousTrustDigest ||
!current ||
!keyMap(current).has(target)
) {
throw new LocalPluginPackagePublisherTrustConflictError(
'trust state changed before revocation confirmation',
);
}
const remainingTrust = normalizeLocalPluginPackagePublisherTrustDocument({
schema: LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_SCHEMA,
keys: current.keys.filter(
(key) => `${key.publisher}\0${key.keyId}` !== target,
),
});
const receiptMaterial = Object.freeze({
schema: LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_REVOCATION_RECEIPT_SCHEMA,
publisher,
keyId,
expectedGeneration,
mutationId: value.mutationId,
proposalDigest: proposal.proposalDigest,
proposerSubjectId,
confirmerSubjectId,
authorizationMode: value.authorizationMode,
reasonCode: value.reasonCode,
confirmedAtMs,
impactDigest: proposal.impactDigest,
impactedLockDigests: proposal.impactedLockDigests,
});
const requestedReceipt = Object.freeze({
...receiptMaterial,
receiptDigest: digest(revocationReceiptMaterial(receiptMaterial)),
});
const existingReceipt = state.revocationReceipts.find(
(candidate) => candidate.mutationId === proposal.mutationId,
);
if (
existingReceipt &&
existingReceipt.receiptDigest !== requestedReceipt.receiptDigest
) {
throw new LocalPluginPackagePublisherTrustConflictError(
'revocation receipt identity was reused',
);
}
if (!existingReceipt) {
publishImmutableDocument(
state.root,
`revocation-receipt-${identityDigest}.json`,
`${JSON.stringify(requestedReceipt)}\n`,
normalizeRevocationReceipt,
);
}
await value.afterReceiptPublished?.(requestedReceipt);
const requestedSnapshot = createSnapshot(
'revoke',
expectedGeneration,
value.mutationId,
confirmedAtMs,
remainingTrust,
previous,
);
state = loadState(value.trustRoot);
if (
state.pending ||
state.pendingRetirement ||
state.pendingRevocation?.proposalDigest !== proposal.proposalDigest ||
!state.revocationReceipts.some(
(candidate) => candidate.receiptDigest === requestedReceipt.receiptDigest,
) ||
(state.committed?.generation ?? 0) !== expectedGeneration ||
state.current?.digest !== proposal.previousTrustDigest
) {
throw new LocalPluginPackagePublisherTrustConflictError(
'trust state changed before revocation publication',
);
}
revalidateDirectory(state.root);
publishSnapshot(state.root, requestedSnapshot);
await value.afterSnapshotPublished?.();
promoteCurrent(state.root, requestedSnapshot);
return Object.freeze({
status: existingReceipt ? 'recovered' : 'published',
generation: requestedSnapshot.generation,
keyCount: requestedSnapshot.trust.keys.length,
trustDigest: requestedSnapshot.trustDigest,
authorizationMode: requestedReceipt.authorizationMode,
quarantinedLockCount: requestedReceipt.impactedLockDigests.length,
runtimeAction: 'restart_required',
});
}
@@ -0,0 +1,841 @@
import { randomBytes } from 'node:crypto';
import fs, { constants } from 'node:fs';
import path from 'node:path';
import {
LocalPluginPackagePublisherTrustConfigurationError,
LocalPluginPackagePublisherTrustConflictError,
MAX_LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_GENERATIONS,
MAX_LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_RETIREMENTS,
MAX_LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_REVOCATIONS,
type LocalPluginPackagePublisherTrustDocument,
} from './contracts';
import {
activeKeyCount,
digest,
keyMap,
normalizeLocalPluginPackagePublisherTrustDocument,
canonicalTrust,
retirementIdentityDigest,
normalizeRetirementIntent,
normalizeRetirementReceipt,
normalizeRevocationProposal,
normalizeRevocationReceipt,
normalizeSnapshot,
sameSnapshot,
snapshotName,
type TrustSnapshot,
type RetirementIntent,
type RetirementReceipt,
type RevocationProposal,
type RevocationReceipt,
} from './codec';
const CURRENT_FILE = 'current.json';
const SNAPSHOT_PATTERN = /^([0-9]{20})\.json$/;
export const TEMPORARY_PATTERN = /^\.qlpkg-trust-[0-9a-f]{32}\.tmp$/;
const RETIREMENT_INTENT_PATTERN = /^retirement-([0-9a-f]{64})\.json$/;
const RETIREMENT_RECEIPT_PATTERN = /^retirement-receipt-([0-9a-f]{64})\.json$/;
const REVOCATION_PROPOSAL_PATTERN = /^revocation-([0-9a-f]{64})\.json$/;
const REVOCATION_RECEIPT_PATTERN = /^revocation-receipt-([0-9a-f]{64})\.json$/;
const MAX_ROOT_ENTRIES =
MAX_LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_GENERATIONS * 2 +
MAX_LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_RETIREMENTS * 2 +
MAX_LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_REVOCATIONS * 2 +
1;
const MAX_FILE_BYTES = 256 * 1024;
const MAX_PATH_BYTES = 4_096;
export interface DirectoryIdentity {
readonly path: string;
readonly uid: number;
readonly device: bigint;
readonly inode: bigint;
readonly entries: readonly string[];
}
export interface LoadedState {
readonly root: DirectoryIdentity;
readonly snapshots: readonly Readonly<TrustSnapshot>[];
readonly current:
| Readonly<{
trust: Readonly<LocalPluginPackagePublisherTrustDocument>;
digest: string;
}>
| undefined;
readonly committed: Readonly<TrustSnapshot> | undefined;
readonly pending: Readonly<TrustSnapshot> | undefined;
readonly retirementIntents: readonly Readonly<RetirementIntent>[];
readonly retirementReceipts: readonly Readonly<RetirementReceipt>[];
readonly pendingRetirement: Readonly<RetirementIntent> | undefined;
readonly revocationProposals: readonly Readonly<RevocationProposal>[];
readonly revocationReceipts: readonly Readonly<RevocationReceipt>[];
readonly pendingRevocation: Readonly<RevocationProposal> | undefined;
}
export function absolutePath(value: unknown, label: string): string {
if (
typeof value !== 'string' ||
value.length === 0 ||
Buffer.byteLength(value, 'utf8') > MAX_PATH_BYTES ||
value.includes('\0') ||
!path.isAbsolute(value) ||
path.normalize(value) !== value ||
path.parse(value).root === value
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
`${label} must be a normalized bounded absolute non-root path`,
);
}
return value;
}
export function currentUid(): number {
if (
typeof process.getuid !== 'function' ||
typeof process.geteuid !== 'function' ||
process.getuid() !== process.geteuid()
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'real and effective POSIX users must match',
);
}
return process.getuid();
}
export function directory(candidate: unknown): DirectoryIdentity {
const root = absolutePath(candidate, 'trustRoot');
const uid = currentUid();
let stat: fs.BigIntStats;
try {
stat = fs.lstatSync(root, { bigint: true });
} catch (error) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'trust root is unavailable',
error,
);
}
if (
!stat.isDirectory() ||
stat.isSymbolicLink() ||
Number(stat.uid) !== uid ||
(Number(stat.mode) & 0o777) !== 0o700 ||
fs.realpathSync(root) !== root
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'trust root must be an owner-only non-symlink directory',
);
}
const entries = fs.readdirSync(root).sort();
const snapshots = entries.filter((entry) => SNAPSHOT_PATTERN.test(entry));
const temporary = entries.filter((entry) => TEMPORARY_PATTERN.test(entry));
const retirementIntents = entries.filter((entry) =>
RETIREMENT_INTENT_PATTERN.test(entry),
);
const retirementReceipts = entries.filter((entry) =>
RETIREMENT_RECEIPT_PATTERN.test(entry),
);
const revocationProposals = entries.filter((entry) =>
REVOCATION_PROPOSAL_PATTERN.test(entry),
);
const revocationReceipts = entries.filter((entry) =>
REVOCATION_RECEIPT_PATTERN.test(entry),
);
if (
entries.length > MAX_ROOT_ENTRIES ||
snapshots.length > MAX_LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_GENERATIONS ||
temporary.length > MAX_LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_GENERATIONS ||
retirementIntents.length >
MAX_LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_RETIREMENTS ||
retirementReceipts.length >
MAX_LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_RETIREMENTS ||
revocationProposals.length >
MAX_LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_REVOCATIONS ||
revocationReceipts.length >
MAX_LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_REVOCATIONS ||
entries.some(
(entry) =>
entry !== CURRENT_FILE &&
!SNAPSHOT_PATTERN.test(entry) &&
!TEMPORARY_PATTERN.test(entry) &&
!RETIREMENT_INTENT_PATTERN.test(entry) &&
!RETIREMENT_RECEIPT_PATTERN.test(entry) &&
!REVOCATION_PROPOSAL_PATTERN.test(entry) &&
!REVOCATION_RECEIPT_PATTERN.test(entry),
)
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'trust root contains unbounded or unknown entries',
);
}
return Object.freeze({
path: root,
uid,
device: stat.dev,
inode: stat.ino,
entries: Object.freeze(entries),
});
}
export function revalidateDirectory(identity: DirectoryIdentity): void {
const stat = fs.lstatSync(identity.path, { bigint: true });
if (
!stat.isDirectory() ||
stat.isSymbolicLink() ||
Number(stat.uid) !== identity.uid ||
(Number(stat.mode) & 0o777) !== 0o700 ||
stat.dev !== identity.device ||
stat.ino !== identity.inode ||
fs.realpathSync(identity.path) !== identity.path
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'trust root identity changed',
);
}
}
export function syncDirectory(directoryPath: string): void {
const descriptor = fs.openSync(directoryPath, constants.O_RDONLY);
try {
fs.fsyncSync(descriptor);
} finally {
fs.closeSync(descriptor);
}
}
export function readPrivateJson(filePath: string, uid: number): unknown {
let descriptor: number | undefined;
try {
descriptor = fs.openSync(
filePath,
constants.O_RDONLY |
(typeof constants.O_NOFOLLOW === 'number' ? constants.O_NOFOLLOW : 0),
);
const before = fs.fstatSync(descriptor, { bigint: true });
if (
!before.isFile() ||
before.isSymbolicLink() ||
Number(before.uid) !== uid ||
(Number(before.mode) & 0o777) !== 0o600 ||
before.size < 1n ||
before.size > BigInt(MAX_FILE_BYTES)
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'trust file must be a bounded owner-only regular file',
);
}
const buffer = Buffer.alloc(Number(before.size) + 1);
const bytes = fs.readSync(descriptor, buffer, 0, buffer.length, 0);
const after = fs.fstatSync(descriptor, { bigint: true });
if (
bytes !== Number(before.size) ||
after.dev !== before.dev ||
after.ino !== before.ino ||
after.size !== before.size ||
Number(after.uid) !== uid ||
(Number(after.mode) & 0o777) !== 0o600
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'trust file changed while being read',
);
}
const text = buffer.subarray(0, bytes).toString('utf8');
if (!Buffer.from(text, 'utf8').equals(buffer.subarray(0, bytes))) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'trust file must contain strict UTF-8',
);
}
return JSON.parse(text) as unknown;
} catch (error) {
if (error instanceof LocalPluginPackagePublisherTrustConfigurationError) {
throw error;
}
throw new LocalPluginPackagePublisherTrustConfigurationError(
'trust file cannot be read',
error,
);
} finally {
if (descriptor !== undefined) fs.closeSync(descriptor);
}
}
export function loadRetirements(
root: DirectoryIdentity,
snapshots: readonly Readonly<TrustSnapshot>[],
): Readonly<{
intents: readonly Readonly<RetirementIntent>[];
receipts: readonly Readonly<RetirementReceipt>[];
pending: Readonly<RetirementIntent> | undefined;
}> {
const intents = root.entries
.filter((entry) => RETIREMENT_INTENT_PATTERN.test(entry))
.map((entry) => {
const intent = normalizeRetirementIntent(
readPrivateJson(path.join(root.path, entry), root.uid),
);
if (
entry !==
`retirement-${retirementIdentityDigest(
intent.publisher,
intent.keyId,
)}.json`
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'publisher key retirement intent filename is invalid',
);
}
return intent;
});
const receipts = root.entries
.filter((entry) => RETIREMENT_RECEIPT_PATTERN.test(entry))
.map((entry) => {
const receipt = normalizeRetirementReceipt(
readPrivateJson(path.join(root.path, entry), root.uid),
);
if (
entry !==
`retirement-receipt-${retirementIdentityDigest(
receipt.publisher,
receipt.keyId,
)}.json`
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'publisher key retirement receipt filename is invalid',
);
}
return receipt;
});
const intentByMutation = new Map(
intents.map((intent) => [intent.mutationId, intent]),
);
const receiptByMutation = new Map(
receipts.map((receipt) => [receipt.mutationId, receipt]),
);
if (
intentByMutation.size !== intents.length ||
receiptByMutation.size !== receipts.length
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'publisher key retirement mutation identity is duplicated',
);
}
for (const receipt of receipts) {
const intent = intentByMutation.get(receipt.mutationId);
if (
!intent ||
receipt.publisher !== intent.publisher ||
receipt.keyId !== intent.keyId ||
receipt.expectedGeneration !== intent.expectedGeneration ||
receipt.intentDigest !== intent.intentDigest ||
receipt.occurredAtMs !== intent.occurredAtMs
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'publisher key retirement receipt is not bound to its intent',
);
}
}
for (const snapshot of snapshots.filter(({ mode }) => mode === 'retire')) {
const intent = intentByMutation.get(snapshot.mutationId);
const receipt = receiptByMutation.get(snapshot.mutationId);
const previous = snapshots[snapshot.generation - 2];
if (
!intent ||
!receipt ||
!previous ||
intent.expectedGeneration !== previous.generation ||
intent.previousTrustDigest !== previous.trustDigest ||
intent.occurredAtMs !== snapshot.occurredAtMs
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'publisher key retirement snapshot lacks its proof chain',
);
}
const before = keyMap(previous.trust);
const after = keyMap(snapshot.trust);
const target = `${intent.publisher}\0${intent.keyId}`;
if (
!before.has(target) ||
after.has(target) ||
before.size !== after.size + 1 ||
[...after].some(
([identifier, definition]) =>
JSON.stringify(before.get(identifier)) !== JSON.stringify(definition),
) ||
activeKeyCount(snapshot.trust, snapshot.occurredAtMs) < 1
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'publisher key retirement snapshot transition is invalid',
);
}
}
const pending = intents.filter(
(intent) =>
!snapshots.some(
(snapshot) =>
snapshot.mode === 'retire' &&
snapshot.mutationId === intent.mutationId,
),
);
if (
pending.length > 1 ||
pending.some(
(intent) =>
intent.expectedGeneration !== snapshots.length ||
intent.previousTrustDigest !== snapshots.at(-1)?.trustDigest,
)
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'publisher key retirement intent does not match the trust head',
);
}
return Object.freeze({
intents: Object.freeze(intents),
receipts: Object.freeze(receipts),
pending: pending[0],
});
}
export function loadRevocations(
root: DirectoryIdentity,
snapshots: readonly Readonly<TrustSnapshot>[],
): Readonly<{
proposals: readonly Readonly<RevocationProposal>[];
receipts: readonly Readonly<RevocationReceipt>[];
pending: Readonly<RevocationProposal> | undefined;
}> {
const proposals = root.entries
.filter((entry) => REVOCATION_PROPOSAL_PATTERN.test(entry))
.map((entry) => {
const proposal = normalizeRevocationProposal(
readPrivateJson(path.join(root.path, entry), root.uid),
);
if (
entry !==
`revocation-${retirementIdentityDigest(
proposal.publisher,
proposal.keyId,
)}.json`
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'publisher key revocation proposal filename is invalid',
);
}
return proposal;
});
const receipts = root.entries
.filter((entry) => REVOCATION_RECEIPT_PATTERN.test(entry))
.map((entry) => {
const receipt = normalizeRevocationReceipt(
readPrivateJson(path.join(root.path, entry), root.uid),
);
if (
entry !==
`revocation-receipt-${retirementIdentityDigest(
receipt.publisher,
receipt.keyId,
)}.json`
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'publisher key revocation receipt filename is invalid',
);
}
return receipt;
});
const proposalByMutation = new Map(
proposals.map((proposal) => [proposal.mutationId, proposal]),
);
const receiptByMutation = new Map(
receipts.map((receipt) => [receipt.mutationId, receipt]),
);
if (
proposalByMutation.size !== proposals.length ||
receiptByMutation.size !== receipts.length
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'publisher key revocation mutation identity is duplicated',
);
}
for (const receipt of receipts) {
const proposal = proposalByMutation.get(receipt.mutationId);
if (
!proposal ||
receipt.publisher !== proposal.publisher ||
receipt.keyId !== proposal.keyId ||
receipt.expectedGeneration !== proposal.expectedGeneration ||
receipt.proposalDigest !== proposal.proposalDigest ||
receipt.proposerSubjectId !== proposal.proposerSubjectId ||
receipt.impactDigest !== proposal.impactDigest ||
JSON.stringify(receipt.impactedLockDigests) !==
JSON.stringify(proposal.impactedLockDigests) ||
receipt.confirmedAtMs < proposal.occurredAtMs
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'publisher key revocation receipt is not bound to its proposal',
);
}
}
for (const snapshot of snapshots.filter(({ mode }) => mode === 'revoke')) {
const proposal = proposalByMutation.get(snapshot.mutationId);
const receipt = receiptByMutation.get(snapshot.mutationId);
const previous = snapshots[snapshot.generation - 2];
if (
!proposal ||
!receipt ||
!previous ||
proposal.expectedGeneration !== previous.generation ||
proposal.previousTrustDigest !== previous.trustDigest ||
receipt.confirmedAtMs !== snapshot.occurredAtMs
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'publisher key revocation snapshot lacks its authorization chain',
);
}
const before = keyMap(previous.trust);
const after = keyMap(snapshot.trust);
const target = `${proposal.publisher}\0${proposal.keyId}`;
if (
!before.has(target) ||
after.has(target) ||
before.size !== after.size + 1 ||
[...after].some(
([identifier, definition]) =>
JSON.stringify(before.get(identifier)) !== JSON.stringify(definition),
)
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'publisher key revocation snapshot transition is invalid',
);
}
}
const pending = proposals.filter(
(proposal) =>
!snapshots.some(
(snapshot) =>
snapshot.mode === 'revoke' &&
snapshot.mutationId === proposal.mutationId,
),
);
if (
pending.length > 1 ||
pending.some(
(proposal) =>
proposal.expectedGeneration !== snapshots.length ||
proposal.previousTrustDigest !== snapshots.at(-1)?.trustDigest,
)
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'publisher key revocation proposal does not match the trust head',
);
}
return Object.freeze({
proposals: Object.freeze(proposals),
receipts: Object.freeze(receipts),
pending: pending[0],
});
}
export function loadState(candidateRoot: unknown): LoadedState {
const root = directory(candidateRoot);
const snapshots = root.entries
.filter((entry) => SNAPSHOT_PATTERN.test(entry))
.map((entry) => {
const match = SNAPSHOT_PATTERN.exec(entry)!;
const generation = Number(match[1]);
const snapshot = normalizeSnapshot(
readPrivateJson(path.join(root.path, entry), root.uid),
);
if (
!Number.isSafeInteger(generation) ||
generation !== snapshot.generation ||
entry !== snapshotName(snapshot.generation)
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'publisher trust snapshot filename is invalid',
);
}
return snapshot;
})
.sort((left, right) => left.generation - right.generation);
for (const [index, snapshot] of snapshots.entries()) {
const previous = snapshots[index - 1];
if (
snapshot.generation !== index + 1 ||
(previous === undefined
? snapshot.mode !== 'provision' ||
snapshot.previousSnapshotDigest !== null ||
snapshot.previousTrustDigest !== null
: (snapshot.mode !== 'rotate' &&
snapshot.mode !== 'retire' &&
snapshot.mode !== 'revoke') ||
snapshot.previousSnapshotDigest !== previous.snapshotDigest ||
snapshot.previousTrustDigest !== previous.trustDigest)
) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'publisher trust snapshot chain is invalid',
);
}
}
const retirements = loadRetirements(root, snapshots);
const revocations = loadRevocations(root, snapshots);
if (retirements.pending && revocations.pending) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'trust root has conflicting pending key lifecycle mutations',
);
}
const currentPath = path.join(root.path, CURRENT_FILE);
const current = root.entries.includes(CURRENT_FILE)
? (() => {
const trust = normalizeLocalPluginPackagePublisherTrustDocument(
readPrivateJson(currentPath, root.uid),
);
return Object.freeze({
trust,
digest: digest(canonicalTrust(trust)),
});
})()
: undefined;
if (snapshots.length === 0) {
if (current !== undefined) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'publisher trust current file has no immutable snapshot',
);
}
return Object.freeze({
root,
snapshots: Object.freeze([]),
current: undefined,
committed: undefined,
pending: undefined,
retirementIntents: retirements.intents,
retirementReceipts: retirements.receipts,
pendingRetirement: retirements.pending,
revocationProposals: revocations.proposals,
revocationReceipts: revocations.receipts,
pendingRevocation: revocations.pending,
});
}
const latest = snapshots.at(-1)!;
if (current?.digest === latest.trustDigest) {
return Object.freeze({
root,
snapshots: Object.freeze(snapshots),
current,
committed: latest,
pending: undefined,
retirementIntents: retirements.intents,
retirementReceipts: retirements.receipts,
pendingRetirement: retirements.pending,
revocationProposals: revocations.proposals,
revocationReceipts: revocations.receipts,
pendingRevocation: revocations.pending,
});
}
const previous = snapshots.at(-2);
if (
(previous === undefined && current === undefined) ||
current?.digest === previous?.trustDigest
) {
return Object.freeze({
root,
snapshots: Object.freeze(snapshots),
current,
committed: previous,
pending: latest,
retirementIntents: retirements.intents,
retirementReceipts: retirements.receipts,
pendingRetirement: retirements.pending,
revocationProposals: revocations.proposals,
revocationReceipts: revocations.receipts,
pendingRevocation: revocations.pending,
});
}
throw new LocalPluginPackagePublisherTrustConfigurationError(
'publisher trust current file does not match its snapshot chain',
);
}
export function writePrivateTemporary(
root: DirectoryIdentity,
contents: string,
): string {
if (Buffer.byteLength(contents, 'utf8') > MAX_FILE_BYTES) {
throw new LocalPluginPackagePublisherTrustConfigurationError(
'publisher trust file exceeds its byte bound',
);
}
const temporaryPath = path.join(
root.path,
`.qlpkg-trust-${randomBytes(16).toString('hex')}.tmp`,
);
const descriptor = fs.openSync(
temporaryPath,
constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL,
0o600,
);
try {
fs.writeFileSync(descriptor, contents, 'utf8');
fs.fsyncSync(descriptor);
} finally {
fs.closeSync(descriptor);
}
return temporaryPath;
}
export function publishSnapshot(
root: DirectoryIdentity,
snapshot: Readonly<TrustSnapshot>,
): void {
const targetPath = path.join(root.path, snapshotName(snapshot.generation));
const temporaryPath = writePrivateTemporary(
root,
`${JSON.stringify(snapshot)}\n`,
);
try {
try {
fs.linkSync(temporaryPath, targetPath);
syncDirectory(root.path);
} catch (error) {
if (
!error ||
typeof error !== 'object' ||
!('code' in error) ||
(error as { code?: string }).code !== 'EEXIST'
) {
throw error;
}
const existing = normalizeSnapshot(readPrivateJson(targetPath, root.uid));
if (!sameSnapshot(existing, snapshot)) {
throw new LocalPluginPackagePublisherTrustConflictError(
'another trust generation won publication',
);
}
}
} finally {
try {
fs.unlinkSync(temporaryPath);
syncDirectory(root.path);
} catch (error) {
if (
!error ||
typeof error !== 'object' ||
!('code' in error) ||
(error as { code?: string }).code !== 'ENOENT'
) {
throw error;
}
}
}
}
export function publishImmutableDocument(
root: DirectoryIdentity,
fileName: string,
contents: string,
normalize: (value: unknown) => Readonly<{ readonly schema: string }>,
): void {
const targetPath = path.join(root.path, fileName);
const temporaryPath = writePrivateTemporary(root, contents);
try {
try {
fs.linkSync(temporaryPath, targetPath);
syncDirectory(root.path);
} catch (error) {
if (
!error ||
typeof error !== 'object' ||
!('code' in error) ||
(error as { code?: string }).code !== 'EEXIST'
) {
throw error;
}
const existing = normalize(readPrivateJson(targetPath, root.uid));
if (`${JSON.stringify(existing)}\n` !== contents) {
throw new LocalPluginPackagePublisherTrustConflictError(
'immutable retirement evidence already has different content',
);
}
}
} finally {
try {
fs.unlinkSync(temporaryPath);
syncDirectory(root.path);
} catch (error) {
if (
!error ||
typeof error !== 'object' ||
!('code' in error) ||
(error as { code?: string }).code !== 'ENOENT'
) {
throw error;
}
}
}
}
export function promoteCurrent(
root: DirectoryIdentity,
snapshot: Readonly<TrustSnapshot>,
): void {
revalidateDirectory(root);
const currentPath = path.join(root.path, CURRENT_FILE);
if (fs.existsSync(currentPath)) {
const current = normalizeLocalPluginPackagePublisherTrustDocument(
readPrivateJson(currentPath, root.uid),
);
const currentDigest = digest(canonicalTrust(current));
if (currentDigest === snapshot.trustDigest) return;
if (currentDigest !== snapshot.previousTrustDigest) {
throw new LocalPluginPackagePublisherTrustConflictError(
'current trust changed before promotion',
);
}
} else if (snapshot.previousTrustDigest !== null) {
throw new LocalPluginPackagePublisherTrustConflictError(
'current trust disappeared before promotion',
);
}
const temporaryPath = writePrivateTemporary(
root,
canonicalTrust(snapshot.trust),
);
try {
if (snapshot.previousTrustDigest === null) {
try {
fs.linkSync(temporaryPath, currentPath);
} catch (error) {
if (
!error ||
typeof error !== 'object' ||
!('code' in error) ||
(error as { code?: string }).code !== 'EEXIST'
) {
throw error;
}
const current = normalizeLocalPluginPackagePublisherTrustDocument(
readPrivateJson(currentPath, root.uid),
);
if (digest(canonicalTrust(current)) !== snapshot.trustDigest) {
throw new LocalPluginPackagePublisherTrustConflictError(
'another initial trust won publication',
);
}
}
} else {
fs.renameSync(temporaryPath, currentPath);
}
syncDirectory(root.path);
} finally {
try {
fs.unlinkSync(temporaryPath);
syncDirectory(root.path);
} catch (error) {
if (
!error ||
typeof error !== 'object' ||
!('code' in error) ||
(error as { code?: string }).code !== 'ENOENT'
) {
throw error;
}
}
}
}