11#!/usr/bin/env node
2+ import { execFile } from 'node:child_process' ;
23import { createHash } from 'node:crypto' ;
34import { createReadStream } from 'node:fs' ;
45import { access , readFile } from 'node:fs/promises' ;
5- import { dirname , resolve } from 'node:path' ;
6+ import { basename , dirname , resolve } from 'node:path' ;
7+ import { promisify } from 'node:util' ;
68import { fileURLToPath } from 'node:url' ;
79
10+ const run = promisify ( execFile ) ;
811const root = resolve ( dirname ( fileURLToPath ( import . meta. url ) ) , '..' ) ;
912const lock = JSON . parse ( await readFile ( resolve ( root , 'opencode-hosted-runtime.lock.json' ) , 'utf8' ) ) ;
10- const provenance = JSON . parse (
11- await readFile ( resolve ( root , 'opencode-hosted-runtime.provenance.json' ) , 'utf8' )
12- ) ;
13- const candidatePath = process . argv [ 2 ] ? resolve ( process . argv [ 2 ] ) : null ;
13+ const candidateManifestPath = process . argv [ 2 ] ? resolve ( process . argv [ 2 ] ) : null ;
14+ const failures = [ ] ;
15+
16+ function check ( value , message ) {
17+ if ( ! value ) failures . push ( message ) ;
18+ }
1419
15- function invariant ( value , message ) {
16- if ( ! value ) throw new Error ( `hosted-opencode-provenance-invalid: ${ message } ` ) ;
20+ async function exists ( path ) {
21+ return access ( path ) . then ( ( ) => true , ( ) => false ) ;
1722}
1823
1924async function sha256File ( path ) {
@@ -22,94 +27,149 @@ async function sha256File(path) {
2227 return hash . digest ( 'hex' ) ;
2328}
2429
25- invariant ( provenance . schemaVersion === 1 , 'schema' ) ;
26- invariant ( provenance . assets . length === 5 , 'asset-count' ) ;
27- invariant ( lock . productionEligible === false , 'eligibility' ) ;
28- invariant ( provenance . release . productionEligible === false , 'provenance-eligibility' ) ;
29- invariant ( lock . releaseRepository === provenance . release . repository , 'repository' ) ;
30- invariant ( lock . version === provenance . release . version , 'version' ) ;
31- invariant ( lock . tag === provenance . release . tag && lock . tag === `v${ lock . version } ` , 'tag' ) ;
32- invariant ( lock . source . commit === provenance . release . sourceCommit , 'source-commit' ) ;
33- invariant ( lock . source . baseCommit === provenance . release . baseCommit , 'base-commit' ) ;
34- invariant ( lock . source . reviewedPatchSha256 === provenance . release . patchSha256 , 'patch' ) ;
30+ async function sha256ArchiveBinary ( path , asset ) {
31+ const archiveKind = path . endsWith ( '.tar.gz' ) ? 'tar.gz' : path . endsWith ( '.zip' ) ? 'zip' : null ;
32+ if ( archiveKind === null ) throw new Error ( 'archive-kind' ) ;
33+ const executable = archiveKind === 'tar.gz' ? '/usr/bin/tar' : '/usr/bin/unzip' ;
34+ const args =
35+ archiveKind === 'tar.gz'
36+ ? [ '-xOzf' , path , asset . binaryPath ]
37+ : [ '-p' , path , asset . binaryPath ] ;
38+ const child = await run ( executable , args , {
39+ encoding : 'buffer' ,
40+ maxBuffer : Math . max ( asset . binarySize + 1024 , 256 * 1024 * 1024 ) ,
41+ } ) ;
42+ return createHash ( 'sha256' ) . update ( child . stdout ) . digest ( 'hex' ) ;
43+ }
3544
36- for ( const asset of provenance . assets ) {
37- const locked = lock . platforms [ asset . platform ] ;
38- invariant ( locked ?. status === 'available' , `lock-platform:${ asset . platform } ` ) ;
39- invariant ( locked . file === asset . archive , `archive-name:${ asset . platform } ` ) ;
40- invariant ( locked . archiveSha256 === asset . archiveSha256 , `archive-hash:${ asset . platform } ` ) ;
41- invariant ( locked . binaryName === asset . binary , `binary-name:${ asset . platform } ` ) ;
42- invariant ( locked . binarySha256 === asset . binarySha256 , `binary-hash:${ asset . platform } ` ) ;
43- invariant (
44- locked . assetUrl ===
45- `https://github.com/${ provenance . release . repository } /releases/download/${ provenance . release . tag } /${ asset . archive } ` ,
46- `tag-url:${ asset . platform } `
47- ) ;
45+ async function verifyAttestation ( subjectPath , repository , platform ) {
46+ const attestationPath = `${ subjectPath } .intoto.jsonl` ;
47+ if ( ! ( await exists ( attestationPath ) ) ) {
48+ failures . push ( `materialized-attestation-missing:${ platform } ` ) ;
49+ return ;
50+ }
51+ try {
52+ await run ( '/usr/bin/gh' , [
53+ 'attestation' ,
54+ 'verify' ,
55+ subjectPath ,
56+ '--repo' ,
57+ repository ,
58+ '--bundle' ,
59+ attestationPath ,
60+ ] ) ;
61+ } catch {
62+ failures . push ( `materialized-attestation-invalid:${ platform } ` ) ;
63+ }
4864}
4965
50- if ( candidatePath ) {
51- const candidateBytes = await readFile ( candidatePath ) ;
52- invariant (
53- createHash ( 'sha256' ) . update ( candidateBytes ) . digest ( 'hex' ) ===
54- provenance . candidateManifestSha256 ,
55- 'candidate-manifest-hash'
56- ) ;
57- const candidate = JSON . parse ( candidateBytes . toString ( 'utf8' ) ) ;
58- invariant ( candidate . release . sourceCommit === provenance . release . sourceCommit , 'candidate-commit' ) ;
59- invariant ( candidate . release . sourceTree === provenance . release . sourceTree , 'candidate-tree' ) ;
60- invariant ( candidate . release . baseCommit === provenance . release . baseCommit , 'candidate-base' ) ;
61- invariant ( candidate . release . patchSha256 === provenance . release . patchSha256 , 'candidate-patch' ) ;
62- invariant ( candidate . release . tag === provenance . release . tag , 'candidate-tag' ) ;
63- invariant ( candidate . release . productionEligible === false , 'candidate-eligibility' ) ;
64- const candidateAssets = new Map (
65- candidate . assets . map ( ( asset ) => [
66- `${ asset . os === 'windows' ? 'win32' : asset . os } -${ asset . arch } ` ,
67- asset ,
68- ] )
66+ check ( candidateManifestPath !== null , 'candidate-manifest-required' ) ;
67+ if ( candidateManifestPath === null ) {
68+ throw new Error ( `hosted-opencode-provenance-invalid:${ failures . join ( ',' ) } ` ) ;
69+ }
70+
71+ const candidateBytes = await readFile ( candidateManifestPath ) ;
72+ const candidate = JSON . parse ( candidateBytes . toString ( 'utf8' ) ) ;
73+ const candidateDirectory = dirname ( candidateManifestPath ) ;
74+ const manifestDigest = createHash ( 'sha256' ) . update ( candidateBytes ) . digest ( 'hex' ) ;
75+
76+ check (
77+ manifestDigest === '608adf3705a367415e0811469bedd41f388034b7e2ea4e42bfa36895593a8486' ,
78+ 'candidate-manifest-hash'
79+ ) ;
80+ check ( candidate . schemaVersion === 1 , 'candidate-schema' ) ;
81+ check ( candidate . release ?. productionEligible === false , 'candidate-eligibility' ) ;
82+ check ( candidate . workflow ?. repository === '777genius/opencode-anomaly' , 'candidate-repository' ) ;
83+ check ( candidate . workflow ?. workflow === 'hardened CLI prerelease' , 'candidate-workflow' ) ;
84+ check ( / ^ 3 1 8 2 4 3 0 8 7 9 5 $ / . test ( candidate . workflow ?. runId ?? '' ) , 'candidate-workflow-run' ) ;
85+ check ( candidate . workflow ?. runAttempt === '1' , 'candidate-workflow-attempt' ) ;
86+ check ( candidate . workflow ?. actor === '777genius' , 'candidate-workflow-actor' ) ;
87+ check ( candidate . workflow ?. ref === 'refs/pull/2/merge' , 'candidate-workflow-ref' ) ;
88+ check (
89+ candidate . workflow ?. sha === 'a9145f4407abe4cbfefe6703cb53389a56293844' ,
90+ 'candidate-workflow-sha'
91+ ) ;
92+ check ( candidate . release ?. tag === `v${ candidate . release ?. version } ` , 'candidate-tag' ) ;
93+ check (
94+ candidate . release ?. sourceCommit === '476b667c385210b19fbd15bcb57456cacb0ae9e7' ,
95+ 'candidate-source-commit'
96+ ) ;
97+ check (
98+ candidate . release ?. sourceTree === '122dd7d77fd01f1a054ac52666a1e9a8a5529dcb' ,
99+ 'candidate-source-tree'
100+ ) ;
101+ check (
102+ candidate . release ?. baseCommit === '49c69c5ed3ccf706b61b3febb43c8aaff7f8325e' ,
103+ 'candidate-base-commit'
104+ ) ;
105+ check (
106+ candidate . release ?. patchSha256 ===
107+ 'dbd8b2c1eda38043e3bfc9e2b809f4ef393fa075349ed219109a7deaca0c590e' ,
108+ 'candidate-patch'
109+ ) ;
110+ check ( Array . isArray ( candidate . assets ) && candidate . assets . length === 5 , 'candidate-assets' ) ;
111+ await verifyAttestation ( candidateManifestPath , candidate . workflow ?. repository , 'release-manifest' ) ;
112+ const patchPath = resolve ( candidateDirectory , 'reviewed.patch' ) ;
113+ if ( ! ( await exists ( patchPath ) ) ) {
114+ failures . push ( 'materialized-reviewed-patch-missing' ) ;
115+ } else {
116+ check ( ( await sha256File ( patchPath ) ) === candidate . release ?. patchSha256 , 'materialized-patch-hash' ) ;
117+ }
118+
119+ check ( lock . productionEligible === false , 'lock-eligibility' ) ;
120+ check ( lock . releaseRepository === candidate . workflow ?. repository , 'lock-repository' ) ;
121+ check ( lock . version === candidate . release ?. version , 'lock-version' ) ;
122+ check ( lock . tag === candidate . release ?. tag , 'lock-tag' ) ;
123+ check ( lock . source ?. commit === candidate . release ?. sourceCommit , 'lock-source-commit' ) ;
124+ check ( lock . source ?. baseCommit === candidate . release ?. baseCommit , 'lock-base-commit' ) ;
125+ check ( lock . source ?. reviewedPatchSha256 === candidate . release ?. patchSha256 , 'lock-patch' ) ;
126+
127+ const platforms = new Set ( ) ;
128+
129+ for ( const asset of candidate . assets ?? [ ] ) {
130+ const platform = `${ asset . os === 'windows' ? 'win32' : asset . os } -${ asset . arch } ` ;
131+ check ( ! platforms . has ( platform ) , `candidate-platform-duplicate:${ platform } ` ) ;
132+ platforms . add ( platform ) ;
133+ const locked = lock . platforms ?. [ platform ] ;
134+ check ( locked ?. status === 'available' , `lock-platform:${ platform } ` ) ;
135+ check ( locked ?. file === asset . archive , `lock-archive:${ platform } ` ) ;
136+ check ( locked ?. archiveSha256 === asset . archiveSha256 , `lock-archive-hash:${ platform } ` ) ;
137+ check ( locked ?. binaryName === basename ( asset . binaryPath ) , `lock-binary:${ platform } ` ) ;
138+ check ( locked ?. binarySha256 === asset . binarySha256 , `lock-binary-hash:${ platform } ` ) ;
139+ check (
140+ locked ?. assetUrl ===
141+ `https://github.com/${ candidate . workflow . repository } /releases/download/${ candidate . release . tag } /${ asset . archive } ` ,
142+ `lock-tag-url:${ platform } `
69143 ) ;
70- for ( const expected of provenance . assets ) {
71- const actual = candidateAssets . get ( expected . platform ) ;
72- invariant ( actual ?. archive === expected . archive , `candidate-archive:${ expected . platform } ` ) ;
73- invariant (
74- actual . archiveSha256 === expected . archiveSha256 ,
75- `candidate-archive-hash:${ expected . platform } `
76- ) ;
77- invariant ( actual . binaryPath === expected . binary , `candidate-binary:${ expected . platform } ` ) ;
78- invariant (
79- actual . binarySha256 === expected . binarySha256 ,
80- `candidate-binary-hash:${ expected . platform } `
81- ) ;
82- }
83- const candidateDirectory = dirname ( candidatePath ) ;
84- for ( const expected of provenance . assets ) {
85- const archivePath = resolve ( candidateDirectory , expected . archive ) ;
86- try {
87- await access ( archivePath ) ;
88- invariant (
89- ( await sha256File ( archivePath ) ) === expected . archiveSha256 ,
90- `materialized-archive:${ expected . platform } `
91- ) ;
92- } catch ( error ) {
93- if ( error ?. code !== 'ENOENT' ) throw error ;
94- }
144+ const archivePath = resolve ( candidateDirectory , asset . archive ) ;
145+ if ( ! ( await exists ( archivePath ) ) ) {
146+ failures . push ( `materialized-archive-missing:${ platform } ` ) ;
147+ continue ;
95148 }
96- const linuxBinary = resolve ( candidateDirectory , 'opencode' ) ;
149+ check ( ( await sha256File ( archivePath ) ) === asset . archiveSha256 , `materialized-archive-hash: ${ platform } ` ) ;
97150 try {
98- await access ( linuxBinary ) ;
99- const expected = provenance . assets . find ( ( asset ) => asset . platform === 'linux-x64' ) ;
100- invariant (
101- ( await sha256File ( linuxBinary ) ) === expected . binarySha256 ,
102- 'materialized-linux-x64-binary'
151+ check (
152+ ( await sha256ArchiveBinary ( archivePath , asset ) ) === asset . binarySha256 ,
153+ `materialized-binary-hash:${ platform } `
103154 ) ;
104- } catch ( error ) {
105- if ( error ?. code !== 'ENOENT' ) throw error ;
155+ } catch {
156+ failures . push ( `materialized-binary-unverifiable: ${ platform } ` ) ;
106157 }
158+ await verifyAttestation ( archivePath , candidate . workflow . repository , platform ) ;
107159}
108160
161+ if ( failures . length > 0 ) {
162+ throw new Error ( `hosted-opencode-provenance-invalid:${ failures . join ( ',' ) } ` ) ;
163+ }
109164process . stdout . write (
110165 `${ JSON . stringify ( {
111166 verified : true ,
112- assets : provenance . assets . length ,
113- candidate : candidatePath !== null ,
167+ manifestSha256 : manifestDigest ,
168+ repository : candidate . workflow . repository ,
169+ sourceCommit : candidate . release . sourceCommit ,
170+ sourceTree : candidate . release . sourceTree ,
171+ workflowRunId : candidate . workflow . runId ,
172+ tag : candidate . release . tag ,
173+ assets : candidate . assets . length ,
114174 } ) } \n`
115175) ;
0 commit comments