Skip to content
Closed
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
1 change: 1 addition & 0 deletions lib/commands/publish.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class Publish extends BaseCommand {
'workspaces',
'include-workspace-root',
'provenance',
'provenance-bundle',
]

static usage = ['<package-spec>']
Expand Down
9 changes: 9 additions & 0 deletions lib/utils/config/definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -1629,6 +1629,15 @@ define('provenance', {
flatten,
})

define('provenance-bundle', {
default: null,
type: [null, String],
description: `
A sigstore bundle of signed SLSA provenance to be published with the package.
`,
flatten,
})

define('proxy', {
default: null,
type: [null, false, url], // allow proxy to be disabled explicitly
Expand Down
31 changes: 27 additions & 4 deletions workspaces/libnpmpublish/lib/publish.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ const npa = require('npm-package-arg')
const log = require('proc-log')
const semver = require('semver')
const { URL } = require('url')
const { sigstore } = require('sigstore')
const ssri = require('ssri')
const ciInfo = require('ci-info')
const fs = require(`fs`)

const { generateProvenance } = require('./provenance')

Expand Down Expand Up @@ -96,7 +98,7 @@ const patchManifest = (_manifest, opts) => {
}

const buildMetadata = async (registry, manifest, tarballData, spec, opts) => {
const { access, defaultTag, algorithms, provenance } = opts
const { access, defaultTag, algorithms, provenance, provenanceBundle } = opts
const root = {
_id: manifest.name,
name: manifest.name,
Expand Down Expand Up @@ -137,6 +139,13 @@ const buildMetadata = async (registry, manifest, tarballData, spec, opts) => {
length: tarballData.length,
}

if (provenance === true && provenanceBundle) {
throw Object.assign(
new Error('--provenance and --provenance-bundle cannot be specified at the same time.'),
{ code: 'EUSAGE' }
)
}
Comment on lines +142 to +147
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems confusing - if they're mutually exclusive, why not --provenance=path/to/bundle?

Copy link
Author

@ianlewis ianlewis Mar 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did try this. However, I wanted to preserve the existing --provenance boolean, and npm seems finicky about its flags. If I create a flag that can be a boolean or string then npm publish --provenance package-name will no longer work and parse package-name as the argument to --provenance.


// Handle case where --provenance flag was set to true
if (provenance === true) {
const subject = {
Expand Down Expand Up @@ -170,26 +179,40 @@ const buildMetadata = async (registry, manifest, tarballData, spec, opts) => {
{ code: 'EUSAGE' }
)
}
const provenanceBundle = await generateProvenance([subject], opts)
const bundle = await generateProvenance([subject], opts)

/* eslint-disable-next-line max-len */
log.notice('publish', 'Signed provenance statement with source and build information from GitHub Actions')

const tlogEntry = provenanceBundle?.verificationMaterial?.tlogEntries[0]
const tlogEntry = bundle?.verificationMaterial?.tlogEntries[0]
/* istanbul ignore else */
if (tlogEntry) {
const logUrl = `${TLOG_BASE_URL}?logIndex=${tlogEntry.logIndex}`
log.notice('publish', `Provenance statement published to transparency log: ${logUrl}`)
}

const serializedBundle = JSON.stringify(provenanceBundle)
const serializedBundle = JSON.stringify(bundle)
root._attachments[provenanceBundleName] = {
content_type: provenanceBundle.mediaType,
data: serializedBundle,
length: serializedBundle.length,
}
}

if (provenanceBundle) {
const serializedBundle = fs.readFileSync(provenanceBundle, 'utf-8')

// Parse and validate the bundle.
const bundle = JSON.parse(serializedBundle)
const verifiedBundle = sigstore.verify(bundle)

root._attachments[provenanceBundleName] = {
content_type: verifiedBundle.mediaType,
data: serializedBundle,
length: serializedBundle.length,
}
}

return root
}

Expand Down