verify-update-manifest.mjs
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