Skip to content

feat: oidc provenance by default #8412

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: oidc
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions lib/utils/oidc.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,44 @@ async function oidc ({ packageName, registry, opts, config }) {
return undefined
}

// this checks if the user configured provenance or it's the default unset value
const isDefaultProvenance = config.isDefault('provenance')
const provenanceIntent = config.get('provenance')
const skipProvenance = isDefaultProvenance || provenanceIntent

if (skipProvenance) {
const [headerB64, payloadB64] = idToken.split('.')
let isPublicRepo = false
if (headerB64 && payloadB64) {
const payloadJson = Buffer.from(payloadB64, 'base64').toString('utf8')
try {
const payload = JSON.parse(payloadJson)
if (ciInfo.GITHUB_ACTIONS && payload.repository_visibility === 'public') {
isPublicRepo = true
}
if (ciInfo.GITLAB && payload.project_visibility === 'public') {
isPublicRepo = true
}
} catch (e) {
log.silly('oidc', 'Failed to parse idToken payload as JSON')
}
}

if (isPublicRepo) {
log.silly('oidc', 'Repository is public, setting provenance')
opts.provenance = true
config.set('provenance', true, 'user')
}
}

log.silly('oidc', `id_token has a length of ${idToken.length} characters`)

const parsedRegistry = new URL(registry)
const regKey = `//${parsedRegistry.host}${parsedRegistry.pathname}`
const authTokenKey = `${regKey}:_authToken`

const exitingToken = config.get(authTokenKey)
if (exitingToken) {
const existingToken = config.get(authTokenKey)
if (existingToken) {
log.silly('oidc', 'Existing token found')
} else {
log.silly('oidc', 'No existing token found')
Expand Down
6 changes: 5 additions & 1 deletion mock-registry/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,11 @@ class MockRegistry {
// XXX: this is opt-in currently because it breaks some existing CLI
// tests. We should work towards making this the default for all tests.
t.comment(logReq(req, 'interceptors', 'socket', 'response', '_events'))
t.fail(`Unmatched request: ${req.method} ${req.path}`)
const protocol = req?.options?.protocol || 'http:'
const hostname = req?.options?.hostname || req?.hostname || 'localhost'
const p = req?.path || '/'
const url = new URL(p, `${protocol}//${hostname}`).toString()
t.fail(`Unmatched request: ${req.method} ${url}`)
}
}

Expand Down
99 changes: 99 additions & 0 deletions mock-registry/lib/provenance.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
'use strict'
const t = require('tap')
const mockGlobals = require('@npmcli/mock-globals')
const nock = require('nock')

class MockProvenance {
static sigstoreIdToken () {
return `.${Buffer.from(JSON.stringify({
iss: 'https://oauth2.sigstore.dev/auth',
email: 'foo@bar.com',
})).toString('base64')}.`
}

static successfulNock ({
oidcURL,
requestToken,
workflowPath,
repository,
serverUrl,
ref,
sha,
runID,
runAttempt,
runnerEnv,
}) {
mockGlobals(t, {
'process.env': {
CI: true,
GITHUB_ACTIONS: true,
ACTIONS_ID_TOKEN_REQUEST_URL: oidcURL,
ACTIONS_ID_TOKEN_REQUEST_TOKEN: requestToken,
GITHUB_WORKFLOW_REF: `${repository}/${workflowPath}@${ref}`,
GITHUB_REPOSITORY: repository,
GITHUB_SERVER_URL: serverUrl,
GITHUB_REF: ref,
GITHUB_SHA: sha,
GITHUB_RUN_ID: runID,
GITHUB_RUN_ATTEMPT: runAttempt,
RUNNER_ENVIRONMENT: runnerEnv,
},
})

const idToken = this.sigstoreIdToken()

const url = new URL(oidcURL)
nock(url.origin)
.get(url.pathname)
.query({ audience: 'sigstore' })
.matchHeader('authorization', `Bearer ${requestToken}`)
.matchHeader('accept', 'application/json')
.reply(200, { value: idToken })

const leafCertificate = `-----BEGIN CERTIFICATE-----\nabc\n-----END CERTIFICATE-----\n`

// Mock the Fulcio signing certificate endpoint
nock('https://fulcio.sigstore.dev')
.post('/api/v2/signingCert')
.reply(200, {
signedCertificateEmbeddedSct: {
chain: {
certificates: [
leafCertificate,
`-----BEGIN CERTIFICATE-----\nxyz\n-----END CERTIFICATE-----\n`,
],
},
},
})

nock('https://rekor.sigstore.dev')
.post('/api/v1/log/entries')
.reply(201, {
'69e5a0c1663ee4452674a5c9d5050d866c2ee31e2faaf79913aea7cc27293cf6': {
body: Buffer.from(JSON.stringify({
kind: 'hashedrekord',
apiVersion: '0.0.1',
spec: {
signature: {
content: 'ABC123',
publicKey: { content: Buffer.from(leafCertificate).toString('base64') },
},
},
})).toString(
'base64'
),
integratedTime: 1654015743,
logID:
'c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d',
logIndex: 2513258,
verification: {
signedEntryTimestamp: 'MEUCIQD6CD7ZNLUipFoxzmSL/L8Ewic4SRkXN77UjfJZ7d/wAAIgatokSuX9Rg0iWxAgSfHMtcsagtDCQalU5IvXdQ+yLEA=',
},
},
})
}
}

module.exports = {
MockProvenance,
}
36 changes: 36 additions & 0 deletions test/fixtures/mock-oidc.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,35 @@
const nock = require('nock')
const ciInfo = require('ci-info')

// this is an effort to not add a dependency to the cli just for testing
function makeJwt (payload) {
const header = { alg: 'none', typ: 'JWT' }
const headerB64 = Buffer.from(JSON.stringify(header)).toString('base64')
const payloadB64 = Buffer.from(JSON.stringify(payload)).toString('base64')
// empty signature section
return `${headerB64}.${payloadB64}.`
}

function gitlabIdToken ({ visibility = 'public' } = { visibility: 'public' }) {
const now = Math.floor(Date.now() / 1000)
const payload = {
project_visibility: visibility,
iat: now,
exp: now + 3600, // 1 hour expiration
}
return makeJwt(payload)
}

function githubIdToken ({ visibility = 'public' } = { visibility: 'public' }) {
const now = Math.floor(Date.now() / 1000)
const payload = {
repository_visibility: visibility,
iat: now,
exp: now + 3600, // 1 hour expiration
}
return makeJwt(payload)
}

class MockOidc {
constructor (opts) {
const defaultOpts = {
Expand All @@ -17,6 +46,8 @@ class MockOidc {
this.gitlab = options.gitlab
this.ACTIONS_ID_TOKEN_REQUEST_URL = options.ACTIONS_ID_TOKEN_REQUEST_URL
this.ACTIONS_ID_TOKEN_REQUEST_TOKEN = options.ACTIONS_ID_TOKEN_REQUEST_TOKEN
this.SIGSTORE_ID_TOKEN = options.SIGSTORE_ID_TOKEN

this.NPM_ID_TOKEN = options.NPM_ID_TOKEN
this.GITHUB_ID_TOKEN = options.GITHUB_ID_TOKEN

Expand All @@ -28,6 +59,7 @@ class MockOidc {
ACTIONS_ID_TOKEN_REQUEST_TOKEN: process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN,
GITLAB_CI: process.env.GITLAB_CI,
NPM_ID_TOKEN: process.env.NPM_ID_TOKEN,
SIGSTORE_ID_TOKEN: process.env.SIGSTORE_ID_TOKEN,
}
this.originalCiInfo = {
GITLAB: ciInfo.GITLAB,
Expand All @@ -53,6 +85,7 @@ class MockOidc {
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN
delete process.env.GITLAB_CI
delete process.env.NPM_ID_TOKEN
delete process.env.SIGSTORE_ID_TOKEN

ciInfo.GITHUB_ACTIONS = false
ciInfo.GITLAB = false
Expand All @@ -65,6 +98,7 @@ class MockOidc {

if (this.gitlab) {
process.env.NPM_ID_TOKEN = this.NPM_ID_TOKEN
process.env.SIGSTORE_ID_TOKEN = this.SIGSTORE_ID_TOKEN
ciInfo.GITLAB = true
}
}
Expand Down Expand Up @@ -128,4 +162,6 @@ class MockOidc {

module.exports = {
MockOidc,
gitlabIdToken,
githubIdToken,
}
Loading
Loading