verify-update-manifest.mjs
52 lines 2.7 KB
Raw
sha256:bd1eb24434d281fe65142038c1bf217493421f5a86880c22dff7e6037d2d4700 feat(companion): add production macOS distribution path Human minor ⚠ breaking 24 days ago
1 #!/usr/bin/env node
2 import crypto from 'node:crypto';
3 import fs from 'node:fs';
4 import path from 'node:path';
5
6 function option(name) {
7 const index = process.argv.indexOf(`--${name}`);
8 if (index < 0 || !process.argv[index + 1]) throw new Error(`missing --${name}`);
9 return process.argv[index + 1];
10 }
11
12 const manifest = fs.readFileSync(option('manifest'));
13 const signature = fs.readFileSync(option('signature'));
14 const publicKey = crypto.createPublicKey(fs.readFileSync(option('public-key-file')));
15 const trust = JSON.parse(fs.readFileSync(option('trust'), 'utf8'));
16 const packageFile = option('package');
17 if (publicKey.asymmetricKeyType !== 'ed25519') throw new Error('update public key must be Ed25519');
18 if (!crypto.verify(null, manifest, publicKey, signature)) throw new Error('update manifest signature invalid');
19 const parsed = JSON.parse(manifest.toString('utf8'));
20 const publicJwk = publicKey.export({ format: 'jwk' });
21 const rawPublicKey = Buffer.from(publicJwk.x, 'base64url').toString('base64');
22 if (trust.algorithm !== 'Ed25519' ||
23 trust.activeKey?.keyID !== parsed.keyID ||
24 trust.activeKey?.publicKey !== rawPublicKey ||
25 trust.revokedKeyIDs?.includes(parsed.keyID)) {
26 throw new Error('update trust, key ID, and public key do not match');
27 }
28 const packageBytes = fs.readFileSync(packageFile);
29 const packageSHA256 = crypto.createHash('sha256').update(packageBytes).digest('hex');
30 if (parsed.target?.packageSHA256 !== packageSHA256 ||
31 parsed.target?.packageBytes !== packageBytes.length ||
32 path.basename(new URL(parsed.target?.packageURL).pathname) !== path.basename(packageFile)) {
33 throw new Error('update package does not match signed manifest');
34 }
35 if (parsed.schema !== 1 || parsed.platform !== 'macos' || parsed.architecture !== 'arm64' ||
36 !Number.isSafeInteger(parsed.issuedAt) || !Number.isSafeInteger(parsed.expiresAt) ||
37 parsed.expiresAt <= parsed.issuedAt || parsed.minimumAcceptedBuild < trust.minimumAcceptedBuild ||
38 parsed.sourceRevision?.status !== 'committed') {
39 throw new Error('update manifest policy is invalid');
40 }
41 const dist = path.dirname(path.resolve(option('manifest')));
42 for (const artifact of parsed.artifacts ?? []) {
43 if (typeof artifact.path !== 'string' || path.basename(artifact.path) !== artifact.path) {
44 throw new Error('update manifest artifact path is invalid');
45 }
46 const bytes = fs.readFileSync(path.join(dist, artifact.path));
47 const artifactHash = crypto.createHash('sha256').update(bytes).digest('hex');
48 if (artifact.sha256 !== artifactHash || artifact.bytes !== bytes.length) {
49 throw new Error(`release artifact does not match signed manifest: ${artifact.path}`);
50 }
51 }
52 process.stdout.write('update manifest signature, trust, source, and package verified\n');
File History 1 commit
sha256:bd1eb24434d281fe65142038c1bf217493421f5a86880c22dff7e6037d2d4700 feat(companion): add production macOS distribution path Human minor ⚠ 24 days ago