• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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}
246