Skip to content
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

835 - Keyless Support for SBOM Attestations #910

Merged
merged 44 commits into from
May 6, 2022
Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
06be3a6
wip
spiffcs Mar 15, 2022
bfb3827
clean up and consolodate validation functions
spiffcs Mar 15, 2022
ce069ae
update tests with new path and new launcher
spiffcs Mar 15, 2022
26a8ffe
update docker-compose for e2e testing
spiffcs Mar 15, 2022
f101e9a
wip
spiffcs Mar 17, 2022
ab4998c
wip
spiffcs Mar 17, 2022
e49bdbc
wip
spiffcs Mar 18, 2022
8afc1fb
master election working for trillian
spiffcs Mar 20, 2022
a9a5920
ct_server up and running
spiffcs Mar 20, 2022
56410ec
update hard values for local testing
spiffcs Mar 20, 2022
8e18b90
dex issues
spiffcs Mar 20, 2022
4443d4f
x509 CT server failure from fulcio
spiffcs Mar 21, 2022
3259c71
local flow works
spiffcs Mar 21, 2022
f3c7ab5
add skeleton flags for keyless workflow wip
spiffcs Mar 21, 2022
659e1da
update options to sane defaults
spiffcs Mar 22, 2022
cb25858
Merge branch 'main' into 835-keyless-attestation-upgrade
spiffcs May 2, 2022
37ca6b2
update keyless options to new cmd format
spiffcs May 2, 2022
ebc96d3
update ko with new injected values
spiffcs May 2, 2022
b0b3907
update log to use syft logging package
spiffcs May 2, 2022
dbf4f87
static touchups
spiffcs May 3, 2022
ebded6f
update injected options
spiffcs May 3, 2022
9acebc1
wip
spiffcs May 3, 2022
e599636
update to upload attestation
spiffcs May 5, 2022
6fa29e7
update options to include annotations
spiffcs May 6, 2022
211e460
remove old cli test
spiffcs May 6, 2022
b440344
remove unused var
spiffcs May 6, 2022
52a9aff
pass by value for ko
spiffcs May 6, 2022
f595e3a
update nil check for tlog entry; privatize defaults
spiffcs May 6, 2022
48234e1
remove dead code
spiffcs May 6, 2022
e58f5f5
remove default docs and prescribed default since optional flag
spiffcs May 6, 2022
a1732be
remove replace flag add initial comments
spiffcs May 6, 2022
47c8ff0
update flag description
spiffcs May 6, 2022
5cc1704
s/_/-/g <- for all flag options
spiffcs May 6, 2022
c624f6c
refacor cli test
spiffcs May 6, 2022
667b977
remove replace from binding
spiffcs May 6, 2022
5585a82
added ETUI entries for keyless path
spiffcs May 6, 2022
d1a5d68
don't upload keyed workflow
spiffcs May 6, 2022
659685d
update linter
spiffcs May 6, 2022
f68493a
add specific flag now no longer default
spiffcs May 6, 2022
02196db
update comment with inputs and outputs
spiffcs May 6, 2022
e43e426
Merge branch 'main' into 835-keyless-attestation-upgrade
spiffcs May 6, 2022
e5bb6df
remove output on upload
spiffcs May 6, 2022
a24372e
update attestation upload etui
wagoodman May 6, 2022
5d58aa3
commit new changes for static analysis
spiffcs May 6, 2022
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
15 changes: 14 additions & 1 deletion cmd/syft/cli/attest.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/config"
"github.com/sigstore/cosign/cmd/cosign/cli/sign"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
Expand Down Expand Up @@ -53,7 +54,19 @@ func Attest(v *viper.Viper, app *config.Application, ro *options.RootOptions) *c
checkForApplicationUpdate()
}

return attest.Run(cmd.Context(), app, args)
// build cosign key options for attestation
ko := sign.KeyOpts{
KeyRef: app.Attest.KeyRef,
FulcioURL: app.Attest.FulcioURL,
IDToken: app.Attest.FulcioIdentityToken,
InsecureSkipFulcioVerify: app.Attest.InsecureSkipFulcioVerify,
RekorURL: app.Attest.RekorURL,
OIDCIssuer: app.Attest.OIDCIssuer,
OIDCClientID: app.Attest.OIDCClientID,
OIDCRedirectURL: app.Attest.OIDCRedirectURL,
}

return attest.Run(cmd.Context(), app, ko, args)
},
}

Expand Down
186 changes: 163 additions & 23 deletions cmd/syft/cli/attest/attest.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/wagoodman/go-progress"
"os"
"strings"

"github.com/anchore/stereoscope"
"github.com/anchore/stereoscope/pkg/image"
Expand All @@ -18,15 +18,28 @@ import (
"github.com/anchore/syft/internal/formats/cyclonedxjson"
"github.com/anchore/syft/internal/formats/spdx22json"
"github.com/anchore/syft/internal/formats/syftjson"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/ui"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/in-toto/in-toto-golang/in_toto"
"github.com/pkg/errors"
"github.com/sigstore/cosign/cmd/cosign/cli/rekor"
"github.com/sigstore/cosign/cmd/cosign/cli/sign"
"github.com/sigstore/cosign/pkg/cosign"
"github.com/sigstore/cosign/pkg/cosign/attestation"
cbundle "github.com/sigstore/cosign/pkg/cosign/bundle"
"github.com/sigstore/cosign/pkg/oci/mutate"
ociremote "github.com/sigstore/cosign/pkg/oci/remote"
"github.com/sigstore/cosign/pkg/oci/static"
sigs "github.com/sigstore/cosign/pkg/signature"
"github.com/sigstore/cosign/pkg/types"
"github.com/sigstore/rekor/pkg/generated/client"
"github.com/sigstore/rekor/pkg/generated/models"
"github.com/sigstore/sigstore/pkg/signature/dsse"
"github.com/wagoodman/go-partybus"

Expand All @@ -43,7 +56,7 @@ var (
intotoJSONDsseType = `application/vnd.in-toto+json`
)

func Run(ctx context.Context, app *config.Application, args []string) error {
func Run(ctx context.Context, app *config.Application, ko sign.KeyOpts, args []string) error {
// We cannot generate an attestation for more than one output
if len(app.Outputs) > 1 {
return fmt.Errorf("unable to generate attestation for more than one output")
Expand All @@ -59,17 +72,20 @@ func Run(ctx context.Context, app *config.Application, args []string) error {
format := syft.FormatByName(app.Outputs[0])
predicateType := formatPredicateType(format)
if predicateType == "" {
return fmt.Errorf("could not produce attestation predicate for given format: %q. Available formats: %+v", options.FormatAliases(format.ID()), options.FormatAliases(allowedAttestFormats...))
return fmt.Errorf(
"could not produce attestation predicate for given format: %q. Available formats: %+v",
options.FormatAliases(format.ID()),
options.FormatAliases(allowedAttestFormats...),
)
}

passFunc, err := selectPassFunc(app.Attest.Key, app.Attest.Password)
if err != nil {
return err
}
if app.Attest.KeyRef != "" {
passFunc, err := selectPassFunc(app.Attest.KeyRef, app.Attest.Password)
if err != nil {
return err
}

ko := sign.KeyOpts{
KeyRef: app.Attest.Key,
PassFunc: passFunc,
ko.PassFunc = passFunc
}

sv, err := sign.SignerFromKeyOpts(ctx, "", "", ko)
Expand Down Expand Up @@ -144,7 +160,7 @@ func execWorker(app *config.Application, sourceInput source.Input, format sbom.F
return
}

err = generateAttestation(sbomBytes, src, sv, predicateType)
err = generateAttestation(app, sbomBytes, src, sv, predicateType)
if err != nil {
errs <- err
return
Expand All @@ -153,7 +169,7 @@ func execWorker(app *config.Application, sourceInput source.Input, format sbom.F
return errs
}

func generateAttestation(predicate []byte, src *source.Source, sv *sign.SignerVerifier, predicateType string) error {
func generateAttestation(app *config.Application, predicate []byte, src *source.Source, sv *sign.SignerVerifier, predicateType string) error {
switch len(src.Image.Metadata.RepoDigests) {
case 0:
return fmt.Errorf("cannot generate attestation since no repo digests were found; make sure you're passing an OCI registry source for the attest command")
Expand All @@ -163,11 +179,22 @@ func generateAttestation(predicate []byte, src *source.Source, sv *sign.SignerVe
}

wrapped := dsse.WrapSigner(sv, intotoJSONDsseType)
ref, err := name.ParseReference(src.Metadata.ImageMetadata.UserInput)
if err != nil {
return err
}

digest, err := ociremote.ResolveDigest(ref)
if err != nil {
return err
}

h, _ := v1.NewHash(digest.Identifier())

sh, err := attestation.GenerateStatement(attestation.GenerateOpts{
Predicate: bytes.NewBuffer(predicate),
Type: predicateType,
Digest: findValidDigest(src.Image.Metadata.RepoDigests),
Digest: h.Hex,
})
if err != nil {
return err
Expand All @@ -183,24 +210,107 @@ func generateAttestation(predicate []byte, src *source.Source, sv *sign.SignerVe
return errors.Wrap(err, "unable to sign SBOM")
}

// We want to give the option to not upload the generated attestation
// if passed or if the user is using local PKI
if app.Attest.NoUpload || app.Attest.KeyRef != "" {
bus.Publish(partybus.Event{
Type: event.Exit,
Value: func() error {
_, err := os.Stdout.Write(signedPayload)
return err
},
})
return nil
}

return uploadAttestation(app, signedPayload, digest, sv)
}

func trackUploadAttestation() (*progress.Stage, *progress.Manual) {
stage := &progress.Stage{}
prog := &progress.Manual{}

bus.Publish(partybus.Event{
Type: event.UploadAttestation,
Value: progress.StagedProgressable(&struct {
progress.Stager
progress.Progressable
}{
Stager: stage,
Progressable: prog,
}),
})

return stage, prog
}

// uploads signed SBOM payload to Rekor transparency log along with key information;
// returns a bundle for attestation annotations
// rekor bundle includes a signed payload and rekor timestamp;
// the bundle is then wrapped onto an OCI signed entity and uploaded to
// the user's image's OCI registry repository as *.att
func uploadAttestation(app *config.Application, signedPayload []byte, digest name.Digest, sv *sign.SignerVerifier) error {
// add application/vnd.dsse.envelope.v1+json as media type for other applications to decode attestation
opts := []static.Option{static.WithLayerMediaType(types.DssePayloadType)}
if sv.Cert != nil {
opts = append(opts, static.WithCertChain(sv.Cert, sv.Chain))
}

stage, prog := trackUploadAttestation()
defer prog.SetCompleted() // just in case we return early

prog.Total = 2
stage.Current = "uploading signing information to transparency log"

// uploads payload to Rekor transparency log along with key information;
// returns bundle for attesation annotations
// rekor bundle includes a signed payload and rekor timestamp;
// the bundle is then wrapped onto an OCI signed entity and uploaded to
// the user's image's OCI registry repository as *.att
bundle, err := uploadToTlog(context.TODO(), sv, app.Attest.RekorURL, func(r *client.Rekor, b []byte) (*models.LogEntryAnon, error) {
return cosign.TLogUploadInTotoAttestation(context.TODO(), r, signedPayload, b)
})
if err != nil {
return err
}

prog.N = 1
stage.Current = "uploading attestation to OCI registry"

// add bundle OCI attestation that is uploaded to
opts = append(opts, static.WithBundle(bundle))
sig, err := static.NewAttestation(signedPayload, opts...)
if err != nil {
return err
}

se, err := ociremote.SignedEntity(digest)
if err != nil {
return err
}

newSE, err := mutate.AttachAttestationToEntity(se, sig)
if err != nil {
return err
}

// Publish the attestations associated with this entity
err = ociremote.WriteAttestations(digest.Repository, newSE)
if err != nil {
return err
}

prog.SetCompleted()

bus.Publish(partybus.Event{
Type: event.Exit,
Value: func() error {
_, err := os.Stdout.Write(signedPayload)
return err
return nil
},
})

return nil
}

func findValidDigest(digests []string) string {
// since we are only using the OCI repo provider for this source we are safe that this is only 1 value
// see https://github.com/anchore/stereoscope/blob/25ebd49a842b5ac0a20c2e2b4b81335b64ad248c/pkg/image/oci/registry_provider.go#L57-L63
split := strings.Split(digests[0], "sha256:")
return split[1]
}

func formatPredicateType(format sbom.Format) string {
switch format.ID() {
case spdx22json.ID:
Expand All @@ -214,3 +324,33 @@ func formatPredicateType(format sbom.Format) string {
return ""
}
}

type tlogUploadFn func(*client.Rekor, []byte) (*models.LogEntryAnon, error)

func uploadToTlog(ctx context.Context, sv *sign.SignerVerifier, rekorURL string, upload tlogUploadFn) (*cbundle.RekorBundle, error) {
var rekorBytes []byte
// Upload the cert or the public key, depending on what we have
if sv.Cert != nil {
rekorBytes = sv.Cert
} else {
pemBytes, err := sigs.PublicKeyPem(sv, signatureoptions.WithContext(ctx))
if err != nil {
return nil, err
}
rekorBytes = pemBytes
}

rekorClient, err := rekor.NewClient(rekorURL)
if err != nil {
return nil, err
}
entry, err := upload(rekorClient, rekorBytes)
if err != nil {
return nil, err
}

if entry.LogIndex != nil {
log.Debugf("transparency log entry created with index: %v", *entry.LogIndex)
}
return cbundle.EntryToBundle(entry), nil
}
55 changes: 52 additions & 3 deletions cmd/syft/cli/options/attest.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,72 @@ import (
"github.com/spf13/viper"
)

const defaultKeyFileName = "cosign.key"

type AttestOptions struct {
Key string
Key string
Cert string
CertChain string
NoUpload bool
Force bool
Recursive bool

Rekor RekorOptions
Fulcio FulcioOptions
OIDC OIDCOptions
}

var _ Interface = (*AttestOptions)(nil)

func (o *AttestOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
cmd.PersistentFlags().StringVarP(&o.Key, "key", "", "cosign.key",
if err := o.Rekor.AddFlags(cmd, v); err != nil {
return err
}
if err := o.Fulcio.AddFlags(cmd, v); err != nil {
return err
}
if err := o.OIDC.AddFlags(cmd, v); err != nil {
return err
}

cmd.Flags().StringVarP(&o.Key, "key", "", defaultKeyFileName,
"path to the private key file to use for attestation")

return bindAttestationConfigOptions(cmd.PersistentFlags(), v)
cmd.Flags().StringVarP(&o.Cert, "cert", "", "",
"path to the x.509 certificate in PEM format to include in the OCI Signature")

cmd.Flags().BoolVarP(&o.NoUpload, "no-upload", "", false,
"do not upload the generated attestation")

cmd.Flags().BoolVarP(&o.Force, "force", "", false,
"skip warnings and confirmations")

cmd.Flags().BoolVarP(&o.Recursive, "recursive", "", false,
"if a multi-arch image is specified, additionally sign each discrete image")

return bindAttestationConfigOptions(cmd.Flags(), v)
}

func bindAttestationConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
if err := v.BindPFlag("attest.key", flags.Lookup("key")); err != nil {
return err
}

if err := v.BindPFlag("attest.cert", flags.Lookup("cert")); err != nil {
return err
}

if err := v.BindPFlag("attest.no-upload", flags.Lookup("no-upload")); err != nil {
return err
}

if err := v.BindPFlag("attest.force", flags.Lookup("force")); err != nil {
return err
}

if err := v.BindPFlag("attest.recursive", flags.Lookup("recursive")); err != nil {
return err
}

return nil
}
Loading