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