update-verifier.mjs
sha256:bd1eb24434d281fe65142038c1bf217493421f5a86880c22dff7e6037d2d4700
feat(companion): add production macOS distribution path
Human
minor
⚠ breaking
24 days ago
| 1 | import crypto from 'node:crypto'; |
| 2 | |
| 3 | const HEX_256 = /^[0-9a-f]{64}$/; |
| 4 | |
| 5 | function reject(reason) { |
| 6 | const error = new Error(reason); |
| 7 | error.code = reason; |
| 8 | throw error; |
| 9 | } |
| 10 | |
| 11 | export function verifyUpdateSignature({ manifestBytes, signature, publicKey }) { |
| 12 | if (!Buffer.isBuffer(manifestBytes) || !Buffer.isBuffer(signature)) { |
| 13 | return false; |
| 14 | } |
| 15 | try { |
| 16 | return crypto.verify(null, manifestBytes, publicKey, signature); |
| 17 | } catch { |
| 18 | return false; |
| 19 | } |
| 20 | } |
| 21 | |
| 22 | export function parseUpdateManifest(manifestBytes) { |
| 23 | if (!Buffer.isBuffer(manifestBytes) || manifestBytes.length === 0 || manifestBytes.length > 131_072) { |
| 24 | reject('update_manifest_malformed'); |
| 25 | } |
| 26 | let value; |
| 27 | try { |
| 28 | value = JSON.parse(manifestBytes.toString('utf8')); |
| 29 | } catch { |
| 30 | reject('update_manifest_malformed'); |
| 31 | } |
| 32 | if (!value || typeof value !== 'object' || Array.isArray(value)) { |
| 33 | reject('update_manifest_malformed'); |
| 34 | } |
| 35 | return value; |
| 36 | } |
| 37 | |
| 38 | export function evaluateUpdateManifest(manifest, context) { |
| 39 | if (manifest?.schema !== 1) reject('update_schema_unsupported'); |
| 40 | if (manifest.channel !== context.channel) reject('update_channel_mismatch'); |
| 41 | if (manifest.platform !== context.platform) reject('update_platform_mismatch'); |
| 42 | if (manifest.architecture !== context.architecture) reject('update_architecture_mismatch'); |
| 43 | if (manifest.keyID !== context.keyID) reject('update_key_mismatch'); |
| 44 | if (!Number.isSafeInteger(manifest.issuedAt) || manifest.issuedAt > context.now) reject('update_not_yet_valid'); |
| 45 | if (!Number.isSafeInteger(manifest.expiresAt) || manifest.expiresAt < context.now || manifest.expiresAt <= manifest.issuedAt) { |
| 46 | reject('update_expired'); |
| 47 | } |
| 48 | if (!Number.isSafeInteger(manifest.source?.minimumBuild) || |
| 49 | !Number.isSafeInteger(manifest.source?.maximumBuild) || |
| 50 | context.currentBuild < manifest.source.minimumBuild || |
| 51 | context.currentBuild > manifest.source.maximumBuild) { |
| 52 | reject('update_source_mismatch'); |
| 53 | } |
| 54 | if (!Number.isSafeInteger(manifest.target?.build) || manifest.target.build <= context.currentBuild) { |
| 55 | reject('update_target_not_newer'); |
| 56 | } |
| 57 | const floor = Math.max(context.persistedFloor, manifest.minimumAcceptedBuild); |
| 58 | if (!Number.isSafeInteger(floor) || manifest.target.build < floor) reject('update_rollback_rejected'); |
| 59 | if (typeof manifest.target.version !== 'string' || !manifest.target.version || |
| 60 | typeof manifest.target.packageURL !== 'string' || |
| 61 | !manifest.target.packageURL.startsWith('https://') || |
| 62 | !HEX_256.test(manifest.target.packageSHA256 ?? '') || |
| 63 | !Number.isSafeInteger(manifest.target.packageBytes) || manifest.target.packageBytes <= 0) { |
| 64 | reject('update_package_invalid'); |
| 65 | } |
| 66 | return Object.freeze({ ...manifest.target, acceptedFloor: floor }); |
| 67 | } |
| 68 | |
| 69 | export async function readPersistedUpdateFloor(custody) { |
| 70 | const raw = await custody.get('knowtation.companion.updateFloor'); |
| 71 | if (raw === null) return 0; |
| 72 | if (!/^[0-9]+$/.test(raw)) reject('update_floor_invalid'); |
| 73 | const floor = Number(raw); |
| 74 | if (!Number.isSafeInteger(floor)) reject('update_floor_invalid'); |
| 75 | return floor; |
| 76 | } |
| 77 | |
| 78 | export async function persistUpdateFloor(custody, build, previousFloor = 0) { |
| 79 | if (!Number.isSafeInteger(build) || build < previousFloor) reject('update_rollback_rejected'); |
| 80 | await custody.set('knowtation.companion.updateFloor', String(build)); |
| 81 | } |
File History
1 commit
sha256:bd1eb24434d281fe65142038c1bf217493421f5a86880c22dff7e6037d2d4700
feat(companion): add production macOS distribution path
Human
minor
⚠
24 days ago