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