feat(coding): add marketplace package trust primitives
This commit is contained in:
155
electron/coding-plugins/signature-verifier.ts
Normal file
155
electron/coding-plugins/signature-verifier.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { Buffer } from 'node:buffer';
|
||||
import { createHash, createPublicKey, verify } from 'node:crypto';
|
||||
import type { KeyObject } from 'node:crypto';
|
||||
import {
|
||||
isMakeLoreVersionCompatible,
|
||||
parsePluginReleaseDescriptor,
|
||||
serializePluginReleaseDescriptor,
|
||||
type PluginReleaseDescriptor,
|
||||
} from './release-descriptor';
|
||||
import {
|
||||
createCodeOwnedPluginTrustStore,
|
||||
type PluginSigningKeyStore,
|
||||
} from './trusted-keys';
|
||||
|
||||
export type PluginSignatureFailureCode =
|
||||
| 'plugin_signature_invalid'
|
||||
| 'plugin_artifact_invalid'
|
||||
| 'plugin_incompatible_client';
|
||||
|
||||
export type PluginSignatureVerificationResult =
|
||||
| { readonly ok: true }
|
||||
| { readonly ok: false; readonly code: PluginSignatureFailureCode; readonly message: string };
|
||||
|
||||
export interface PluginSignatureVerifierOptions {
|
||||
readonly keyStore?: PluginSigningKeyStore | ReadonlyMap<string, Uint8Array | string>;
|
||||
readonly clientVersion: string;
|
||||
}
|
||||
|
||||
export interface PluginSignatureVerifier {
|
||||
verify(input: {
|
||||
readonly keyId: string;
|
||||
readonly signature: string | Uint8Array;
|
||||
readonly descriptor: PluginReleaseDescriptor;
|
||||
readonly artifact: Uint8Array;
|
||||
}): PluginSignatureVerificationResult;
|
||||
}
|
||||
|
||||
function failure(code: PluginSignatureFailureCode, message: string): PluginSignatureVerificationResult {
|
||||
return { ok: false, code, message };
|
||||
}
|
||||
|
||||
function keyFromStore(
|
||||
keyStore: PluginSigningKeyStore | ReadonlyMap<string, Uint8Array | string>,
|
||||
keyId: string,
|
||||
): Uint8Array | string | null {
|
||||
const key = keyStore.get(keyId);
|
||||
return key === undefined ? null : key;
|
||||
}
|
||||
|
||||
function publicKey(value: Uint8Array | string): KeyObject | null {
|
||||
try {
|
||||
if (typeof value === 'string' && !value.includes('PUBLIC KEY')) return null;
|
||||
const key = typeof value === 'string'
|
||||
? createPublicKey(value)
|
||||
: createPublicKey({ key: Buffer.from(value), format: 'der', type: 'spki' });
|
||||
return key.asymmetricKeyType === 'ed25519' ? key : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function signatureBytes(value: string | Uint8Array): Buffer | null {
|
||||
try {
|
||||
if (typeof value === 'string'
|
||||
&& (value.length !== 86 || !/^[A-Za-z0-9_-]+$/u.test(value))) return null;
|
||||
const bytes = typeof value === 'string' ? Buffer.from(value, 'base64url') : Buffer.from(value);
|
||||
return bytes.length === 64 ? bytes : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function artifactDigest(artifact: Uint8Array): string {
|
||||
return createHash('sha256').update(artifact).digest('hex');
|
||||
}
|
||||
|
||||
export function createPluginSignatureVerifier(
|
||||
options: PluginSignatureVerifierOptions,
|
||||
): PluginSignatureVerifier {
|
||||
const keyStore = options.keyStore ?? createCodeOwnedPluginTrustStore();
|
||||
return {
|
||||
verify(input) {
|
||||
if (!(input.artifact instanceof Uint8Array)) {
|
||||
return failure('plugin_artifact_invalid', 'artifact bytes are invalid');
|
||||
}
|
||||
let descriptorBytes: Uint8Array;
|
||||
try {
|
||||
descriptorBytes = serializePluginReleaseDescriptor(input.descriptor);
|
||||
} catch {
|
||||
return failure('plugin_signature_invalid', 'descriptor is invalid');
|
||||
}
|
||||
if (input.artifact.byteLength !== input.descriptor.artifact.sizeBytes
|
||||
|| artifactDigest(input.artifact) !== input.descriptor.artifact.sha256) {
|
||||
return failure('plugin_artifact_invalid', 'artifact bytes do not match the descriptor');
|
||||
}
|
||||
if (!isCompatibleClient(options.clientVersion, input.descriptor)) {
|
||||
return failure('plugin_incompatible_client', 'client version is outside the Release range');
|
||||
}
|
||||
const encodedKey = keyFromStore(keyStore, input.keyId);
|
||||
if (encodedKey === null) return failure('plugin_signature_invalid', 'signing key is not trusted');
|
||||
const key = publicKey(encodedKey);
|
||||
const signature = signatureBytes(input.signature);
|
||||
if (!key || !signature || !verify(null, descriptorBytes, key, signature)) {
|
||||
return failure('plugin_signature_invalid', 'descriptor signature is invalid');
|
||||
}
|
||||
return { ok: true };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function isCompatibleClient(clientVersion: string, descriptor: PluginReleaseDescriptor): boolean {
|
||||
// Kept local to avoid making the verifier infer a range from untrusted fields.
|
||||
return isMakeLoreVersionCompatible(
|
||||
clientVersion,
|
||||
descriptor.minMakeloreVersion,
|
||||
descriptor.maxMakeloreVersion,
|
||||
);
|
||||
}
|
||||
|
||||
export interface VerifyPluginReleaseSignatureInput {
|
||||
readonly verifier?: PluginSignatureVerifier;
|
||||
readonly keyId: string;
|
||||
readonly signature: string | Uint8Array;
|
||||
readonly descriptor: PluginReleaseDescriptor | Uint8Array | string;
|
||||
readonly artifact: Uint8Array;
|
||||
readonly clientVersion?: string;
|
||||
readonly keyStore?: PluginSigningKeyStore | ReadonlyMap<string, Uint8Array | string>;
|
||||
}
|
||||
|
||||
export function verifyPluginReleaseSignature(
|
||||
input: VerifyPluginReleaseSignatureInput,
|
||||
): PluginSignatureVerificationResult {
|
||||
const descriptor = input.descriptor instanceof Uint8Array || typeof input.descriptor === 'string'
|
||||
? (() => {
|
||||
try {
|
||||
return parsePluginReleaseDescriptor(input.descriptor as Uint8Array | string);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})()
|
||||
: input.descriptor;
|
||||
if (!descriptor) return failure('plugin_signature_invalid', 'descriptor is invalid');
|
||||
const verifier = input.verifier ?? createPluginSignatureVerifier({
|
||||
keyStore: input.keyStore,
|
||||
clientVersion: input.clientVersion ?? '',
|
||||
});
|
||||
return verifier.verify({
|
||||
keyId: input.keyId,
|
||||
signature: input.signature,
|
||||
descriptor,
|
||||
artifact: input.artifact,
|
||||
});
|
||||
}
|
||||
|
||||
export const verifyReleaseSignature = verifyPluginReleaseSignature;
|
||||
Reference in New Issue
Block a user