UNPKG

9.7 kBJavaScriptView Raw
1const sigstore = require('sigstore')
2const { readFile } = require('fs/promises')
3const ci = require('ci-info')
4const { env } = process
5
6const INTOTO_PAYLOAD_TYPE = 'application/vnd.in-toto+json'
7const INTOTO_STATEMENT_V01_TYPE = 'https://in-toto.io/Statement/v0.1'
8const INTOTO_STATEMENT_V1_TYPE = 'https://in-toto.io/Statement/v1'
9const SLSA_PREDICATE_V02_TYPE = 'https://slsa.dev/provenance/v0.2'
10const SLSA_PREDICATE_V1_TYPE = 'https://slsa.dev/provenance/v1'
11
12const GITHUB_BUILDER_ID_PREFIX = 'https://github.com/actions/runner'
13const GITHUB_BUILD_TYPE = 'https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1'
14
15const GITLAB_BUILD_TYPE_PREFIX = 'https://github.com/npm/cli/gitlab'
16const GITLAB_BUILD_TYPE_VERSION = 'v0alpha1'
17
18const generateProvenance = async (subject, opts) => {
19 let payload
20 if (ci.GITHUB_ACTIONS) {
21 /* istanbul ignore next - not covering missing env var case */
22 const relativeRef = (env.GITHUB_WORKFLOW_REF || '').replace(env.GITHUB_REPOSITORY + '/', '')
23 const delimiterIndex = relativeRef.indexOf('@')
24 const workflowPath = relativeRef.slice(0, delimiterIndex)
25 const workflowRef = relativeRef.slice(delimiterIndex + 1)
26
27 payload = {
28 _type: INTOTO_STATEMENT_V1_TYPE,
29 subject,
30 predicateType: SLSA_PREDICATE_V1_TYPE,
31 predicate: {
32 buildDefinition: {
33 buildType: GITHUB_BUILD_TYPE,
34 externalParameters: {
35 workflow: {
36 ref: workflowRef,
37 repository: `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}`,
38 path: workflowPath,
39 },
40 },
41 internalParameters: {
42 github: {
43 event_name: env.GITHUB_EVENT_NAME,
44 repository_id: env.GITHUB_REPOSITORY_ID,
45 repository_owner_id: env.GITHUB_REPOSITORY_OWNER_ID,
46 },
47 },
48 resolvedDependencies: [
49 {
50 uri: `git+${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}@${env.GITHUB_REF}`,
51 digest: {
52 gitCommit: env.GITHUB_SHA,
53 },
54 },
55 ],
56 },
57 runDetails: {
58 builder: { id: `${GITHUB_BUILDER_ID_PREFIX}/${env.RUNNER_ENVIRONMENT}` },
59 metadata: {
60 /* eslint-disable-next-line max-len */
61 invocationId: `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}/attempts/${env.GITHUB_RUN_ATTEMPT}`,
62 },
63 },
64 },
65 }
66 }
67 if (ci.GITLAB) {
68 payload = {
69 _type: INTOTO_STATEMENT_V01_TYPE,
70 subject,
71 predicateType: SLSA_PREDICATE_V02_TYPE,
72 predicate: {
73 buildType: `${GITLAB_BUILD_TYPE_PREFIX}/${GITLAB_BUILD_TYPE_VERSION}`,
74 builder: { id: `${env.CI_PROJECT_URL}/-/runners/${env.CI_RUNNER_ID}` },
75 invocation: {
76 configSource: {
77 uri: `git+${env.CI_PROJECT_URL}`,
78 digest: {
79 sha1: env.CI_COMMIT_SHA,
80 },
81 entryPoint: env.CI_JOB_NAME,
82 },
83 parameters: {
84 CI: env.CI,
85 CI_API_GRAPHQL_URL: env.CI_API_GRAPHQL_URL,
86 CI_API_V4_URL: env.CI_API_V4_URL,
87 CI_BUILD_BEFORE_SHA: env.CI_BUILD_BEFORE_SHA,
88 CI_BUILD_ID: env.CI_BUILD_ID,
89 CI_BUILD_NAME: env.CI_BUILD_NAME,
90 CI_BUILD_REF: env.CI_BUILD_REF,
91 CI_BUILD_REF_NAME: env.CI_BUILD_REF_NAME,
92 CI_BUILD_REF_SLUG: env.CI_BUILD_REF_SLUG,
93 CI_BUILD_STAGE: env.CI_BUILD_STAGE,
94 CI_COMMIT_BEFORE_SHA: env.CI_COMMIT_BEFORE_SHA,
95 CI_COMMIT_BRANCH: env.CI_COMMIT_BRANCH,
96 CI_COMMIT_REF_NAME: env.CI_COMMIT_REF_NAME,
97 CI_COMMIT_REF_PROTECTED: env.CI_COMMIT_REF_PROTECTED,
98 CI_COMMIT_REF_SLUG: env.CI_COMMIT_REF_SLUG,
99 CI_COMMIT_SHA: env.CI_COMMIT_SHA,
100 CI_COMMIT_SHORT_SHA: env.CI_COMMIT_SHORT_SHA,
101 CI_COMMIT_TIMESTAMP: env.CI_COMMIT_TIMESTAMP,
102 CI_COMMIT_TITLE: env.CI_COMMIT_TITLE,
103 CI_CONFIG_PATH: env.CI_CONFIG_PATH,
104 CI_DEFAULT_BRANCH: env.CI_DEFAULT_BRANCH,
105 CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX:
106 env.CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX,
107 CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX: env.CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX,
108 CI_DEPENDENCY_PROXY_SERVER: env.CI_DEPENDENCY_PROXY_SERVER,
109 CI_DEPENDENCY_PROXY_USER: env.CI_DEPENDENCY_PROXY_USER,
110 CI_JOB_ID: env.CI_JOB_ID,
111 CI_JOB_NAME: env.CI_JOB_NAME,
112 CI_JOB_NAME_SLUG: env.CI_JOB_NAME_SLUG,
113 CI_JOB_STAGE: env.CI_JOB_STAGE,
114 CI_JOB_STARTED_AT: env.CI_JOB_STARTED_AT,
115 CI_JOB_URL: env.CI_JOB_URL,
116 CI_NODE_TOTAL: env.CI_NODE_TOTAL,
117 CI_PAGES_DOMAIN: env.CI_PAGES_DOMAIN,
118 CI_PAGES_URL: env.CI_PAGES_URL,
119 CI_PIPELINE_CREATED_AT: env.CI_PIPELINE_CREATED_AT,
120 CI_PIPELINE_ID: env.CI_PIPELINE_ID,
121 CI_PIPELINE_IID: env.CI_PIPELINE_IID,
122 CI_PIPELINE_SOURCE: env.CI_PIPELINE_SOURCE,
123 CI_PIPELINE_URL: env.CI_PIPELINE_URL,
124 CI_PROJECT_CLASSIFICATION_LABEL: env.CI_PROJECT_CLASSIFICATION_LABEL,
125 CI_PROJECT_DESCRIPTION: env.CI_PROJECT_DESCRIPTION,
126 CI_PROJECT_ID: env.CI_PROJECT_ID,
127 CI_PROJECT_NAME: env.CI_PROJECT_NAME,
128 CI_PROJECT_NAMESPACE: env.CI_PROJECT_NAMESPACE,
129 CI_PROJECT_NAMESPACE_ID: env.CI_PROJECT_NAMESPACE_ID,
130 CI_PROJECT_PATH: env.CI_PROJECT_PATH,
131 CI_PROJECT_PATH_SLUG: env.CI_PROJECT_PATH_SLUG,
132 CI_PROJECT_REPOSITORY_LANGUAGES: env.CI_PROJECT_REPOSITORY_LANGUAGES,
133 CI_PROJECT_ROOT_NAMESPACE: env.CI_PROJECT_ROOT_NAMESPACE,
134 CI_PROJECT_TITLE: env.CI_PROJECT_TITLE,
135 CI_PROJECT_URL: env.CI_PROJECT_URL,
136 CI_PROJECT_VISIBILITY: env.CI_PROJECT_VISIBILITY,
137 CI_REGISTRY: env.CI_REGISTRY,
138 CI_REGISTRY_IMAGE: env.CI_REGISTRY_IMAGE,
139 CI_REGISTRY_USER: env.CI_REGISTRY_USER,
140 CI_RUNNER_DESCRIPTION: env.CI_RUNNER_DESCRIPTION,
141 CI_RUNNER_ID: env.CI_RUNNER_ID,
142 CI_RUNNER_TAGS: env.CI_RUNNER_TAGS,
143 CI_SERVER_HOST: env.CI_SERVER_HOST,
144 CI_SERVER_NAME: env.CI_SERVER_NAME,
145 CI_SERVER_PORT: env.CI_SERVER_PORT,
146 CI_SERVER_PROTOCOL: env.CI_SERVER_PROTOCOL,
147 CI_SERVER_REVISION: env.CI_SERVER_REVISION,
148 CI_SERVER_SHELL_SSH_HOST: env.CI_SERVER_SHELL_SSH_HOST,
149 CI_SERVER_SHELL_SSH_PORT: env.CI_SERVER_SHELL_SSH_PORT,
150 CI_SERVER_URL: env.CI_SERVER_URL,
151 CI_SERVER_VERSION: env.CI_SERVER_VERSION,
152 CI_SERVER_VERSION_MAJOR: env.CI_SERVER_VERSION_MAJOR,
153 CI_SERVER_VERSION_MINOR: env.CI_SERVER_VERSION_MINOR,
154 CI_SERVER_VERSION_PATCH: env.CI_SERVER_VERSION_PATCH,
155 CI_TEMPLATE_REGISTRY_HOST: env.CI_TEMPLATE_REGISTRY_HOST,
156 GITLAB_CI: env.GITLAB_CI,
157 GITLAB_FEATURES: env.GITLAB_FEATURES,
158 GITLAB_USER_ID: env.GITLAB_USER_ID,
159 GITLAB_USER_LOGIN: env.GITLAB_USER_LOGIN,
160 RUNNER_GENERATE_ARTIFACTS_METADATA: env.RUNNER_GENERATE_ARTIFACTS_METADATA,
161 },
162 environment: {
163 name: env.CI_RUNNER_DESCRIPTION,
164 architecture: env.CI_RUNNER_EXECUTABLE_ARCH,
165 server: env.CI_SERVER_URL,
166 project: env.CI_PROJECT_PATH,
167 job: {
168 id: env.CI_JOB_ID,
169 },
170 pipeline: {
171 id: env.CI_PIPELINE_ID,
172 ref: env.CI_CONFIG_PATH,
173 },
174 },
175 },
176 metadata: {
177 buildInvocationId: `${env.CI_JOB_URL}`,
178 completeness: {
179 parameters: true,
180 environment: true,
181 materials: false,
182 },
183 reproducible: false,
184 },
185 materials: [
186 {
187 uri: `git+${env.CI_PROJECT_URL}`,
188 digest: {
189 sha1: env.CI_COMMIT_SHA,
190 },
191 },
192 ],
193 },
194 }
195 }
196 return sigstore.attest(Buffer.from(JSON.stringify(payload)), INTOTO_PAYLOAD_TYPE, opts)
197}
198
199const verifyProvenance = async (subject, provenancePath) => {
200 let provenanceBundle
201 try {
202 provenanceBundle = JSON.parse(await readFile(provenancePath))
203 } catch (err) {
204 err.message = `Invalid provenance provided: ${err.message}`
205 throw err
206 }
207
208 const payload = extractProvenance(provenanceBundle)
209 if (!payload.subject || !payload.subject.length) {
210 throw new Error('No subject found in sigstore bundle payload')
211 }
212 if (payload.subject.length > 1) {
213 throw new Error('Found more than one subject in the sigstore bundle payload')
214 }
215
216 const bundleSubject = payload.subject[0]
217 if (subject.name !== bundleSubject.name) {
218 throw new Error(
219 `Provenance subject ${bundleSubject.name} does not match the package: ${subject.name}`
220 )
221 }
222 if (subject.digest.sha512 !== bundleSubject.digest.sha512) {
223 throw new Error('Provenance subject digest does not match the package')
224 }
225
226 await sigstore.verify(provenanceBundle)
227 return provenanceBundle
228}
229
230const extractProvenance = (bundle) => {
231 if (!bundle?.dsseEnvelope?.payload) {
232 throw new Error('No dsseEnvelope with payload found in sigstore bundle')
233 }
234 try {
235 return JSON.parse(Buffer.from(bundle.dsseEnvelope.payload, 'base64').toString('utf8'))
236 } catch (err) {
237 err.message = `Failed to parse payload from dsseEnvelope: ${err.message}`
238 throw err
239 }
240}
241
242module.exports = {
243 generateProvenance,
244 verifyProvenance,
245}