Skip to content

Commit

Permalink
feat: npm default runner support (#495)
Browse files Browse the repository at this point in the history
* update

Signed-off-by: laurentsimon <laurentsimon@google.com>

* update

Signed-off-by: laurentsimon <laurentsimon@google.com>

* update

Signed-off-by: laurentsimon <laurentsimon@google.com>

* update

Signed-off-by: laurentsimon <laurentsimon@google.com>

* update

Signed-off-by: laurentsimon <laurentsimon@google.com>

* update

Signed-off-by: laurentsimon <laurentsimon@google.com>

* update

Signed-off-by: laurentsimon <laurentsimon@google.com>

* update

Signed-off-by: laurentsimon <laurentsimon@google.com>

* update

Signed-off-by: laurentsimon <laurentsimon@google.com>

* update

Signed-off-by: laurentsimon <laurentsimon@google.com>

* update

Signed-off-by: laurentsimon <laurentsimon@google.com>

* update

Signed-off-by: laurentsimon <laurentsimon@google.com>

* update

Signed-off-by: laurentsimon <laurentsimon@google.com>

* update

Signed-off-by: laurentsimon <laurentsimon@google.com>

* update

Signed-off-by: laurentsimon <laurentsimon@google.com>

* update

Signed-off-by: laurentsimon <laurentsimon@google.com>

* update

Signed-off-by: laurentsimon <laurentsimon@google.com>

* update

Signed-off-by: laurentsimon <laurentsimon@google.com>

* update

Signed-off-by: laurentsimon <laurentsimon@google.com>

* update

Signed-off-by: laurentsimon <laurentsimon@google.com>

---------

Signed-off-by: laurentsimon <laurentsimon@google.com>
  • Loading branch information
laurentsimon committed Mar 2, 2023
1 parent 12910ea commit 82a1259
Show file tree
Hide file tree
Showing 36 changed files with 2,938 additions and 237 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/pre-submit.e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,5 @@ jobs:

- name: Run verification script with testdata and slsa-verifier HEAD
run: ./__THIS_REPO__/.github/workflows/scripts/e2e-cli.sh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Necessary to use the gh CLI.
31 changes: 27 additions & 4 deletions .github/workflows/scripts/e2e-cli.sh
Original file line number Diff line number Diff line change
@@ -1,19 +1,42 @@
#!/bin/bash

repo="slsa-framework/example-package"
api_version="X-GitHub-Api-Version: 2022-11-28"
# Verify provenance authenticity with slsa-verifier at HEAD

download_artifact(){
local run_id="$1"
local artifact_name="$2"
# Get the artifact ID for 'artifact1'
artifact_id=$(gh api -H "Accept: application/vnd.github+json" -H "$api_version" "/repos/$repo/actions/runs/$run_id/artifacts" | jq ".artifacts[] | select(.name == \"$artifact_name\") | .id")
echo "artifact_id:$artifact_id"

gh api -H "Accept: application/vnd.github+json" -H "$api_version" "/repos/$repo/actions/artifacts/$artifact_id/zip" > "$artifact_name.zip"
unzip "$artifact_name".zip
}

# Get workflow ID.
workflow_id=$(gh api -H "Accept: application/vnd.github+json" -H "$api_version" "/repos/$repo/actions/workflows?per_page=100" | jq '.workflows[] | select(.path == ".github/workflows/e2e.generic.schedule.main.multi-uses.slsa3.yml") | .id')
echo "workflow_id:$workflow_id"

# Get the run ID for the most recent run.
run_id=$(gh api -H "Accept: application/vnd.github+json" -H "$api_version" "/repos/$repo/actions/workflows/$workflow_id/runs?per_page=1" | jq '.workflow_runs[0].id')
echo "run_id:$run_id"

download_artifact "$run_id" "artifacts1"
download_artifact "$run_id" "attestation1.intoto.jsonl"

cd __EXAMPLE_PACKAGE__
# shellcheck source=/dev/null
source "./.github/workflows/scripts/e2e-verify.common.sh"

# Set THIS_FILE to correspond with the artifact properties
export THIS_FILE=e2e.go.workflow_dispatch.main.config-noldflags.slsa3.yml
export THIS_FILE=e2e.generic.schedule.main.multi-uses.slsa3.yml
export BRANCH=main

# Set BINARY and PROVENANCE
cd -
export BINARY=__THIS_REPO__/cli/slsa-verifier/testdata/gha_go/v1.2.2/binary-linux-amd64-workflow_dispatch
export PROVENANCE=__THIS_REPO__/cli/slsa-verifier/testdata/gha_go/v1.2.2/binary-linux-amd64-workflow_dispatch.intoto.jsonl
export BINARY=artifact1
export PROVENANCE=attestation1.intoto.jsonl

GITHUB_REPOSITORY=slsa-framework/example-package verify_provenance_authenticity "./__THIS_REPO__/slsa-verifier" "HEAD"
GITHUB_REPOSITORY="$repo" verify_provenance_authenticity "./__THIS_REPO__/slsa-verifier" "HEAD"
1 change: 1 addition & 0 deletions cli/slsa-verifier/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ For more information on SLSA, visit https://slsa.dev`,
c.AddCommand(version.Version())
c.AddCommand(verifyArtifactCmd())
c.AddCommand(verifyImageCmd())
c.AddCommand(verifyNpmPackageCmd())
// We print our own errors and usage in the check function.
c.SilenceErrors = true
return c
Expand Down
60 changes: 60 additions & 0 deletions cli/slsa-verifier/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,63 @@ func verifyImageCmd() *cobra.Command {
o.AddFlags(cmd)
return cmd
}

func verifyNpmPackageCmd() *cobra.Command {
o := &verify.VerifyNpmOptions{}

cmd := &cobra.Command{
Use: "verify-npm-package [flags] tarball",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return errors.New("expects a single path to an image")
}
return nil
},
Short: "Verifies SLSA provenance for an npm package tarball [experimental]",
Run: func(cmd *cobra.Command, args []string) {
v := verify.VerifyNpmPackageCommand{
SourceURI: o.SourceURI,
PrintProvenance: o.PrintProvenance,
BuildWorkflowInputs: o.BuildWorkflowInputs.AsMap(),
}
if cmd.Flags().Changed("attestations-path") {
v.AttestationsPath = o.AttestationsPath
}
if cmd.Flags().Changed("package-name") {
v.PackageName = &o.PackageName
}
if cmd.Flags().Changed("package-version") {
v.PackageVersion = &o.PackageVersion
}
if cmd.Flags().Changed("source-branch") {
fmt.Fprintf(os.Stderr, "%s: --source-branch not supported\n", FAILURE)
os.Exit(1)
}
if cmd.Flags().Changed("source-tag") {
fmt.Fprintf(os.Stderr, "%s: --source-tag not supported\n", FAILURE)
os.Exit(1)
}
if cmd.Flags().Changed("source-versioned-tag") {
fmt.Fprintf(os.Stderr, "%s: --source-versioned-tag not supported\n", FAILURE)
os.Exit(1)
}
if cmd.Flags().Changed("print-provenance") {
fmt.Fprintf(os.Stderr, "%s: --print-provenance not supported\n", FAILURE)
os.Exit(1)
}
if cmd.Flags().Changed("builder-id") {
v.BuilderID = &o.BuilderID
}

if _, err := v.Exec(cmd.Context(), args); err != nil {
fmt.Fprintf(os.Stderr, "%s: %v\n", FAILURE, err)
os.Exit(1)
} else {
fmt.Fprintf(os.Stderr, "%s\n", SUCCESS)
}
},
}

o.AddFlags(cmd)
return cmd
}
46 changes: 46 additions & 0 deletions cli/slsa-verifier/verify/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,52 @@ func (o *VerifyOptions) AddFlags(cmd *cobra.Command) {
cmd.MarkFlagsMutuallyExclusive("source-versioned-tag", "source-tag")
}

// VerifyNpmOptions is the top-level options for the `verifyNpmPackage` command.
type VerifyNpmOptions struct {
VerifyOptions
/* Other */
AttestationsPath string
PackageName string
PackageVersion string
}

var _ Interface = (*VerifyNpmOptions)(nil)

// AddFlags implements Interface.
func (o *VerifyNpmOptions) AddFlags(cmd *cobra.Command) {
/* Builder options */
cmd.Flags().Var(&o.BuildWorkflowInputs, "build-workflow-input",
"[optional] a workflow input provided by a user at trigger time in the format 'key=value'. (Only for 'workflow_dispatch' events on GitHub Actions).")

cmd.Flags().StringVar(&o.BuilderID, "builder-id", "", "[optional] the unique builder ID who created the provenance")

/* Source options */
cmd.Flags().StringVar(&o.SourceURI, "source-uri", "",
"expected source repository that should have produced the binary, e.g. github.com/some/repo")

cmd.Flags().StringVar(&o.SourceBranch, "source-branch", "", "[optional] expected branch the binary was compiled from")

cmd.Flags().StringVar(&o.SourceTag, "source-tag", "", "[optional] expected tag the binary was compiled from")

cmd.Flags().StringVar(&o.SourceVersionTag, "source-versioned-tag", "",
"[optional] expected version the binary was compiled from. Uses semantic version to match the tag")

cmd.Flags().StringVar(&o.AttestationsPath, "attestations-path", "",
"path to a file containing the attestations")

cmd.Flags().StringVar(&o.PackageName, "package-name", "",
"[optional] the package name")

cmd.Flags().StringVar(&o.PackageVersion, "package-version", "",
"[optional] the package version")

cmd.Flags().BoolVar(&o.PrintProvenance, "print-provenance", false,
"[optional] print the verified provenance to stdout")

cmd.MarkFlagRequired("source-uri")
cmd.MarkFlagsMutuallyExclusive("source-versioned-tag", "source-tag")
}

type workflowInputs struct {
kv map[string]string
}
Expand Down
35 changes: 35 additions & 0 deletions cli/slsa-verifier/verify/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright 2023 SLSA Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package verify

import (
"encoding/hex"
"hash"
"io"
"os"
)

func computeFileHash(filePath string, h hash.Hash) (string, error) {
f, err := os.Open(filePath)
if err != nil {
return "", err
}
defer f.Close()

if _, err := io.Copy(h, f); err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}
17 changes: 1 addition & 16 deletions cli/slsa-verifier/verify/verify_artifact.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ package verify
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"os"

"github.com/slsa-framework/slsa-verifier/v2/options"
Expand All @@ -43,7 +41,7 @@ func (c *VerifyArtifactCommand) Exec(ctx context.Context, artifacts []string) (*
var builderID *utils.TrustedBuilderID

for _, artifact := range artifacts {
artifactHash, err := getArtifactHash(artifact)
artifactHash, err := computeFileHash(artifact, sha256.New())
if err != nil {
fmt.Fprintf(os.Stderr, "Verifying artifact %s: FAILED: %v\n\n", artifact, err)
return nil, err
Expand Down Expand Up @@ -90,16 +88,3 @@ func (c *VerifyArtifactCommand) Exec(ctx context.Context, artifacts []string) (*

return builderID, nil
}

func getArtifactHash(artifactPath string) (string, error) {
f, err := os.Open(artifactPath)
if err != nil {
return "", err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}
96 changes: 96 additions & 0 deletions cli/slsa-verifier/verify/verify_npm_package.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright 2022 SLSA Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package verify

import (
"context"
"crypto/sha512"
"errors"
"fmt"
"os"

"github.com/slsa-framework/slsa-verifier/v2/options"
"github.com/slsa-framework/slsa-verifier/v2/verifiers"
"github.com/slsa-framework/slsa-verifier/v2/verifiers/utils"
)

type VerifyNpmPackageCommand struct {
AttestationsPath string
BuilderID *string
SourceURI string
SourceBranch *string
SourceTag *string
SourceVersionTag *string
PackageName *string
PackageVersion *string
BuildWorkflowInputs map[string]string
PrintProvenance bool
}

func (c *VerifyNpmPackageCommand) Exec(ctx context.Context, tarballs []string) (*utils.TrustedBuilderID, error) {
var builderID *utils.TrustedBuilderID
if !options.ExperimentalEnabled() {
err := errors.New("feature support is only provided in SLSA_VERIFIER_EXPERIMENTAL mode")
fmt.Fprintf(os.Stderr, "Verifying npm package: FAILED: %v\n\n", err)
return nil, err
}
for _, tarball := range tarballs {
tarballHash, err := computeFileHash(tarball, sha512.New())
if err != nil {
fmt.Fprintf(os.Stderr, "Verifying npm package %s: FAILED: %v\n\n", tarball, err)
return nil, err
}

if c.AttestationsPath == "" {
fmt.Fprintf(os.Stderr, "Verifying npm package %s: FAILED: %v\n\n", tarball, err)
return nil, err
}
provenanceOpts := &options.ProvenanceOpts{
ExpectedSourceURI: c.SourceURI,
ExpectedBranch: c.SourceBranch,
ExpectedDigest: tarballHash,
ExpectedVersionedTag: c.SourceVersionTag,
ExpectedTag: c.SourceTag,
ExpectedWorkflowInputs: c.BuildWorkflowInputs,
ExpectedPackageName: c.PackageName,
ExpectedPackageVersion: c.PackageVersion,
}

builderOpts := &options.BuilderOpts{
ExpectedID: c.BuilderID,
}

attestations, err := os.ReadFile(c.AttestationsPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Verifying npm package %s: FAILED: %v\n\n", tarball, err)
return nil, err
}

verifiedProvenance, outBuilderID, err := verifiers.VerifyNpmPackage(ctx, attestations, tarballHash, provenanceOpts, builderOpts)
if err != nil {
fmt.Fprintf(os.Stderr, "Verifying npm package %s: FAILED: %v\n\n", tarball, err)
return nil, err
}

if c.PrintProvenance {
fmt.Fprintf(os.Stdout, "%s\n", string(verifiedProvenance))
}

builderID = outBuilderID
fmt.Fprintf(os.Stderr, "Verifying npm package %s: PASSED\n\n", tarball)
}

return builderID, nil
}
4 changes: 4 additions & 0 deletions errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import "errors"
var (
ErrorInvalidDssePayload = errors.New("invalid DSSE envelope payload")
ErrorMismatchBranch = errors.New("branch used to generate the binary does not match provenance")
ErrorMismatchPackageVersion = errors.New("package version does not match provenance")
ErrorMismatchPackageName = errors.New("package name does not match provenance")
ErrorMismatchBuilderID = errors.New("builderID does not match provenance")
ErrorInvalidBuilderID = errors.New("builderID is invalid")
ErrorMismatchSource = errors.New("source used to generate the binary does not match provenance")
Expand Down Expand Up @@ -33,4 +35,6 @@ var (
ErrorInternal = errors.New("internal error")
ErrorInvalidRekorEntry = errors.New("invalid Rekor entry")
ErrorRekorPubKey = errors.New("error retrieving Rekor public keys")
ErrorInvalidPackageName = errors.New("invalid package name")
ErrorInvalidSubject = errors.New("invalid subject")
)
4 changes: 4 additions & 0 deletions options/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ type ProvenanceOpts struct {

// ExpectedWorkflowInputs is a map of key=value inputs.
ExpectedWorkflowInputs map[string]string

ExpectedPackageName *string

ExpectedPackageVersion *string
}

// BuildOpts are the options for checking the builder.
Expand Down
6 changes: 6 additions & 0 deletions register/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ type SLSAVerifier interface {
provenanceOpts *options.ProvenanceOpts,
builderOpts *options.BuilderOpts,
) ([]byte, *utils.TrustedBuilderID, error)

VerifyNpmPackage(ctx context.Context,
attestations []byte, tarballHash string,
provenanceOpts *options.ProvenanceOpts,
builderOpts *options.BuilderOpts,
) ([]byte, *utils.TrustedBuilderID, error)
}

func RegisterVerifier(name string, verifier SLSAVerifier) {
Expand Down
Loading

0 comments on commit 82a1259

Please sign in to comment.