1 | const sigstore = require('sigstore')
|
2 | const { readFile } = require('fs/promises')
|
3 | const ci = require('ci-info')
|
4 | const { env } = process
|
5 |
|
6 | const INTOTO_PAYLOAD_TYPE = 'application/vnd.in-toto+json'
|
7 | const INTOTO_STATEMENT_V01_TYPE = 'https://in-toto.io/Statement/v0.1'
|
8 | const INTOTO_STATEMENT_V1_TYPE = 'https://in-toto.io/Statement/v1'
|
9 | const SLSA_PREDICATE_V02_TYPE = 'https://slsa.dev/provenance/v0.2'
|
10 | const SLSA_PREDICATE_V1_TYPE = 'https://slsa.dev/provenance/v1'
|
11 |
|
12 | const GITHUB_BUILDER_ID_PREFIX = 'https://github.com/actions/runner'
|
13 | const GITHUB_BUILD_TYPE = 'https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1'
|
14 |
|
15 | const GITLAB_BUILD_TYPE_PREFIX = 'https://github.com/npm/cli/gitlab'
|
16 | const GITLAB_BUILD_TYPE_VERSION = 'v0alpha1'
|
17 |
|
18 | const generateProvenance = async (subject, opts) => {
|
19 | let payload
|
20 | if (ci.GITHUB_ACTIONS) {
|
21 |
|
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 |
|
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 |
|
199 | const 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 |
|
230 | const 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 |
|
242 | module.exports = {
|
243 | generateProvenance,
|
244 | verifyProvenance,
|
245 | }
|