mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 19:29:13 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
+604
@@ -0,0 +1,604 @@
|
||||
import { createHash, createPublicKey } from 'node:crypto';
|
||||
|
||||
import {
|
||||
MAX_PLUGIN_PACKAGE_PUBLISHER_KEYS,
|
||||
PluginPackagePublisherTrustRegistry,
|
||||
type PluginPackagePublisherKeyDefinition,
|
||||
} from '../pluginPackageBundle';
|
||||
|
||||
export const PLUGIN_PACKAGE_PUBLISHER_TRUST_SNAPSHOT_SCHEMA =
|
||||
'qinglong/plugin-package-publisher-trust-snapshot@v1' as const;
|
||||
export const PLUGIN_PACKAGE_PUBLISHER_TRUST_HEAD_SCHEMA =
|
||||
'qinglong/plugin-package-publisher-trust-head@v1' as const;
|
||||
|
||||
export interface PluginPackagePublisherTrustSnapshotKey {
|
||||
readonly publisher: string;
|
||||
readonly keyId: string;
|
||||
readonly publicKeyDigest: string;
|
||||
readonly notBeforeMs: number;
|
||||
readonly notAfterMs: number;
|
||||
}
|
||||
|
||||
export interface PluginPackagePublisherTrustSnapshot {
|
||||
readonly schema: typeof PLUGIN_PACKAGE_PUBLISHER_TRUST_SNAPSHOT_SCHEMA;
|
||||
readonly keys: readonly Readonly<PluginPackagePublisherTrustSnapshotKey>[];
|
||||
readonly snapshotDigest: string;
|
||||
}
|
||||
|
||||
export interface PluginPackagePublisherTrustKeyRef {
|
||||
readonly publisher: string;
|
||||
readonly keyId: string;
|
||||
}
|
||||
|
||||
export interface PluginPackagePublisherTrustHead {
|
||||
readonly schema: typeof PLUGIN_PACKAGE_PUBLISHER_TRUST_HEAD_SCHEMA;
|
||||
readonly authorityId: string;
|
||||
readonly generation: number;
|
||||
readonly baseSnapshotDigest: string;
|
||||
readonly effectiveTrustDigest: string;
|
||||
readonly updatedAtMs: number;
|
||||
readonly headDigest: string;
|
||||
}
|
||||
|
||||
export interface PluginPackagePublisherTrustAuthorityState {
|
||||
readonly head: Readonly<PluginPackagePublisherTrustHead>;
|
||||
readonly effectiveSnapshot: Readonly<PluginPackagePublisherTrustSnapshot>;
|
||||
}
|
||||
|
||||
export interface ObservePluginPackagePublisherTrustSnapshotInput {
|
||||
readonly authorityId: string;
|
||||
readonly snapshot: PluginPackagePublisherTrustSnapshot;
|
||||
readonly observedBy: string;
|
||||
readonly observedAtMs: number;
|
||||
}
|
||||
|
||||
export interface ObservePluginPackagePublisherTrustSnapshotResult
|
||||
extends PluginPackagePublisherTrustAuthorityState {
|
||||
readonly status: 'created' | 'existing' | 'candidate';
|
||||
}
|
||||
|
||||
export interface PluginPackagePublisherTrustAuthorityRepository {
|
||||
findAuthority(
|
||||
authorityId: string,
|
||||
): Promise<Readonly<PluginPackagePublisherTrustAuthorityState> | null>;
|
||||
observeSnapshot(
|
||||
input: ObservePluginPackagePublisherTrustSnapshotInput,
|
||||
): Promise<Readonly<ObservePluginPackagePublisherTrustSnapshotResult>>;
|
||||
}
|
||||
|
||||
export class InvalidPluginPackagePublisherTrustSnapshotError extends TypeError {
|
||||
readonly code = 'PLUGIN_PACKAGE_PUBLISHER_TRUST_SNAPSHOT_INVALID';
|
||||
|
||||
constructor(message: string) {
|
||||
super(`Plugin Package publisher trust snapshot is invalid: ${message}`);
|
||||
this.name = 'InvalidPluginPackagePublisherTrustSnapshotError';
|
||||
}
|
||||
}
|
||||
|
||||
export class PluginPackagePublisherTrustAuthorityConflictError extends Error {
|
||||
readonly code = 'PLUGIN_PACKAGE_PUBLISHER_TRUST_AUTHORITY_CONFLICT';
|
||||
|
||||
constructor() {
|
||||
super(
|
||||
'Plugin Package publisher trust snapshot conflicts with durable authority',
|
||||
);
|
||||
this.name = 'PluginPackagePublisherTrustAuthorityConflictError';
|
||||
}
|
||||
}
|
||||
|
||||
export class PluginPackagePublisherTrustAuthorityUnavailableError extends Error {
|
||||
readonly code = 'PLUGIN_PACKAGE_PUBLISHER_TRUST_AUTHORITY_UNAVAILABLE';
|
||||
|
||||
constructor(options?: ErrorOptions) {
|
||||
super('Plugin Package publisher trust authority is unavailable', options);
|
||||
this.name = 'PluginPackagePublisherTrustAuthorityUnavailableError';
|
||||
}
|
||||
}
|
||||
|
||||
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
|
||||
const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||
const PUBLISHER_PATTERN =
|
||||
/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/;
|
||||
const TRUST_DIGEST_DOMAIN =
|
||||
'qinglong/plugin-package-publisher-trust-keyset-digest@v1\0';
|
||||
const TRUST_HEAD_DIGEST_DOMAIN =
|
||||
'qinglong/plugin-package-publisher-trust-head-digest@v1\0';
|
||||
|
||||
function invalid(message: string): never {
|
||||
throw new InvalidPluginPackagePublisherTrustSnapshotError(message);
|
||||
}
|
||||
|
||||
function exactObject(
|
||||
value: unknown,
|
||||
keys: readonly string[],
|
||||
label: string,
|
||||
): Record<string, unknown> {
|
||||
if (
|
||||
!value ||
|
||||
typeof value !== 'object' ||
|
||||
Array.isArray(value) ||
|
||||
(Object.getPrototypeOf(value) !== Object.prototype &&
|
||||
Object.getPrototypeOf(value) !== null)
|
||||
) {
|
||||
return invalid(`${label} must be an object`);
|
||||
}
|
||||
const actual = Reflect.ownKeys(value);
|
||||
const expected = [...keys].sort();
|
||||
if (
|
||||
actual.some((key) => typeof key !== 'string') ||
|
||||
actual.length !== expected.length ||
|
||||
actual
|
||||
.map(String)
|
||||
.sort()
|
||||
.some((key, index) => key !== expected[index])
|
||||
) {
|
||||
return invalid(`${label} shape is invalid`);
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function publisher(value: unknown): string {
|
||||
if (
|
||||
typeof value !== 'string' ||
|
||||
Buffer.byteLength(value, 'utf8') > 253 ||
|
||||
!PUBLISHER_PATTERN.test(value)
|
||||
) {
|
||||
return invalid('publisher is invalid');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function keyId(value: unknown): string {
|
||||
if (typeof value !== 'string' || !IDENTIFIER_PATTERN.test(value)) {
|
||||
return invalid('keyId is invalid');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function timestamp(value: unknown, label: string): number {
|
||||
if (!Number.isSafeInteger(value) || (value as number) < 0) {
|
||||
return invalid(`${label} is invalid`);
|
||||
}
|
||||
return value as number;
|
||||
}
|
||||
|
||||
function positiveInteger(value: unknown, label: string): number {
|
||||
if (!Number.isSafeInteger(value) || (value as number) < 1) {
|
||||
return invalid(`${label} is invalid`);
|
||||
}
|
||||
return value as number;
|
||||
}
|
||||
|
||||
function digest(value: unknown, label: string): string {
|
||||
if (typeof value !== 'string' || !DIGEST_PATTERN.test(value)) {
|
||||
return invalid(`${label} is invalid`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function compareKeys(
|
||||
left: Readonly<PluginPackagePublisherTrustSnapshotKey>,
|
||||
right: Readonly<PluginPackagePublisherTrustSnapshotKey>,
|
||||
): number {
|
||||
return (
|
||||
Buffer.compare(
|
||||
Buffer.from(left.publisher, 'utf8'),
|
||||
Buffer.from(right.publisher, 'utf8'),
|
||||
) ||
|
||||
Buffer.compare(
|
||||
Buffer.from(left.keyId, 'utf8'),
|
||||
Buffer.from(right.keyId, 'utf8'),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeKey(
|
||||
value: PluginPackagePublisherTrustSnapshotKey,
|
||||
): Readonly<PluginPackagePublisherTrustSnapshotKey> {
|
||||
exactObject(
|
||||
value,
|
||||
[
|
||||
'publisher',
|
||||
'keyId',
|
||||
'publicKeyDigest',
|
||||
'notBeforeMs',
|
||||
'notAfterMs',
|
||||
],
|
||||
'snapshot key',
|
||||
);
|
||||
const notBeforeMs = timestamp(value.notBeforeMs, 'notBeforeMs');
|
||||
const notAfterMs = timestamp(value.notAfterMs, 'notAfterMs');
|
||||
if (notAfterMs <= notBeforeMs) {
|
||||
return invalid('key lifetime is invalid');
|
||||
}
|
||||
return Object.freeze({
|
||||
publisher: publisher(value.publisher),
|
||||
keyId: keyId(value.keyId),
|
||||
publicKeyDigest: digest(value.publicKeyDigest, 'publicKeyDigest'),
|
||||
notBeforeMs,
|
||||
notAfterMs,
|
||||
});
|
||||
}
|
||||
|
||||
export function pluginPackagePublisherTrustKeysetDigest(
|
||||
keysValue: readonly Readonly<PluginPackagePublisherTrustSnapshotKey>[],
|
||||
): string {
|
||||
if (
|
||||
!Array.isArray(keysValue) ||
|
||||
Object.keys(keysValue).some((key, index) => key !== String(index))
|
||||
) {
|
||||
return invalid('keyset is invalid');
|
||||
}
|
||||
const keys = keysValue.map(normalizeKey);
|
||||
if (
|
||||
keys.some(
|
||||
(key, index) => index > 0 && compareKeys(keys[index - 1]!, key) >= 0,
|
||||
)
|
||||
) {
|
||||
return invalid('keyset must be unique and sorted');
|
||||
}
|
||||
return createHash('sha256')
|
||||
.update(TRUST_DIGEST_DOMAIN)
|
||||
.update(JSON.stringify(keys))
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
export function normalizePluginPackagePublisherTrustSnapshot(
|
||||
value: PluginPackagePublisherTrustSnapshot,
|
||||
): Readonly<PluginPackagePublisherTrustSnapshot> {
|
||||
exactObject(value, ['schema', 'keys', 'snapshotDigest'], 'snapshot');
|
||||
if (
|
||||
value.schema !== PLUGIN_PACKAGE_PUBLISHER_TRUST_SNAPSHOT_SCHEMA ||
|
||||
!Array.isArray(value.keys) ||
|
||||
value.keys.length > MAX_PLUGIN_PACKAGE_PUBLISHER_KEYS ||
|
||||
Object.keys(value.keys).some((key, index) => key !== String(index))
|
||||
) {
|
||||
return invalid('snapshot schema or keys are invalid');
|
||||
}
|
||||
const keys = Object.freeze(value.keys.map(normalizeKey));
|
||||
const snapshotDigest = pluginPackagePublisherTrustKeysetDigest(keys);
|
||||
if (value.snapshotDigest !== snapshotDigest) {
|
||||
return invalid('snapshotDigest does not match keys');
|
||||
}
|
||||
return Object.freeze({
|
||||
schema: PLUGIN_PACKAGE_PUBLISHER_TRUST_SNAPSHOT_SCHEMA,
|
||||
keys,
|
||||
snapshotDigest,
|
||||
});
|
||||
}
|
||||
|
||||
export function createPluginPackagePublisherTrustSnapshot(
|
||||
definitions: readonly Readonly<PluginPackagePublisherKeyDefinition>[],
|
||||
): Readonly<PluginPackagePublisherTrustSnapshot> {
|
||||
try {
|
||||
new PluginPackagePublisherTrustRegistry(definitions);
|
||||
} catch {
|
||||
return invalid('publisher key definitions are invalid');
|
||||
}
|
||||
const keys = Object.freeze(
|
||||
definitions
|
||||
.map((definition) => {
|
||||
let publicKeyDigest: string;
|
||||
try {
|
||||
publicKeyDigest = createHash('sha256')
|
||||
.update(
|
||||
createPublicKey(definition.publicKeyPem).export({
|
||||
type: 'spki',
|
||||
format: 'der',
|
||||
}),
|
||||
)
|
||||
.digest('hex');
|
||||
} catch {
|
||||
return invalid('publisher public key is invalid');
|
||||
}
|
||||
return normalizeKey({
|
||||
publisher: definition.publisher,
|
||||
keyId: definition.keyId,
|
||||
publicKeyDigest,
|
||||
notBeforeMs: definition.notBeforeMs,
|
||||
notAfterMs: definition.notAfterMs,
|
||||
});
|
||||
})
|
||||
.sort(compareKeys),
|
||||
);
|
||||
const unsigned = Object.freeze({
|
||||
schema: PLUGIN_PACKAGE_PUBLISHER_TRUST_SNAPSHOT_SCHEMA,
|
||||
keys,
|
||||
});
|
||||
return normalizePluginPackagePublisherTrustSnapshot({
|
||||
...unsigned,
|
||||
snapshotDigest: pluginPackagePublisherTrustKeysetDigest(keys),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Treats the mounted document as public-key material only and lets the durable
|
||||
* effective snapshot decide which identities are trusted. Extra candidate
|
||||
* keys in the document are deliberately excluded.
|
||||
*/
|
||||
export function createPluginPackagePublisherEffectiveTrustRegistry(
|
||||
definitions: readonly Readonly<PluginPackagePublisherKeyDefinition>[],
|
||||
effectiveSnapshotValue: PluginPackagePublisherTrustSnapshot,
|
||||
): PluginPackagePublisherTrustRegistry {
|
||||
const materialSnapshot =
|
||||
createPluginPackagePublisherTrustSnapshot(definitions);
|
||||
const effectiveSnapshot =
|
||||
normalizePluginPackagePublisherTrustSnapshot(effectiveSnapshotValue);
|
||||
const definitionsByIdentity = new Map(
|
||||
definitions.map((definition) => [
|
||||
`${definition.publisher}\0${definition.keyId}`,
|
||||
definition,
|
||||
]),
|
||||
);
|
||||
const materialKeys = new Map(
|
||||
materialSnapshot.keys.map((key) => [
|
||||
`${key.publisher}\0${key.keyId}`,
|
||||
Object.freeze({
|
||||
key,
|
||||
definition: definitionsByIdentity.get(
|
||||
`${key.publisher}\0${key.keyId}`,
|
||||
),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
const selected: PluginPackagePublisherKeyDefinition[] = [];
|
||||
for (const effectiveKey of effectiveSnapshot.keys) {
|
||||
const material = materialKeys.get(
|
||||
`${effectiveKey.publisher}\0${effectiveKey.keyId}`,
|
||||
);
|
||||
if (
|
||||
!material ||
|
||||
JSON.stringify(material.key) !== JSON.stringify(effectiveKey) ||
|
||||
!material.definition
|
||||
) {
|
||||
return invalid(
|
||||
'effective trust snapshot is not backed by mounted key material',
|
||||
);
|
||||
}
|
||||
selected.push({ ...material.definition });
|
||||
}
|
||||
if (selected.length < 1) {
|
||||
return invalid('effective trust snapshot must contain one key');
|
||||
}
|
||||
return new PluginPackagePublisherTrustRegistry(selected);
|
||||
}
|
||||
|
||||
function trustSnapshotFromKeys(
|
||||
keys: readonly Readonly<PluginPackagePublisherTrustSnapshotKey>[],
|
||||
): Readonly<PluginPackagePublisherTrustSnapshot> {
|
||||
const canonical = Object.freeze([...keys].sort(compareKeys));
|
||||
return normalizePluginPackagePublisherTrustSnapshot({
|
||||
schema: PLUGIN_PACKAGE_PUBLISHER_TRUST_SNAPSHOT_SCHEMA,
|
||||
keys: canonical,
|
||||
snapshotDigest: pluginPackagePublisherTrustKeysetDigest(canonical),
|
||||
});
|
||||
}
|
||||
|
||||
export function createPluginPackagePublisherTrustOverlapAdditionSnapshot(
|
||||
currentValue: PluginPackagePublisherTrustSnapshot,
|
||||
candidateValue: PluginPackagePublisherTrustSnapshot,
|
||||
publisherValue: string,
|
||||
keyIdValue: string,
|
||||
observedAtMsValue: number,
|
||||
): Readonly<PluginPackagePublisherTrustSnapshot> {
|
||||
const current =
|
||||
normalizePluginPackagePublisherTrustSnapshot(currentValue);
|
||||
const candidate =
|
||||
normalizePluginPackagePublisherTrustSnapshot(candidateValue);
|
||||
const targetPublisher = publisher(publisherValue);
|
||||
const targetKeyId = keyId(keyIdValue);
|
||||
const observedAtMs = timestamp(observedAtMsValue, 'observedAtMs');
|
||||
const currentKeys = new Map(
|
||||
current.keys.map((key) => [`${key.publisher}\0${key.keyId}`, key]),
|
||||
);
|
||||
const added = candidate.keys.filter(
|
||||
(key) => !currentKeys.has(`${key.publisher}\0${key.keyId}`),
|
||||
);
|
||||
if (
|
||||
candidate.keys.length !== current.keys.length + 1 ||
|
||||
current.keys.some((key) => {
|
||||
const next = candidate.keys.find(
|
||||
(candidateKey) =>
|
||||
candidateKey.publisher === key.publisher &&
|
||||
candidateKey.keyId === key.keyId,
|
||||
);
|
||||
return !next || JSON.stringify(next) !== JSON.stringify(key);
|
||||
}) ||
|
||||
added.length !== 1 ||
|
||||
added[0]!.publisher !== targetPublisher ||
|
||||
added[0]!.keyId !== targetKeyId ||
|
||||
added[0]!.notBeforeMs > observedAtMs ||
|
||||
observedAtMs >= added[0]!.notAfterMs
|
||||
) {
|
||||
return invalid(
|
||||
'overlap addition must preserve every key and add one active target',
|
||||
);
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
export function createPluginPackagePublisherTrustRetirementSnapshot(
|
||||
currentValue: PluginPackagePublisherTrustSnapshot,
|
||||
publisherValue: string,
|
||||
keyIdValue: string,
|
||||
observedAtMsValue: number,
|
||||
): Readonly<PluginPackagePublisherTrustSnapshot> {
|
||||
const current =
|
||||
normalizePluginPackagePublisherTrustSnapshot(currentValue);
|
||||
const targetPublisher = publisher(publisherValue);
|
||||
const targetKeyId = keyId(keyIdValue);
|
||||
const observedAtMs = timestamp(observedAtMsValue, 'observedAtMs');
|
||||
const remaining = current.keys.filter(
|
||||
(key) =>
|
||||
key.publisher !== targetPublisher || key.keyId !== targetKeyId,
|
||||
);
|
||||
if (
|
||||
remaining.length !== current.keys.length - 1 ||
|
||||
!remaining.some(
|
||||
(key) =>
|
||||
key.publisher === targetPublisher &&
|
||||
key.notBeforeMs <= observedAtMs &&
|
||||
observedAtMs < key.notAfterMs,
|
||||
)
|
||||
) {
|
||||
return invalid(
|
||||
'retirement must remove one target and retain an active publisher key',
|
||||
);
|
||||
}
|
||||
return trustSnapshotFromKeys(remaining);
|
||||
}
|
||||
|
||||
export function createPluginPackagePublisherEffectiveTrustSnapshot(
|
||||
snapshotValue: PluginPackagePublisherTrustSnapshot,
|
||||
revokedValue: readonly Readonly<PluginPackagePublisherTrustKeyRef>[],
|
||||
): Readonly<PluginPackagePublisherTrustSnapshot> {
|
||||
const snapshot =
|
||||
normalizePluginPackagePublisherTrustSnapshot(snapshotValue);
|
||||
if (
|
||||
!Array.isArray(revokedValue) ||
|
||||
Object.keys(revokedValue).some((key, index) => key !== String(index))
|
||||
) {
|
||||
return invalid('revoked key references are invalid');
|
||||
}
|
||||
const revoked = new Set<string>();
|
||||
for (const value of revokedValue) {
|
||||
exactObject(value, ['publisher', 'keyId'], 'revoked key reference');
|
||||
const identity = `${publisher(value.publisher)}\0${keyId(value.keyId)}`;
|
||||
if (revoked.has(identity)) {
|
||||
return invalid('revoked key references must be unique');
|
||||
}
|
||||
revoked.add(identity);
|
||||
}
|
||||
const keys = Object.freeze(
|
||||
snapshot.keys.filter(
|
||||
(key) => !revoked.has(`${key.publisher}\0${key.keyId}`),
|
||||
),
|
||||
);
|
||||
return normalizePluginPackagePublisherTrustSnapshot({
|
||||
schema: PLUGIN_PACKAGE_PUBLISHER_TRUST_SNAPSHOT_SCHEMA,
|
||||
keys,
|
||||
snapshotDigest: pluginPackagePublisherTrustKeysetDigest(keys),
|
||||
});
|
||||
}
|
||||
|
||||
function trustHeadDigest(
|
||||
value: Omit<PluginPackagePublisherTrustHead, 'headDigest'>,
|
||||
): string {
|
||||
return createHash('sha256')
|
||||
.update(TRUST_HEAD_DIGEST_DOMAIN)
|
||||
.update(JSON.stringify(value))
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
export function normalizePluginPackagePublisherTrustHead(
|
||||
value: PluginPackagePublisherTrustHead,
|
||||
): Readonly<PluginPackagePublisherTrustHead> {
|
||||
exactObject(
|
||||
value,
|
||||
[
|
||||
'schema',
|
||||
'authorityId',
|
||||
'generation',
|
||||
'baseSnapshotDigest',
|
||||
'effectiveTrustDigest',
|
||||
'updatedAtMs',
|
||||
'headDigest',
|
||||
],
|
||||
'trust head',
|
||||
);
|
||||
if (value.schema !== PLUGIN_PACKAGE_PUBLISHER_TRUST_HEAD_SCHEMA) {
|
||||
return invalid('trust head schema is invalid');
|
||||
}
|
||||
const normalized = Object.freeze({
|
||||
schema: PLUGIN_PACKAGE_PUBLISHER_TRUST_HEAD_SCHEMA,
|
||||
authorityId: keyId(value.authorityId),
|
||||
generation: positiveInteger(value.generation, 'generation'),
|
||||
baseSnapshotDigest: digest(
|
||||
value.baseSnapshotDigest,
|
||||
'baseSnapshotDigest',
|
||||
),
|
||||
effectiveTrustDigest: digest(
|
||||
value.effectiveTrustDigest,
|
||||
'effectiveTrustDigest',
|
||||
),
|
||||
updatedAtMs: timestamp(value.updatedAtMs, 'updatedAtMs'),
|
||||
});
|
||||
const headDigest = trustHeadDigest(normalized);
|
||||
if (value.headDigest !== headDigest) {
|
||||
return invalid('headDigest does not match trust head');
|
||||
}
|
||||
return Object.freeze({ ...normalized, headDigest });
|
||||
}
|
||||
|
||||
export function createPluginPackagePublisherTrustHead(
|
||||
authorityIdValue: string,
|
||||
snapshotValue: PluginPackagePublisherTrustSnapshot,
|
||||
updatedAtMsValue: number,
|
||||
): Readonly<PluginPackagePublisherTrustHead> {
|
||||
const snapshot =
|
||||
normalizePluginPackagePublisherTrustSnapshot(snapshotValue);
|
||||
if (snapshot.keys.length < 1) {
|
||||
return invalid('base trust snapshot must contain one key');
|
||||
}
|
||||
const unsigned = Object.freeze({
|
||||
schema: PLUGIN_PACKAGE_PUBLISHER_TRUST_HEAD_SCHEMA,
|
||||
authorityId: keyId(authorityIdValue),
|
||||
generation: 1,
|
||||
baseSnapshotDigest: snapshot.snapshotDigest,
|
||||
effectiveTrustDigest: snapshot.snapshotDigest,
|
||||
updatedAtMs: timestamp(updatedAtMsValue, 'updatedAtMs'),
|
||||
});
|
||||
return normalizePluginPackagePublisherTrustHead({
|
||||
...unsigned,
|
||||
headDigest: trustHeadDigest(unsigned),
|
||||
});
|
||||
}
|
||||
|
||||
export function advancePluginPackagePublisherTrustHead(
|
||||
headValue: PluginPackagePublisherTrustHead,
|
||||
effectiveSnapshotValue: PluginPackagePublisherTrustSnapshot,
|
||||
updatedAtMsValue: number,
|
||||
): Readonly<PluginPackagePublisherTrustHead> {
|
||||
const head = normalizePluginPackagePublisherTrustHead(headValue);
|
||||
const effectiveSnapshot =
|
||||
normalizePluginPackagePublisherTrustSnapshot(effectiveSnapshotValue);
|
||||
const updatedAtMs = timestamp(updatedAtMsValue, 'updatedAtMs');
|
||||
if (
|
||||
effectiveSnapshot.snapshotDigest === head.effectiveTrustDigest ||
|
||||
updatedAtMs < head.updatedAtMs ||
|
||||
head.generation >= 2_147_483_647
|
||||
) {
|
||||
return invalid('trust head transition is invalid');
|
||||
}
|
||||
const unsigned = Object.freeze({
|
||||
schema: PLUGIN_PACKAGE_PUBLISHER_TRUST_HEAD_SCHEMA,
|
||||
authorityId: head.authorityId,
|
||||
generation: head.generation + 1,
|
||||
baseSnapshotDigest: head.baseSnapshotDigest,
|
||||
effectiveTrustDigest: effectiveSnapshot.snapshotDigest,
|
||||
updatedAtMs,
|
||||
});
|
||||
return normalizePluginPackagePublisherTrustHead({
|
||||
...unsigned,
|
||||
headDigest: trustHeadDigest(unsigned),
|
||||
});
|
||||
}
|
||||
|
||||
export function pluginPackagePublisherTrustRevokedDigest(
|
||||
snapshotValue: PluginPackagePublisherTrustSnapshot,
|
||||
publisherValue: string,
|
||||
keyIdValue: string,
|
||||
): string {
|
||||
const snapshot =
|
||||
normalizePluginPackagePublisherTrustSnapshot(snapshotValue);
|
||||
const targetPublisher = publisher(publisherValue);
|
||||
const targetKeyId = keyId(keyIdValue);
|
||||
const remaining = snapshot.keys.filter(
|
||||
(key) =>
|
||||
key.publisher !== targetPublisher || key.keyId !== targetKeyId,
|
||||
);
|
||||
if (remaining.length === snapshot.keys.length) {
|
||||
return invalid('revoked key is absent from the trust snapshot');
|
||||
}
|
||||
return pluginPackagePublisherTrustKeysetDigest(remaining);
|
||||
}
|
||||
Reference in New Issue
Block a user