Skip to content

Commit

Permalink
835 - Keyless Support for SBOM Attestations (anchore#910)
Browse files Browse the repository at this point in the history
Co-authored-by: Alex Goodman <alex.goodman@anchore.com>
  • Loading branch information
spiffcs and wagoodman authored May 6, 2022
1 parent 2398469 commit 401a46d
Show file tree
Hide file tree
Showing 24 changed files with 773 additions and 44 deletions.
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
187 changes: 164 additions & 23 deletions cmd/syft/cli/attest/attest.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import (
"encoding/json"
"fmt"
"os"
"strings"

"github.com/wagoodman/go-progress"

"github.com/anchore/stereoscope"
"github.com/anchore/stereoscope/pkg/image"
Expand All @@ -18,15 +19,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 +57,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 +73,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 +161,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 +170,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 +180,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 +211,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 +325,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

0 comments on commit 401a46d

Please sign in to comment.